//! Main executable for the Argentum Finance program.

pub mod error;
pub mod months;
pub mod readwrite;
pub mod expense;
pub mod config;
pub mod crypt;

use std::{
    env,
    process::exit,
    collections::HashMap,
};

use colored::*;

use expense::{
    Expense,
    get_text,
    get_selection,
};

use months::get_month_name;

use error::Error;

use config::{
    Config,
    Sorting,
};

/// Default filename for storing transactions.
const DEFAULT: &str = "expenses.fin";

fn main() {
    let args = env::args().collect::<Vec<String>>();

    // Get configuration information
    let config: Config = readwrite::config_from_file();
    let filename = &config.filename();

    // With no subcommand, display a help menu
    if args.len() < 2 {
        println!(
            "{}\n\
            An open-source personal finance program\n\
            \n\
            Syntax:\n\
            \tag SUBCOMMAND\n\
            \n\
            Subcommands:\n\
            \tadd - Add an expense to the working directory's `{}` file\n\
            \tremove - Remove an expense from the working directory's `{}` file\n\
            \tamend - Amends an expense in the working directory's `{}` file\n\
            \tencrypt - Encrypts the working directory's `{}` file\n\
            \tdecrypt - Decrypts the working directory's `{}` file\n\
            \treport - Generate an expense report for a given month\n\
            \tlsconfig - List configuration information in the terminal\n\
            \tconfig - Configure Argentum Finance\n\
            ",
            "Argentum Finance".blue().bold(),
            filename,
            filename,
            filename,
            filename,
            filename,
        );
        exit(0);
    }

    let subcommand = args[1].clone();

    let e = config.encrypted();

    match subcommand.as_str() {
        "add" => add_expense(filename, e),
        "remove" => remove_expense(filename, e),
        "report" => create_report(filename, config.sorting(), e),
        "amend" => amend_expense(filename, e),
        "encrypt" => encrypt(filename),
        "decrypt" => decrypt(filename),
        "lsconfig" => print_config(config),
        "config" => configure(),
        _ => Error::InvalidSubcommand.throw(),
    }
}


/// Adds an expense to the file provided.
fn add_expense(filename: &str, encrypted: bool) {
    let (mut expenses, key) = readwrite::expenses_from_bin(filename, encrypted);

    // Get the next available ID number
    let id = if expenses.len() != 0 {
        expenses[expenses.len() - 1].id() + 1
    } else {
        0
    };

    // Interact with the user to get the new expense
    let new_expense = Expense::get(id);

    expenses.push(new_expense);

    match key {
        Some(k) => readwrite::expenses_to_bin_using_key(filename, expenses, k),
        None => readwrite::expenses_to_bin(filename, expenses, encrypted),
    };

    println!("{}\nExpense created.", "Success!".green().bold());
}


/// Removes an expense by unique identifier.
fn remove_expense(filename: &str, encrypted: bool) {
    let (mut expenses, key) = readwrite::expenses_from_bin(filename, encrypted);

    if expenses.len() == 0 {
        println!("No expenses in working directory");
        println!("Argentum exiting");
        exit(0);
    }

    let mut options: Vec<String> = Vec::new();

    // Build a list of printable expenses
    Expense::sort_by_date(&mut expenses);
    for expense in &expenses {
        options.push(expense.to_str());
    }
    let options: Vec<&str> = options.iter().map(|x| x.as_str()).collect();

    // Get the ID from the user
    let raw = get_selection("Select an expense to remove.".to_string(), options);
    let id_str = {
        let first = match raw.split("|").nth(0) {
            Some(f) => f,
            None => Error::InvalidId.throw(),
        };
        &first[3..first.len() - 1]
    };
    let id = match str::parse::<u32>(id_str) {
        Ok(y) => y,
        Err(_) => Error::InvalidId.throw(),
    };

    expenses.retain(|e| e.id() != id);

    match key {
        Some(k) => readwrite::expenses_to_bin_using_key(filename, expenses, k),
        None => readwrite::expenses_to_bin(filename, expenses, encrypted),
    };

    println!("{}\nExpense removed.", "Success!".green().bold());
}


/// Amends an expense by unique identifier.
fn amend_expense(filename: &str, encrypted: bool) {
    let (mut expenses, key) = readwrite::expenses_from_bin(filename, encrypted);

    if expenses.len() == 0 {
        println!("No expenses in working directory");
        println!("Argentum exiting");
        exit(0);
    }

    let mut options: Vec<String> = Vec::new();

    // Build a list of printable expenses
    Expense::sort_by_date(&mut expenses);
    for expense in &expenses {
        options.push(expense.to_str());
    }
    let options: Vec<&str> = options.iter().map(|x| x.as_str()).collect();

    // Get the ID from the user
    let raw = get_selection("Select an expense to amend.".to_string(), options);
    let id_str = {
        let first = match raw.split("|").nth(0) {
            Some(f) => f,
            None => Error::InvalidId.throw(),
        };
        &first[3..first.len() - 1]
    };
    let id = match str::parse::<u32>(id_str) {
        Ok(y) => y,
        Err(_) => Error::InvalidId.throw(),
    };

    // Display the old (discarded) expense
    for expense in &expenses {
        if expense.id() == id {
            println!("\nEXPENSE INFORMATION\n{}", expense.to_verbose());
            break;
        }
    }

    // Interact with the user to get the amended expense
    let new_expense = Expense::get(id);

    // Remove the old expense object and add the new expense object
    expenses.retain(|e| e.id() != id);
    expenses.push(new_expense);

    match key {
        Some(k) => readwrite::expenses_to_bin_using_key(filename, expenses, k),
        None => readwrite::expenses_to_bin(filename, expenses, encrypted),
    };

    println!("{}\nExpense amended.", "Success!".green().bold());
}


/// Prepares a PDF report of expenses in a given month.
fn create_report(input: &str, sorting: Sorting, encrypted: bool) {
    // Read all expenses from the file
    let (all, _) = readwrite::expenses_from_bin(input, encrypted);

    // Get the year and month from the user
    let raw_year = get_text("Please input a year.".to_string());
    let year = match str::parse::<i32>(&raw_year) {
        Ok(y) => y,
        Err(_) => Error::InvalidDate.throw(),
    };
    let raw_month = get_text("Please input a month (1 to 12).".to_string());
    let month = match str::parse::<u32>(&raw_month) {
        Ok(m) => m,
        Err(_) => Error::InvalidDate.throw(),
    };

    let output = format!("{:04}_{:02}_Report.pdf", year, month);

    // Sort the expenses
    let mut summarized = Vec::new();
    for expense in all {
        if expense.month() == month && expense.year() == year {
            summarized.push(expense.to_owned());
        }
    }
    match sorting {
        Sorting::Amount => Expense::sort_by_amount(&mut summarized),
        Sorting::Date => Expense::sort_by_date(&mut summarized),
        Sorting::Vendor => Expense::sort_by_vendor(&mut summarized),
    };

    // Create LaTeX
    let mut latex = String::from("
        \\documentclass[12pt]{article}
        \\usepackage[margin=1.0in]{geometry}
        \\begin{document}
        \\title{\\textbf{
    ");

    // Add a date
    latex.push_str(&format!(
        "{} {:04}",
        get_month_name(month),
        year,
    ));
    latex.push_str("
        }}
        \\author{\\textbf{Expense Report}}
        \\date{Autogenerated by Argentum Finance
        }\n\\maketitle
    ");

    // Create expense totals/stats
    let mut total: f32 = 0.0;
    let mut average: f32 = 0.0;
    let mut count: u32 = 0;
    let mut by_account: HashMap<String, f32> = HashMap::new();
    for expense in &summarized {
        total += expense.amount();
        average += expense.amount();
        count += 1;
        match by_account.get(&expense.account()) {
            Some(v) => by_account.insert(expense.account(), *v + expense.amount()),
            None => by_account.insert(expense.account(), expense.amount()),
        };
    }
    let count_f32 = count as f32;
    // Set the average to zero if there are no purchases.
    if count != 0 {
        average /= count_f32;
    } else {
        average = 0.0;
    }
    latex.push_str(&format!(
        "
        \\section*{{Summary}}
        \\textbf{{Total spent}}: \\textbf{{\\${:.2}}}\\newline
        \\textbf{{Expense count}}: {}\\newline
        \\textbf{{Average expense}}: \\${:.2}\\newline
        ",
        total,
        count,
        average,
    ));

    latex.push_str(
        "
        \\subsection*{{By Account}}
        "
    );

    // Push an expense summary by account
    for (account, amount) in by_account {
        latex.push_str(&format!(
            "\\textbf{{{}}}: \\${:.2}\\newline",
            account,
            amount,
        ));
    }

    // Push each expense
    for expense in &summarized {
        latex.push_str(&expense.to_latex());
    }

    // Export
    latex.push_str("\n\\end{document}");

    readwrite::latex_to_pdf(&output, &latex);

    println!("{}\nOutput written to {}", "Success!".green().bold(), output.blue());
}


/// Encrypts an expenses file.
pub fn encrypt(filename: &str) {
    // Read the data as if it is not encrypted
    let (data, _) = readwrite::expenses_from_bin(filename, false);
    readwrite::expenses_to_bin(filename, data, true);

    println!("{}\n`{}` encrypted.", "Success!".green().bold(), filename);
}


/// Decrypts an expenses file.
pub fn decrypt(filename: &str) {
    // Read the data as if it is not encrypted
    let (data, _) = readwrite::expenses_from_bin(filename, true);
    readwrite::expenses_to_bin(filename, data, false);

    println!("{}\n`{}` decrypted.", "Success!".green().bold(), filename);
}


/// Lists configuration information.
pub fn print_config(config: Config) {
    // Header
    println!("{}", "Configuration Information".blue().bold());

    println!("{}", config.to_ascii());
}


/// Allows the user to modify configuration information.
pub fn configure() {
    let mut config = readwrite::config_from_file();

    // Get the filename.
    let mut filename = get_text("Input a transaction history filename.  (Press Enter for default `expenses.fin`)".to_string());
    // If left empty, set to default.
    if filename == "" {
        filename = DEFAULT.to_string();
    }
    config.set_filename(&filename);

    let sorting_options = Config::get_sorting_options();
    let sorting = get_selection("How should Argentum sort transactions?".to_string(), sorting_options);
    config.set_sorting_by_str(sorting);

    let encrypted = get_selection(
        "Should Argentum encrypt and decrypt expense files by default?".to_string(),
        vec!["Yes", "No"],
    );
    match encrypted {
        "Yes" => config.set_encrypted(true),
        "No" => config.set_encrypted(false),
        _ => unreachable!(),
    };

    readwrite::config_to_file(config);

    println!("{}\nConfiguration saved.", "Success!".green().bold());
}