/* Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

use super::kind::BenchmarkKindCommand;
use super::output::BenchmarkOutputCommand;
use super::props::{BenchmarkKindProps, BenchmarkTransportProps};
use super::{
    defaults::{
        DEFAULT_MESSAGE_BATCHES, DEFAULT_MESSAGE_SIZE, DEFAULT_MESSAGES_PER_BATCH,
        DEFAULT_MOVING_AVERAGE_WINDOW, DEFAULT_SAMPLING_TIME, DEFAULT_WARMUP_TIME,
    },
    transport::BenchmarkTransportCommand,
};
use bench_report::benchmark_kind::BenchmarkKind;
use bench_report::numeric_parameter::BenchmarkNumericParameter;
use clap::error::ErrorKind;
use clap::{CommandFactory, Parser};
use iggy::prelude::{
    DEFAULT_ROOT_PASSWORD, DEFAULT_ROOT_USERNAME, IggyByteSize, IggyDuration, IggyExpiry,
    TransportProtocol,
};
use std::num::NonZeroU32;
use std::str::FromStr;

#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
#[allow(clippy::struct_excessive_bools)]
pub struct IggyBenchArgs {
    /// Benchmark kind
    #[command(subcommand)]
    pub benchmark_kind: BenchmarkKindCommand,

    /// Message size in bytes. Accepts either a single value or a range (e.g. "200..500")
    #[arg(long, short = 'm', value_parser = BenchmarkNumericParameter::from_str, default_value_t = BenchmarkNumericParameter::Value(DEFAULT_MESSAGE_SIZE.get()))]
    pub message_size: BenchmarkNumericParameter,

    /// Number of messages per batch
    #[arg(long, short = 'P', value_parser = BenchmarkNumericParameter::from_str, default_value_t = BenchmarkNumericParameter::Value(DEFAULT_MESSAGES_PER_BATCH.get()))]
    pub messages_per_batch: BenchmarkNumericParameter,

    /// Number of message batches per actor (producer / consumer / producing consumer).
    /// This argument is mutually exclusive with `total_messages_size`.
    #[arg(long, short = 'b', group = "data_to_process")]
    pub message_batches: Option<NonZeroU32>,

    /// Total size of all messages to process in bytes (aggregate, for all actors).
    /// This argument is mutually exclusive with `message_batches`.
    #[arg(long, short = 'T', group = "data_to_process")]
    pub total_data: Option<IggyByteSize>,

    /// Optional total rate limit (aggregate, for all actors)
    /// Accepts human-readable formats like "50KB", "10MB", or "1GB"
    #[arg(long, short = 'r', verbatim_doc_comment)]
    pub rate_limit: Option<IggyByteSize>,

    /// Warmup time in human readable format, e.g. "1s", "2m", "3h"
    #[arg(long, short = 'w', default_value_t = IggyDuration::from_str(DEFAULT_WARMUP_TIME).unwrap())]
    pub warmup_time: IggyDuration,

    /// Sampling time for metrics collection. It is also used as bucket size for time series calculations.
    #[arg(long, short = 't', default_value_t = IggyDuration::from_str(DEFAULT_SAMPLING_TIME).unwrap(), value_parser = IggyDuration::from_str)]
    pub sampling_time: IggyDuration,

    /// Window size for moving average calculations in time series data
    #[arg(long, short = 'W', default_value_t = DEFAULT_MOVING_AVERAGE_WINDOW)]
    pub moving_average_window: u32,

    /// Use high-level API for actors
    #[arg(long, short = 'H', default_value_t = false)]
    pub high_level_api: bool,

    /// Username for server authentication
    #[arg(long, short = 'u', default_value_t = DEFAULT_ROOT_USERNAME.to_string())]
    pub username: String,

    /// Password for server authentication
    #[arg(long, short = 'p', default_value_t = DEFAULT_ROOT_PASSWORD.to_string())]
    pub password: String,

    /// Pretty table output for benchmark results
    #[arg(long, default_value_t = false)]
    pub pretty: bool,

    /// Reuse existing bench streams instead of deleting and recreating them.
    /// Without this flag, existing bench streams are deleted to ensure
    /// consumers start with fresh data and accurate latency measurements.
    #[arg(long, default_value_t = false)]
    pub reuse_streams: bool,
}

impl IggyBenchArgs {
    pub fn transport_command(&self) -> &BenchmarkTransportCommand {
        self.benchmark_kind.transport_command()
    }

    pub fn transport(&self) -> &TransportProtocol {
        self.benchmark_kind.transport_command().transport()
    }

    pub fn nodelay(&self) -> bool {
        self.benchmark_kind.transport_command().nodelay()
    }

    pub fn server_address(&self) -> &str {
        self.benchmark_kind
            .inner()
            .transport_command()
            .server_address()
    }

    pub fn validate(&mut self) {
        if self.output_dir().is_none()
            && (self.gitref().is_some()
                || self.identifier().is_some()
                || self.remark().is_some()
                || self.extra_info().is_some()
                || self.gitref_date().is_some())
        {
            Self::command()
                .error(
                    ErrorKind::ArgumentConflict,
                    "--git-ref, --git-ref-date, --identifier, --remark, --extra-info can only be used with --output-dir",
                )
                .exit();
        }

        if (self.message_batches, self.total_data) == (None, None) {
            self.message_batches = Some(DEFAULT_MESSAGE_BATCHES);
        }

        if let Some(total_data) = self.total_data {
            let samples = total_data.as_bytes_u64() / u64::from(self.message_size().min());
            if samples <= 1 {
                Self::command()
                    .error(
                        ErrorKind::ArgumentConflict,
                        "--total-messages-size must be at least 2x greater than --message-size",
                    )
                    .exit();
            }
        }

        if self.high_level_api && !self.messages_per_batch.is_fixed() {
            Self::command()
                .error(
                    ErrorKind::ArgumentConflict,
                    "High-level consumer API (--high-level-api) requires fixed batch size, but random batch size was specified. Use a single value instead of a range for --messages-per-batch.",
                )
                .exit();
        }

        self.benchmark_kind.inner().validate();
    }

    pub const fn messages_per_batch(&self) -> BenchmarkNumericParameter {
        self.messages_per_batch
    }

    pub const fn message_batches(&self) -> Option<NonZeroU32> {
        self.message_batches
    }

    pub const fn message_size(&self) -> BenchmarkNumericParameter {
        self.message_size
    }

    pub const fn total_data(&self) -> Option<IggyByteSize> {
        self.total_data
    }

    // Used only for generation of unique directory name
    pub fn data_volume_identifier(&self) -> String {
        self.total_data().map_or_else(
            || self.message_batches().unwrap().to_string(),
            |total_messages_size| format!("{}B", total_messages_size.as_bytes_u64()),
        )
    }

    pub fn streams(&self) -> u32 {
        self.benchmark_kind.inner().streams()
    }

    pub fn number_of_partitions(&self) -> u32 {
        self.benchmark_kind.inner().partitions()
    }

    pub fn consumers(&self) -> u32 {
        self.benchmark_kind.inner().consumers()
    }

    pub fn producers(&self) -> u32 {
        self.benchmark_kind.inner().producers()
    }

    pub fn kind(&self) -> BenchmarkKind {
        self.benchmark_kind.as_simple_kind()
    }

    pub fn number_of_consumer_groups(&self) -> u32 {
        self.benchmark_kind.inner().number_of_consumer_groups()
    }

    pub const fn warmup_time(&self) -> IggyDuration {
        self.warmup_time
    }

    pub const fn sampling_time(&self) -> IggyDuration {
        self.sampling_time
    }

    pub const fn moving_average_window(&self) -> u32 {
        self.moving_average_window
    }

    pub const fn rate_limit(&self) -> Option<IggyByteSize> {
        self.rate_limit
    }

    pub fn output_dir(&self) -> Option<String> {
        self.benchmark_kind
            .inner()
            .transport_command()
            .output_command()
            .as_ref()
            .map(|cmd| match cmd {
                BenchmarkOutputCommand::Output(args) => args.output_dir.clone(),
            })
    }

    pub fn identifier(&self) -> Option<String> {
        self.benchmark_kind
            .inner()
            .transport_command()
            .output_command()
            .as_ref()
            .map(|cmd| match cmd {
                BenchmarkOutputCommand::Output(args) => args.identifier.clone(),
            })
    }

    pub fn remark(&self) -> Option<String> {
        self.benchmark_kind
            .inner()
            .transport_command()
            .output_command()
            .as_ref()
            .and_then(|cmd| match cmd {
                BenchmarkOutputCommand::Output(args) => args.remark.clone(),
            })
    }

    pub fn extra_info(&self) -> Option<String> {
        self.benchmark_kind
            .inner()
            .transport_command()
            .output_command()
            .as_ref()
            .and_then(|cmd| match cmd {
                BenchmarkOutputCommand::Output(args) => args.extra_info.clone(),
            })
    }

    pub fn gitref(&self) -> Option<String> {
        self.benchmark_kind
            .inner()
            .transport_command()
            .output_command()
            .as_ref()
            .and_then(|cmd| match cmd {
                BenchmarkOutputCommand::Output(args) => args.gitref.clone(),
            })
    }

    pub fn gitref_date(&self) -> Option<String> {
        self.benchmark_kind
            .inner()
            .transport_command()
            .output_command()
            .as_ref()
            .and_then(|cmd| match cmd {
                BenchmarkOutputCommand::Output(args) => args.gitref_date.clone(),
            })
    }

    pub fn open_charts(&self) -> bool {
        self.benchmark_kind
            .inner()
            .transport_command()
            .output_command()
            .as_ref()
            .is_some_and(|cmd| match cmd {
                BenchmarkOutputCommand::Output(args) => args.open_charts,
            })
    }

    pub fn max_topic_size(&self) -> Option<IggyByteSize> {
        self.benchmark_kind.inner().max_topic_size()
    }

    pub fn message_expiry(&self) -> IggyExpiry {
        self.benchmark_kind.inner().message_expiry()
    }

    pub fn read_amplification(&self) -> Option<f32> {
        self.benchmark_kind.inner().read_amplification()
    }

    pub const fn high_level_api(&self) -> bool {
        self.high_level_api
    }

    pub const fn reuse_streams(&self) -> bool {
        self.reuse_streams
    }

    pub fn username(&self) -> &str {
        &self.username
    }

    pub fn password(&self) -> &str {
        &self.password
    }

    /// Generates the output directory name based on benchmark parameters.
    pub fn generate_dir_name(&self) -> String {
        let benchmark_kind = match &self.benchmark_kind {
            BenchmarkKindCommand::PinnedProducer(_) => "pinned_producer",
            BenchmarkKindCommand::PinnedConsumer(_) => "pinned_consumer",
            BenchmarkKindCommand::PinnedProducerAndConsumer(_) => "pinned_producer_and_consumer",
            BenchmarkKindCommand::BalancedProducer(_) => "balanced_producer",
            BenchmarkKindCommand::BalancedConsumerGroup(_) => "balanced_consumer_group",
            BenchmarkKindCommand::BalancedProducerAndConsumerGroup(_) => {
                "balanced_producer_and_consumer"
            }
            BenchmarkKindCommand::EndToEndProducingConsumer(_) => "end_to_end_producing_consumer",
            BenchmarkKindCommand::EndToEndProducingConsumerGroup(_) => {
                "end_to_end_producing_consumer_group"
            }
            BenchmarkKindCommand::Examples => unreachable!(),
        };

        let transport = match self.transport_command() {
            BenchmarkTransportCommand::Tcp(_) => "tcp",
            BenchmarkTransportCommand::Quic(_) => "quic",
            BenchmarkTransportCommand::Http(_) => "http",
            BenchmarkTransportCommand::WebSocket(_) => "ws",
        };

        let actors = match &self.benchmark_kind {
            BenchmarkKindCommand::PinnedProducer(_)
            | BenchmarkKindCommand::BalancedProducer(_)
            | BenchmarkKindCommand::EndToEndProducingConsumer(_)
            | BenchmarkKindCommand::EndToEndProducingConsumerGroup(_) => self.producers(),
            BenchmarkKindCommand::PinnedConsumer(_)
            | BenchmarkKindCommand::BalancedConsumerGroup(_) => self.consumers(),
            BenchmarkKindCommand::PinnedProducerAndConsumer(_)
            | BenchmarkKindCommand::BalancedProducerAndConsumerGroup(_) => {
                self.producers() + self.consumers()
            }
            BenchmarkKindCommand::Examples => unreachable!(),
        };

        let data_volume_arg = match (self.total_data, self.message_batches) {
            (Some(total), None) => format!("{total}"),
            (None, Some(batches)) => format!("{batches}"),
            _ => unreachable!(),
        };

        let mut parts = vec![
            benchmark_kind.to_string(),
            actors.to_string(),
            self.message_size().to_string(),
            self.messages_per_batch().to_string(),
            data_volume_arg,
            transport.to_string(),
        ];

        if let Some(remark) = &self.remark() {
            parts.push(remark.clone());
        }

        if let Some(gitref) = &self.gitref() {
            parts.push(gitref.clone());
        }

        if let Some(identifier) = &self.identifier() {
            parts.push(identifier.clone());
        }

        parts.join("_")
    }

    /// Generates a human-readable pretty name for the benchmark
    pub fn generate_pretty_name(&self) -> String {
        let consumer_or_producer = match &self.benchmark_kind {
            BenchmarkKindCommand::PinnedProducer(_) | BenchmarkKindCommand::BalancedProducer(_) => {
                format!("{} producers", self.producers())
            }
            BenchmarkKindCommand::PinnedConsumer(_)
            | BenchmarkKindCommand::BalancedConsumerGroup(_) => {
                format!("{} consumers", self.consumers())
            }
            BenchmarkKindCommand::PinnedProducerAndConsumer(_)
            | BenchmarkKindCommand::BalancedProducerAndConsumerGroup(_) => format!(
                "{} producers/{} consumers",
                self.producers(),
                self.consumers()
            ),
            BenchmarkKindCommand::EndToEndProducingConsumer(_) => {
                format!("{} producing consumers", self.producers(),)
            }
            BenchmarkKindCommand::EndToEndProducingConsumerGroup(_) => {
                format!(
                    "{} producing consumers/{} consumer groups",
                    self.producers(),
                    self.consumers()
                )
            }
            BenchmarkKindCommand::Examples => unreachable!(),
        };

        let mut name = format!(
            "{}, {}B msgs, {} msgs/batch",
            consumer_or_producer,
            self.message_size(),
            self.messages_per_batch(),
        );

        if let Some(remark) = &self.remark() {
            name = format!("{name} ({remark})");
        }

        name
    }
}
