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

use csv::{self, StringRecord};
use rust_xlsxwriter::Workbook;
use serde_json::{json, Value};

use crate::config::Config;
use crate::util;
use crate::CliError;
use crate::CliResult;

const MAX_SAFE_INTEGER: i64 = 9007199254740991;

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:
    --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_nulls: bool,
    flag_omit: bool,
}

impl Args {
    fn make_json(
        &self,
        record: &StringRecord,
        headers: &StringRecord,
        json_object: &mut serde_json::Map<String, Value>,
    ) {
        for (header, value) in headers.iter().zip(record.iter()) {
            if let Ok(parsed_value) = value.parse::<i64>() {
                if parsed_value.abs() < MAX_SAFE_INTEGER {
                    json_object.insert(header.to_string(), json!(parsed_value as f64));
                    continue;
                }
            } else if let Ok(parsed_value) = value.parse::<f64>() {
                json_object.insert(header.to_string(), json!(parsed_value));
                continue;
            }

            if self.flag_nulls && value.is_empty() {
                json_object.insert(header.to_string(), json!(Value::Null));
                continue;
            } else if self.flag_omit && value.is_empty() {
                json_object.remove(header);
                continue;
            }
            json_object.insert(header.to_string(), json!(value));
        }
    }

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

        let mut json_array = Vec::new();

        while rdr.read_record(&mut record)? {
            self.make_json(&record, &headers, &mut json_object);

            json_array.push(Value::Object(json_object.clone()));
        }
        let _ = 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 record = csv::StringRecord::new();
        let mut json_object = serde_json::Map::new();

        while rdr.read_record(&mut record)? {
            self.make_json(&record, &headers, &mut json_object);

            writeln!(
                writer,
                "{}",
                serde_json::to_string(&json_object).map_err(|e| CliError::Other(e.to_string()))?
            )?;
        }

        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 {
                return fail!(
                    "could not export in xlsx without a path, use -o, --output or pipe the result!"
                );
            }
        }
        _ => return fail!("could not export the file into this format!"),
    }

    Ok(())
}