use std::collections::HashMap;

use chrono_tz::Tz;
use clap::{value_t, ArgMatches};

use crate::chopper::error::ChopperResult;
use crate::chopper::types::TimestampRange;
use crate::cli::config::Config;
use crate::cli::util::YesNoAuto;
use crate::cli_app::CliApp;
use crate::driver::driver::drive;
use crate::input::input::{Input, InputFormat, InputType};
use crate::input::input_factory::InputFactoryBuilder;
use crate::source::csv_input_config::CSVInputConfig;
use crate::source::csv_timestamp_config::{
    TimestampColConfig, TimestampConfig, TimestampFmtConfig,
};
use crate::source::source_factory::SourceFactory;
use crate::transport::streaming::streaming_transport::StreamingTransport;
use crate::util::dc_factory::DCFactory;
use crate::util::timestamp_units::TimestampUnits;
use crate::util::tz::ChopperTz;
use crate::write::csv_output_config::{CSVOutputConfig, TimestampStyle};
use crate::write::factory::OutputFactory;

pub struct ChopperCli {
    streaming_transports: Option<Vec<Box<dyn StreamingTransport>>>,
    source_factories: Option<Vec<Box<dyn SourceFactory>>>,
    timezone_map: Option<HashMap<String, Tz>>,
    dc_factory: Option<DCFactory>,
}

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

    pub async fn run(self) -> ChopperResult<()> {
        let config = parse_cli_args(
            self.streaming_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_source_factories(mut self, source_factories: Vec<Box<dyn SourceFactory>>) -> 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>>>,
    source_factories: Option<Vec<Box<dyn SourceFactory>>>,
    timezone_map: Option<HashMap<String, Tz>>,
    dc_factory: Option<DCFactory>,
) -> ChopperResult<Config> {
    let matches = CliApp.create_cli_app().get_matches();

    let timezone = ChopperTz::new_from_cli_arg(matches.value_of("timezone"), timezone_map)?;

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

    let mut input_formats: Vec<InputFormat> = Vec::new();
    match matches.values_of("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.to_string())
                });
            }
        }
    };
    assert!(!input_formats.is_empty());
    let last_format = input_formats.last().unwrap();

    let mut inputs: Vec<Input> = Vec::new();
    match matches.values_of("input") {
        //TODO: add check here that there is only one format provided
        None => inputs.push(Input {
            input: InputType::StdIn,
            format: input_formats[0].clone(),
        }),
        //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
        Some(input_strings) => {
            for (i, str) in input_strings.enumerate() {
                inputs.push(Input {
                    input: InputType::Path(str.to_string()),
                    format: if i < input_formats.len() {
                        input_formats[i].clone()
                    } else {
                        last_format.clone()
                    },
                });
            }
        }
    };
    assert!(!inputs.is_empty());

    let output = matches.value_of("output").map(|s| s.to_string());

    // csv only
    let csv_input_config = parse_csv_input_config(&matches, timezone.clone())?;
    let csv_output_config = parse_csv_output_config(&matches, timezone);

    let input_factory = InputFactoryBuilder::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 = OutputFactory::new()
        .with_csv_output_config(csv_output_config)
        .with_dc_factory(dc_factory);

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

fn parse_csv_input_config(
    matches: &ArgMatches,
    timezone: ChopperTz,
) -> ChopperResult<CSVInputConfig> {
    let input_delimiter = matches.value_of("csv_in_delimiter");
    let has_header = value_t!(matches, "csv_in_has_header", YesNoAuto)?;

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

    let ts_col = match matches.value_of("csv_in_ts_col_date") {
        None => match matches.value_of("csv_in_ts_col") {
            None => TimestampColConfig::Auto,
            Some(ts) => match ts.parse::<usize>() {
                Ok(i) => TimestampColConfig::Index(i),
                Err(_) => TimestampColConfig::Name(ts.to_string()),
            },
        },
        Some(date_col) => {
            // we should always have time column if we have date column
            let time_col = matches.value_of("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.to_owned(), time_col.to_owned())
                    }
                },
                Err(_) => {
                    TimestampColConfig::DateTimeName(date_col.to_owned(), time_col.to_owned())
                }
            }
        }
    };
    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(matches: &ArgMatches, timezone: ChopperTz) -> CSVOutputConfig {
    let csv_out_delimiter = matches.value_of("csv_out_delimiter").unwrap();
    let csv_out_print_time_col = match matches.value_of("csv_out_print_time_col").unwrap() {
        "yes" => true,
        "no" => false,
        _ => unreachable!(),
    };

    let time_col_name = match matches.value_of("csv_out_time_col_name") {
        None => None,
        Some(name) => Some(name.to_owned()),
    };

    let time_col_style = if matches.is_present("csv_out_time_fmt_epoch") {
        TimestampStyle::Epoch
    } else {
        TimestampStyle::HumanReadable
    };

    let time_col_units = match matches.value_of("csv_out_time_col_units") {
        None => TimestampUnits::Nanos,
        Some(units) => TimestampUnits::from_str(units),
    };

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