use std::io;

use vfile::compress::compress::compress_if_possible;
use vfile::transport::output::file::FileTransport;
use vfile::transport::output::output_transport::OutputTransport;
use vfile::transport::output::writer_factory::WriterTransportFactory;
use vfile::util::buf::writer::BufWriter;

use crate::chopper::error::{Error, Result};
use crate::chopper::types::Header;
use crate::output::output::{Output, OutputFormat, OutputType};
use crate::sink::csv_output_config::CSVOutputConfig;
use crate::sink::csv_sink;
use crate::sink::sink::Sink;
use crate::util::dc_factory::DCFactory;

pub struct OutputFactory {
    dc_factory: Option<DCFactory>,
    csv_output_config: CSVOutputConfig,
    writer_transport_factory: WriterTransportFactory,
}

impl OutputFactory {
    pub fn new(
        dc_factory: Option<DCFactory>,
        csv_output_config: Option<CSVOutputConfig>,
        user_output_transports: Option<Vec<Box<dyn OutputTransport>>>,
    ) -> OutputFactory {
        let csv_output_config = csv_output_config.unwrap_or_else(|| CSVOutputConfig::new_default());

        let mut default_output_transports = create_default_output_transports();
        let output_transports: Vec<Box<dyn OutputTransport>> = match user_output_transports {
            Some(mut t) => {
                t.append(&mut default_output_transports);
                t
            }
            None => default_output_transports,
        };
        let writer_transport_factory = WriterTransportFactory::new(output_transports);

        OutputFactory {
            dc_factory,
            csv_output_config,
            writer_transport_factory,
        }
    }

    fn create_writer_inner(&self, output: &Output) -> Result<(BufWriter, String)> {
        let writer = match &output.output {
            OutputType::Location(loc) => {
                let writer = self.writer_transport_factory.create_writer(loc)?;
                writer.ok_or_else(|| {
                    std::io::Error::new(
                        std::io::ErrorKind::NotFound,
                        format!("no transport for {} could be found", loc),
                    )
                })?
            }
            OutputType::StdOut => BufWriter::new(io::stdout()),
        };

        let format = match &output.format {
            OutputFormat::Extension(ext) => ext.to_string(),
            OutputFormat::Auto => match &output.output {
                OutputType::Location(loc) => match &loc.file_name() {
                    None => {
                        return Err(Error::from(format!(
                            "missing file name on {}, needed to auto-detect output format",
                            loc
                        )))
                    }
                    Some(ext) => ext.to_string(),
                },
                OutputType::StdOut => "csv".to_string(),
            },
        };

        Ok(compress_if_possible(writer, &format)?)
    }

    pub fn create_writer(&self, output: &Output) -> Result<BufWriter> {
        let (writer, _) = self.create_writer_inner(output)?;
        Ok(writer)
    }

    pub fn create_sink(&self, header: &Header, output: &Output) -> Result<Box<dyn Sink>> {
        let (writer, format) = self.create_writer_inner(output)?;
        let ext = format.rsplit('.').next().unwrap();
        self.create_sink_by_ext(writer, header, ext)
    }

    fn create_sink_by_ext(
        &self,
        writer: BufWriter,
        header: &Header,
        ext: &str,
    ) -> Result<Box<dyn Sink>> {
        Ok(match ext {
            "csv" => Box::new(csv_sink::CSVSink::new(
                header,
                writer,
                self.csv_output_config.clone(),
            )?),
            "dc" => match &self.dc_factory {
                None => return Err(Error::DCFactoryMissing),
                Some(dc_factory) => Box::new(dc_factory.new_sink(header, writer)?),
            },
            _ => {
                return Err(Error::from(format!(
                    "output with extension {} is not supported",
                    ext
                )))
            }
        })
    }
}

fn create_default_output_transports() -> Vec<Box<dyn OutputTransport>> {
    vec![Box::new(FileTransport)]
}
