use std::fs;
use std::io::{self, IsTerminal, Read, Write};
use std::num::NonZeroUsize;

use rust_xlsxwriter::Workbook;
use serde_json::Value;

use crate::config::Config;
use crate::json::{JSONEmptyMode, JSONTypeInferrenceBuffer};
use crate::util;
use crate::CliResult;

static USAGE: &str = "
Convert a CSV file to a variety of data formats.

Usage:
    xan to <format> [options] [<input>]
    xan to --help

Supported formats:
    json    - JSON array or object
    ndjson  - Newline-delimited JSON
    jsonl   - Newline-delimited JSON
    xlsx    - Excel spreasheet

JSON options:
    -B, --buffer-size <size>  Number of CSV rows to sample to infer column types.
                              [default: 512]
    --nulls                   Convert empty string to a null value.
    --omit                    Ignore the empty values.

Common options:
    -h, --help             Display this message
    -o, --output <file>    Write output to <file> instead of stdout.
";

#[derive(Deserialize)]
struct Args {
    arg_format: String,
    arg_input: Option<String>,
    flag_output: Option<String>,
    flag_buffer_size: NonZeroUsize,
    flag_nulls: bool,
    flag_omit: bool,
}

impl Args {
    fn json_empty_mode(&self) -> JSONEmptyMode {
        if self.flag_nulls {
            JSONEmptyMode::Null
        } else if self.flag_omit {
            JSONEmptyMode::Omit
        } else {
            JSONEmptyMode::Empty
        }
    }

    fn convert_to_json<R: Read, W: Write>(
        &self,
        mut rdr: csv::Reader<R>,
        writer: W,
    ) -> CliResult<()> {
        let headers = rdr.headers()?.clone();

        let mut inferrence_buffer = JSONTypeInferrenceBuffer::new(
            headers.len(),
            self.flag_buffer_size.get(),
            self.json_empty_mode(),
        );

        inferrence_buffer.read(&mut rdr)?;

        let mut json_object = serde_json::Map::new();
        let mut json_array = Vec::new();

        for record in inferrence_buffer.records() {
            inferrence_buffer.mutate_json_map(&mut json_object, &headers, record);
            json_array.push(Value::Object(json_object.clone()));
        }

        let mut record = csv::StringRecord::new();

        while rdr.read_record(&mut record)? {
            inferrence_buffer.mutate_json_map(&mut json_object, &headers, &record);
            json_array.push(Value::Object(json_object.clone()));
        }

        serde_json::to_writer_pretty(writer, &json_array)?;

        Ok(())
    }

    fn convert_to_ndjson<R: Read, W: Write>(
        &self,
        mut rdr: csv::Reader<R>,
        mut writer: W,
    ) -> CliResult<()> {
        let headers = rdr.headers()?.clone();
        let mut inferrence_buffer = JSONTypeInferrenceBuffer::new(
            headers.len(),
            self.flag_buffer_size.get(),
            self.json_empty_mode(),
        );

        inferrence_buffer.read(&mut rdr)?;

        let mut json_object = serde_json::Map::new();

        for record in inferrence_buffer.records() {
            inferrence_buffer.mutate_json_map(&mut json_object, &headers, record);
            writeln!(writer, "{}", serde_json::to_string(&json_object)?)?;
        }

        let mut record = csv::StringRecord::new();

        while rdr.read_record(&mut record)? {
            inferrence_buffer.mutate_json_map(&mut json_object, &headers, &record);
            writeln!(writer, "{}", serde_json::to_string(&json_object)?)?;
        }

        Ok(())
    }

    fn convert_to_xlsx<R: Read>(
        mut rdr: csv::Reader<R>,
        mut writer: Box<dyn Write>,
    ) -> CliResult<()> {
        let mut workbook = Workbook::new();
        let headers = rdr.headers()?.clone();
        let worksheet = workbook.add_worksheet();

        for (col, header) in headers.iter().enumerate() {
            worksheet.write_string(0, col as u16, header)?;
        }

        for (row, value) in rdr.records().enumerate() {
            let record = value?;
            for (col, field) in record.iter().enumerate() {
                worksheet.write_string((row + 1) as u32, col as u16, field)?;
            }
        }

        let mut cursor = io::Cursor::new(Vec::new());
        workbook.save_to_writer(&mut cursor)?;
        let buf = cursor.into_inner();
        writer.write_all(&buf)?;

        writer.flush()?;
        Ok(())
    }
}

pub fn run(argv: &[&str]) -> CliResult<()> {
    let args: Args = util::get_args(USAGE, argv)?;
    let conf = Config::new(&args.arg_input);
    let rdr = conf.reader()?;

    let writer: Box<dyn Write> = match &args.flag_output {
        Some(output_path) => Box::new(fs::File::create(output_path)?),
        None => Box::new(io::stdout()),
    };

    match args.arg_format.as_str() {
        "json" => Args::convert_to_json(&args, rdr, writer)?,
        "jsonl" | "ndjson" => Args::convert_to_ndjson(&args, rdr, writer)?,
        "xlsx" => {
            if args.flag_output.is_some() || !io::stdout().is_terminal() {
                Args::convert_to_xlsx(rdr, writer)?;
            } else {
                Err(
                    "could not export in xlsx without a path, use -o, --output or pipe the result!",
                )?;
            }
        }
        _ => Err("could not export the file into this format!")?,
    }

    Ok(())
}