use crate::gnucash::{GCTransaction, GCTransactionBuffer};
use serde::Deserialize;
use std::error::Error;

// By default, struct field names are deserialized based on the position of
// a corresponding field in the CSV data's header record.
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct IcaTransaction {
    datum: String,
    text: String,
    typ: String,
    budgetgrupp: String,
    belopp: String,
    saldo: Option<String>,
}

pub fn parse_csv(args: Vec<String>) -> Result<(), Box<dyn Error>> {
    let input_file = &args[1];
    let output_file = &args[2];
    // If we get an argument, this is the special case when creating
    // CSV based on Utdrag PDF files.
    // There the order is sequential, with first entry first
    let utdrag_entry_order = !&args[3].is_empty();
    // Read the CSV file
    // assuming it has a header
    #[cfg(not(feature = "pipe"))]
    let mut rdr = csv::ReaderBuilder::new()
        .has_headers(true)
        .delimiter(b';')
        .trim(csv::Trim::All)
        .from_path(input_file)?;

    #[cfg(feature = "pipe")]
    let mut rdr = csv::ReaderBuilder::new()
        .has_headers(true)
        .delimiter(b';')
        .trim(csv::Trim::All)
        .from_reader(io::stdin());

    #[cfg(not(feature = "pipe"))]
    // Truncates any pre-existing file
    let mut wtr = csv::Writer::from_path(output_file)?;

    #[cfg(feature = "pipe")]
    let mut wtr = csv::Writer::from_writer(io::stdout());

    // Write headers manually.
    wtr.write_record(GCTransaction::headers())?;

    let mut last_seen_date = "".to_string();

    let mut transaction_buffer = GCTransactionBuffer::new();

    // The file is read from top to bottom, ICA exports
    // the most recent transactions at the top
    // thus the deserialization goes "backwards" in time
    //
    // This is of special importance when parsing transaction_number
    // with multiple transactions during the same day
    for result in rdr.deserialize() {
        // Notice that we need to provide a type hint for automatic
        // deserialization.
        let record: IcaTransaction = result?;

        // Match based on action which account
        // Default match on Brukskonto
        let account_name = match record.typ.as_str() {
            "E-faktura" => "Tillgångar:Likvida Medel:Brukskonto",
            "Pg-bg" => "Tillgångar:Likvida Medel:Brukskonto",
            "Autogiro" => "Tillgångar:Likvida Medel:Brukskonto",
            "Korttransaktion" => "Tillgångar:Likvida Medel:Brukskonto",
            "Uttag" => "Tillgångar:Likvida Medel:Brukskonto",
            "Utlandsbetalning" => "Tillgångar:Likvida Medel:Brukskonto",
            "Försäkring" => "Tillgångar:Likvida Medel:Brukskonto",
            "Fritid" => "Tillgångar:Likvida Medel:Brukskonto",
            "Övrigt" => "Tillgångar:Likvida Medel:Brukskonto",
            "Reserverat Belopp" => "Tillgångar:Likvida Medel:Brukskonto",
            "Insättning" => "Tillgångar:Likvida Medel:Brukskonto",
            _ => "Tillgångar:Likvida Medel:Brukskonto",
        };
        let value_num = record
            .belopp
            .replace(" kr", "")
            .replace(',', ".")
            .chars()
            .filter(|c| c.is_ascii())
            .filter(|c| !c.is_whitespace())
            .collect::<String>()
            .parse::<f32>()
            .unwrap();

        let amount_balance = record.saldo.map(|saldo| {
            saldo
                .replace(" kr", "")
                .replace(',', ".")
                .chars()
                .filter(|c| c.is_ascii())
                .filter(|c| !c.is_whitespace())
                .collect::<String>()
                .parse::<f32>()
                .unwrap()
        });

        // If this is the second time a date appears
        if last_seen_date == record.datum {
            // Store the record in write buffer
        } else {
            // A fresh new date, print all buffer contents
            write_csv(&mut wtr, &mut transaction_buffer, utdrag_entry_order)?;

            // Set the last seen date
            last_seen_date = record.datum.clone();
        }

        transaction_buffer.push_back(GCTransaction {
            // Date
            date: record.datum,
            // Transaction ID, let GnuCash generate
            transaction_id: "".into(),
            // Number, 1 by default,
            // if multiple in a day this gets overwritten
            number: 1.to_string(),
            // Description
            description: Some(record.text.to_owned()),
            // Notes
            // Extra, the account balance stored in a note
            notes: amount_balance.map(|value| value.to_string().replace('.', ",")),
            // Currency
            commodity_currency: Some("CURRENCY::SEK".into()),
            // Void Reason
            void_reason: None,
            // Action
            action: Some(record.typ),
            // Memo
            memo: Some(record.budgetgrupp),
            // Full Account Name,
            full_account_name: Some(account_name.to_owned()),
            // Account Name,
            account_name: None,
            // Amount With Sym.,
            amount_with_sym: None,
            // Amount Num,
            amount_num: Some(value_num),
            // Value With Sym.,
            value_with_sym: None,
            // Value Num,
            value_num: Some(value_num),
            // Reconcile,
            reconcile: Some("n".to_owned()),
            // Reconcile Date,
            reconcile_date: None,
            // Rate/Price,
            rate_price: Some("1".to_string()),
        });
    }

    // Make sure any unprinted lines still in the buffer gets printed
    write_csv(&mut wtr, &mut transaction_buffer, utdrag_entry_order)?;

    Ok(())
}

fn write_csv(
    wtr: &mut csv::Writer<std::fs::File>,
    buf: &mut GCTransactionBuffer,
    utdrag_entry_order: bool,
) -> Result<(), Box<dyn Error>> {
    // Iterate in reverse order to assign correct transaction_number
    if utdrag_entry_order {
        for (counter, row) in buf.iter_mut().enumerate() {
            row.set_number((counter + 1).to_string());
        }
        // Write CSV in original order, most recent first
        while let Some(row) = buf.pop_front() {
            wtr.write_record(row.output())?;
        }
    } else {
        for (counter, row) in buf.iter_mut().rev().enumerate() {
            row.set_number((counter + 1).to_string());
        }
        // Write CSV in original order, most recent first
        while let Some(row) = buf.pop_front() {
            wtr.write_record(row.output())?;
        }
    }
    Ok(())
}