use std::collections::HashMap;

use chrono_tz::Tz;
use clap::Parser;

use vfile::transport::output::output_transport::OutputTransport;
use vfile::transport::streaming::streaming_transport::StreamingTransport;

use crate::chopper::error::Result;
use crate::chopper::types::TimestampRange;
use crate::cli::chop_args::{ChopArgs, ChopCsvInArgs, ChopCsvOutArgs};
use crate::cli::config::Config;
use crate::cli::util::YesNo;
use crate::driver::driver::drive;
use crate::input::builder::chopper::ChopperInputFactoryBuilder;
use crate::input::input::{Input, InputFormat, InputType};
use crate::output::builder::OutputFactoryBuilder;
use crate::output::output::{Output, OutputFormat, OutputType};
use crate::sink::csv_output_config::{CSVOutputConfig, TimestampStyle};
use crate::source::csv_input_config::CSVInputConfig;
use crate::source::csv_timestamp_config::{
    TimestampColConfig, TimestampConfig, TimestampFmtConfig,
};
use crate::source::source_factory::ChopperSourceFactory;
use crate::util::dc_factory::DCFactory;
use crate::util::timestamp_units::TimestampUnits;
use crate::util::tz::ChopperTz;

pub struct Chop {
    streaming_transports: Option<Vec<Box<dyn StreamingTransport>>>,
    output_transports: Option<Vec<Box<dyn OutputTransport>>>,
    source_factories: Option<Vec<Box<ChopperSourceFactory>>>,
    timezone_map: Option<HashMap<String, Tz>>,
    dc_factory: Option<DCFactory>,
}

impl Chop {
    pub fn new() -> Chop {
        Chop {
            streaming_transports: None,
            output_transports: None,
            source_factories: None,
            timezone_map: None,
            dc_factory: None,
        }
    }

    pub async fn run(self) -> Result<()> {
        let config = parse_cli_args(
            self.streaming_transports,
            self.output_transports,
            self.source_factories,
            self.timezone_map,
            self.dc_factory,
        )?;
        drive(config).await
    }

    pub fn with_streaming_transports(
        mut self,
        streaming_transports: Vec<Box<dyn StreamingTransport>>,
    ) -> Self {
        self.streaming_transports = Some(streaming_transports);
        self
    }

    pub fn with_output_transports(
        mut self,
        output_transports: Vec<Box<dyn OutputTransport>>,
    ) -> Self {
        self.output_transports = Some(output_transports);
        self
    }

    pub fn with_source_factories(
        mut self,
        source_factories: Vec<Box<ChopperSourceFactory>>,
    ) -> Self {
        self.source_factories = Some(source_factories);
        self
    }

    pub fn with_timezone_map(mut self, timezone_map: HashMap<String, Tz>) -> Self {
        self.timezone_map = Some(timezone_map);
        self
    }

    pub fn with_dc_factory(mut self, dc_factory: DCFactory) -> Self {
        self.dc_factory = Some(dc_factory);
        self
    }
}

pub fn parse_cli_args(
    streaming_transports: Option<Vec<Box<dyn StreamingTransport>>>,
    output_transports: Option<Vec<Box<dyn OutputTransport>>>,
    source_factories: Option<Vec<Box<ChopperSourceFactory>>>,
    timezone_map: Option<HashMap<String, Tz>>,
    dc_factory: Option<DCFactory>,
) -> Result<Config> {
    let args: ChopArgs = ChopArgs::parse();

    let timezone = ChopperTz::new_from_cli_arg(args.timezone, timezone_map)?;

    let timestamp_range = TimestampRange::new(args.begin, args.end, &timezone)?;

    let mut input_formats: Vec<InputFormat> = Vec::new();
    match args.format {
        None => input_formats.push(InputFormat::Auto),
        Some(formats) => {
            for format in formats {
                input_formats.push(if format == "auto" {
                    InputFormat::Auto
                } else {
                    InputFormat::Extension(format)
                });
            }
        }
    };
    assert!(!input_formats.is_empty());
    let last_format = input_formats.last().unwrap();

    let mut inputs: Vec<Input> = Vec::new();
    if args.input.is_empty() {
        //TODO: add check here that there is only one format provided
        inputs.push(Input {
            input: InputType::StdIn,
            format: input_formats[0].clone(),
        });
    } else {
        //TODO: add check here that number of formats provided is no more than number of inputs
        // it's okay if it's less, because last format will be used for rest of inputs
        for (i, str) in args.input.iter().enumerate() {
            inputs.push(Input {
                input: InputType::Location(str.into()),
                format: if i < input_formats.len() {
                    input_formats[i].clone()
                } else {
                    last_format.clone()
                },
            });
        }
    };
    assert!(!inputs.is_empty());

    let output_format = match args.output_format {
        None => OutputFormat::Auto,
        Some(ext) => OutputFormat::Extension(ext),
    };
    let output_type = match args.output {
        None => OutputType::StdOut,
        Some(output) => OutputType::Location(output.into()),
    };
    let output = Output {
        output: output_type,
        format: output_format,
    };

    // csv only
    let csv_input_config = parse_csv_input_config(args.csv_in, timezone.clone())?;
    let csv_output_config = parse_csv_output_config(args.csv_out, timezone);

    let input_factory = ChopperInputFactoryBuilder::new()
        .with_csv_input_config(csv_input_config)
        .with_user_source_factories(source_factories)
        .with_user_streaming_transports(streaming_transports)
        .with_dc_factory(dc_factory.clone())
        .build()?;

    let output_factory = OutputFactoryBuilder::new()
        .with_csv_output_config(csv_output_config)
        .with_dc_factory(dc_factory)
        .with_user_output_transports(output_transports)
        .build()?;

    Ok(Config {
        inputs,
        output,
        input_factory,
        output_factory,
        timestamp_range,
    })
}

fn parse_csv_input_config(args: ChopCsvInArgs, timezone: ChopperTz) -> Result<CSVInputConfig> {
    let input_delimiter = args.csv_in_delimiter;
    let has_header = args.csv_in_has_header;

    let ts_fmt = match args.csv_in_ts_fmt_date {
        None => match args.csv_in_ts_fmt {
            None => match args.csv_in_epoch {
                None => TimestampFmtConfig::Auto,
                Some(units) => TimestampFmtConfig::Epoch(TimestampUnits::from_short_str(&units)),
            },
            Some(fmt) => TimestampFmtConfig::Explicit(fmt),
        },
        Some(date_fmt) => {
            // we should always have time format if we have date format
            let time_fmt = args.csv_in_ts_fmt_time.unwrap();
            TimestampFmtConfig::DateTimeExplicit(date_fmt, time_fmt)
        }
    };

    let ts_col = match args.csv_in_ts_col_date {
        None => match args.csv_in_ts_col {
            None => TimestampColConfig::Auto,
            Some(ts) => match ts.parse::<usize>() {
                Ok(i) => TimestampColConfig::Index(i),
                Err(_) => TimestampColConfig::Name(ts),
            },
        },
        Some(date_col) => {
            // we should always have time column if we have date column
            let time_col = args.csv_in_ts_col_time.unwrap();
            match date_col.parse::<usize>() {
                Ok(d_idx) => match time_col.parse::<usize>() {
                    Ok(t_idx) => TimestampColConfig::DateTimeIndex(d_idx, t_idx),
                    Err(_) => TimestampColConfig::DateTimeName(date_col, time_col),
                },
                Err(_) => TimestampColConfig::DateTimeName(date_col, time_col),
            }
        }
    };
    let ts_config = TimestampConfig::new(ts_col, ts_fmt, timezone);

    Ok(CSVInputConfig::new(ts_config)
        .with_delimiter(input_delimiter)?
        .with_header(has_header))
}

fn parse_csv_output_config(args: ChopCsvOutArgs, timezone: ChopperTz) -> CSVOutputConfig {
    let csv_out_delimiter = args.csv_out_delimiter;
    if csv_out_delimiter.len() != 1 {
        panic!(
            "csv out delimiter should be single character, but got '{}' instead",
            csv_out_delimiter
        );
    }
    let csv_out_delimiter = csv_out_delimiter.chars().next().unwrap();

    //TODO this should probably be YesNoAuto, with Auto default, where
    // if output format is csv, then extra time column is not generated
    let csv_out_print_time_col = match args.csv_out_print_time_col {
        YesNo::Yes => true,
        YesNo::No => false,
    };

    let time_col_name = args.csv_out_time_col_name;

    let time_col_style = if args.csv_out_time_fmt_epoch {
        TimestampStyle::Epoch
    } else {
        TimestampStyle::HumanReadable
    };

    let time_col_units = TimestampUnits::from_short_str(&args.csv_out_time_col_units);

    CSVOutputConfig::new(
        csv_out_delimiter,
        csv_out_print_time_col,
        time_col_name,
        time_col_style,
        time_col_units,
        timezone,
    )
}
