use serde::Deserialize;
use std::collections::VecDeque;
use std::env;
use std::error::Error;
use std::process;

// 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>,
}

// 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 GCTransaction {
    date: String,
    transaction_id: String,
    number: String,
    description: Option<String>,
    notes: Option<String>,
    commodity_currency: Option<String>,
    void_reason: Option<String>,
    action: Option<String>,
    memo: Option<String>,
    full_account_name: Option<String>,
    account_name: Option<String>,
    amount_with_sym: Option<String>,
    amount_num: Option<f32>,
    reconcile: Option<String>,
    reconcile_date: Option<String>,
    rate_price: Option<String>,
}

fn parse_csv(args: Vec<String>) -> Result<(), Box<dyn Error>> {
    // Read the CSV file
    // assuming it has a header
    #[cfg(not(pipe))]
    let mut rdr = csv::ReaderBuilder::new()
        .has_headers(true)
        .delimiter(b';')
        .trim(csv::Trim::All)
        .from_path(&args[1])?;

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

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

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

    // Write headers manually.
    wtr.write_record(&[
        "Date",
        "Transaction ID",
        "Number",
        "Description",
        "Notes",
        "Commodity/Currency",
        "Void Reason",
        "Action",
        "Memo",
        "Full Account Name",
        "Account Name",
        "Amount With Sym.",
        "Amount Num",
        "Reconcile",
        "Reconcile Date",
        "Rate/Price",
    ])?;

    let mut last_seen_date = "".to_string();

    let mut transaction_buffer: VecDeque<[String; 16]> = VecDeque::new();

    // The file is read from top to bottom, ICA exorts
    // 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 amount_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)?;

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

        transaction_buffer.push_back([
            // Date
            record.datum,
            // Transaction ID, let GnuCash generate
            "".into(),
            // Number, 1 by default,
            // if multiple in a day this gets overwritten
            1.to_string(),
            // Description
            record.text,
            // Notes
            // Extra, the account balance stored in a note
            match amount_balance {
                Some(value) => value.to_string().replace(".", ","),
                None => "".into(),
            },
            // Currency
            "CURRENCY::SEK".into(),
            // Void Reason
            "".into(),
            // Action
            record.typ,
            // Memo
            "".into(),
            // Full Account Name",
            account_name.into(),
            // Account Name",
            "".into(),
            // Amount With Sym.",
            "".into(),
            // Amount Num",
            amount_num.to_string().replace(".", ","),
            // Reconcile",
            "n".into(),
            // Reconcile Date",
            "".into(),
            // Rate/Price",
            "1".into(),
        ]);
    }

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

    Ok(())
}

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

fn print_help() {
    println!("Error: Missing arguments!");
    println!();
    println!("Usage:");
    println!();
    println!("This program requires two arguments:");
    println!("Argument 1: Input CSV file from ICA Banken");
    println!("Argument 2: Output CSV file formatted for GnuCash import");
    println!();
    println!("Example:");
    println!("icabanken2gnucash ica.csv ica-gnucash.csv");
    println!();
    println!("If compiled with `pipe` feature, pipe in and out instead");
    println!("icabanken2gnucash < ica.csv > ica-gnucash.csv");
}

fn main() {
    let args: Vec<String> = env::args().collect();
    if args.len() < 2 {
        print_help();
        process::exit(1);
    }
    println!(" Input file: {}", args[1]);
    println!("Output file: {}", args[2]);

    if let Err(err) = parse_csv(args) {
        println!("Unable to parse CSV: {}", err);
        process::exit(1);
    } else {
        println!("Done!");
    }
}