use crate::db::Database;
use crate::models::{NewTransaction, TransactionType};
use chrono::NaiveDate;
use std::path::Path;

/// Import transactions from a QIF (Quicken Interchange Format) file.
///
/// QIF records use single-character line prefixes:
/// - D = date (MM/DD/YYYY or MM/DD'YY)
/// - T = amount (negative = expense, positive = income)
/// - P = payee
/// - L = category
/// - M = memo/note
/// - S/$/E = split lines (category/amount/memo)
/// - ^ = end of record
///
/// Categories are matched by name. If a category is not found,
/// the transaction is assigned to the first matching-type category.
pub fn import_qif(db: &Database, path: &Path, merge: bool) -> Result<usize, Box<dyn std::error::Error>> {
    let content = std::fs::read_to_string(path)?;

    if !merge {
        db.reset_all_data()?;
    }

    let expense_cats = db.list_categories(Some(TransactionType::Expense))?;
    let income_cats = db.list_categories(Some(TransactionType::Income))?;

    let default_expense_id = expense_cats.first().map(|c| c.id).unwrap_or(1);
    let default_income_id = income_cats.first().map(|c| c.id).unwrap_or(1);

    let mut count = 0;
    let mut date: Option<NaiveDate> = None;
    let mut amount: Option<f64> = None;
    let mut payee: Option<String> = None;
    let mut category: Option<String> = None;
    let mut memo: Option<String> = None;

    for line in content.lines() {
        let line = line.trim();
        if line.is_empty() || line.starts_with('!') {
            continue;
        }

        let prefix = &line[..1];
        let value = &line[1..];

        match prefix {
            "D" => {
                date = parse_qif_date(value);
            }
            "T" => {
                amount = value.replace(',', "").parse::<f64>().ok();
            }
            "P" => {
                if !value.is_empty() {
                    payee = Some(value.to_string());
                }
            }
            "L" => {
                if !value.is_empty() {
                    category = Some(value.to_string());
                }
            }
            "M" => {
                if !value.is_empty() {
                    memo = Some(value.to_string());
                }
            }
            "^" => {
                // End of record - save transaction
                if let (Some(d), Some(amt)) = (date, amount) {
                    let txn_type = if amt < 0.0 {
                        TransactionType::Expense
                    } else {
                        TransactionType::Income
                    };
                    let abs_amount = amt.abs();

                    let category_id = resolve_category(
                        &category,
                        txn_type,
                        &expense_cats,
                        &income_cats,
                        default_expense_id,
                        default_income_id,
                    );

                    if merge && db.find_duplicate_transaction(abs_amount, txn_type, category_id, d)? {
                        // Skip duplicate
                    } else {
                        let new_txn = NewTransaction {
                            amount: abs_amount,
                            transaction_type: txn_type,
                            category_id,
                            currency: base_currency(db),
                            exchange_rate: 1.0,
                            note: memo.clone(),
                            date: d,
                            recurring_id: None,
                            payee: payee.clone(),
                        };
                        db.insert_transaction(&new_txn)?;
                        count += 1;
                    }
                }

                // Reset for next record
                date = None;
                amount = None;
                payee = None;
                category = None;
                memo = None;
            }
            // Skip split lines (S, $, E) and other unknown prefixes
            _ => {}
        }
    }

    Ok(count)
}

fn base_currency(db: &Database) -> String {
    db.get_setting("base_currency")
        .ok()
        .flatten()
        .unwrap_or_else(|| "USD".to_string())
}

/// Parse a QIF date string. Supports:
/// - MM/DD/YYYY (e.g., 03/01/2026)
/// - MM/DD'YY (e.g., 3/ 1'26)
/// - M/D/YYYY
/// - MM-DD-YYYY
fn parse_qif_date(s: &str) -> Option<NaiveDate> {
    let s = s.trim().replace(' ', "");

    // Try MM/DD/YYYY or M/D/YYYY
    if let Ok(d) = NaiveDate::parse_from_str(&s, "%m/%d/%Y") {
        return Some(d);
    }
    // Try MM-DD-YYYY
    if let Ok(d) = NaiveDate::parse_from_str(&s, "%m-%d-%Y") {
        return Some(d);
    }
    // Try the apostrophe format: M/D'YY
    if let Some(apos_idx) = s.find('\'') {
        let date_part = &s[..apos_idx];
        let year_part = &s[apos_idx + 1..];
        if let Some((month_str, day_str)) = date_part.split_once('/') {
            let month: u32 = month_str.parse().ok()?;
            let day: u32 = day_str.parse().ok()?;
            let year_short: i32 = year_part.parse().ok()?;
            let year = if year_short < 100 { 2000 + year_short } else { year_short };
            return NaiveDate::from_ymd_opt(year, month, day);
        }
    }
    None
}

/// Resolve a QIF category name to a database category ID.
/// QIF uses "Parent:Sub" for subcategories.
fn resolve_category(
    cat_name: &Option<String>,
    txn_type: TransactionType,
    expense_cats: &[crate::models::Category],
    income_cats: &[crate::models::Category],
    default_expense_id: i64,
    default_income_id: i64,
) -> i64 {
    let cats = match txn_type {
        TransactionType::Expense => expense_cats,
        TransactionType::Income => income_cats,
    };
    let default_id = match txn_type {
        TransactionType::Expense => default_expense_id,
        TransactionType::Income => default_income_id,
    };

    let Some(name) = cat_name else {
        return default_id;
    };

    // Try exact match first
    if let Some(c) = cats.iter().find(|c| c.name == *name) {
        return c.id;
    }

    // For "Parent:Sub" format, try matching just the sub-category name
    if let Some((_parent, sub)) = name.split_once(':') {
        if let Some(c) = cats.iter().find(|c| c.name == sub) {
            return c.id;
        }
    }

    // Case-insensitive match
    let lower = name.to_lowercase();
    if let Some(c) = cats.iter().find(|c| c.name.to_lowercase() == lower) {
        return c.id;
    }

    default_id
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Write;
    use std::sync::atomic::{AtomicUsize, Ordering};

    static COUNTER: AtomicUsize = AtomicUsize::new(0);

    fn setup_db() -> Database {
        Database::open_in_memory().unwrap()
    }

    fn write_temp_qif(content: &str) -> std::path::PathBuf {
        let n = COUNTER.fetch_add(1, Ordering::SeqCst);
        let path = std::env::temp_dir().join(format!("outlay_test_qif_{}.qif", n));
        let mut f = std::fs::File::create(&path).unwrap();
        f.write_all(content.as_bytes()).unwrap();
        f.flush().unwrap();
        path
    }

    #[test]
    fn test_import_qif_expense() {
        let db = setup_db();
        let path = write_temp_qif(
            "!Type:Bank\nD03/01/2026\nT-42.50\nPCafe\nMLunch\n^\n",
        );
        let count = import_qif(&db, &path, true).unwrap();
        assert_eq!(count, 1);

        let txns = db.list_all_transactions(None, None).unwrap();
        assert_eq!(txns.len(), 1);
        assert_eq!(txns[0].amount, 42.50);
        assert_eq!(txns[0].transaction_type, TransactionType::Expense);
        assert_eq!(txns[0].payee.as_deref(), Some("Cafe"));
        assert_eq!(txns[0].note.as_deref(), Some("Lunch"));
        let _ = std::fs::remove_file(&path);
    }

    #[test]
    fn test_import_qif_income() {
        let db = setup_db();
        let path = write_temp_qif(
            "!Type:Bank\nD02/15/2026\nT1000.00\nMSalary\n^\n",
        );
        let count = import_qif(&db, &path, true).unwrap();
        assert_eq!(count, 1);

        let txns = db.list_all_transactions(None, None).unwrap();
        assert_eq!(txns[0].transaction_type, TransactionType::Income);
        assert_eq!(txns[0].amount, 1000.0);
        let _ = std::fs::remove_file(&path);
    }

    #[test]
    fn test_import_qif_multiple_records() {
        let db = setup_db();
        let path = write_temp_qif(
            "!Type:Bank\nD01/01/2026\nT-10.00\n^\nD01/02/2026\nT-20.00\n^\nD01/03/2026\nT50.00\n^\n",
        );
        let count = import_qif(&db, &path, true).unwrap();
        assert_eq!(count, 3);
        let _ = std::fs::remove_file(&path);
    }

    #[test]
    fn test_import_qif_merge_deduplication() {
        let db = setup_db();
        let path = write_temp_qif(
            "!Type:Bank\nD03/01/2026\nT-42.50\n^\n",
        );
        let count1 = import_qif(&db, &path, true).unwrap();
        assert_eq!(count1, 1);

        let count2 = import_qif(&db, &path, true).unwrap();
        assert_eq!(count2, 0);

        let txns = db.list_all_transactions(None, None).unwrap();
        assert_eq!(txns.len(), 1);
        let _ = std::fs::remove_file(&path);
    }

    #[test]
    fn test_import_qif_category_matching() {
        let db = setup_db();
        let cats = db.list_categories(Some(TransactionType::Expense)).unwrap();
        let cat_name = &cats[0].name;

        let path = write_temp_qif(&format!(
            "!Type:Bank\nD01/01/2026\nT-25.00\nL{}\n^\n",
            cat_name
        ));
        let count = import_qif(&db, &path, true).unwrap();
        assert_eq!(count, 1);

        let txns = db.list_all_transactions(None, None).unwrap();
        assert_eq!(txns[0].category_id, cats[0].id);
        let _ = std::fs::remove_file(&path);
    }

    #[test]
    fn test_parse_qif_date_formats() {
        assert_eq!(
            parse_qif_date("03/01/2026"),
            Some(NaiveDate::from_ymd_opt(2026, 3, 1).unwrap())
        );
        assert_eq!(
            parse_qif_date("3/1/2026"),
            Some(NaiveDate::from_ymd_opt(2026, 3, 1).unwrap())
        );
        assert_eq!(
            parse_qif_date("03-01-2026"),
            Some(NaiveDate::from_ymd_opt(2026, 3, 1).unwrap())
        );
        assert_eq!(
            parse_qif_date("3/ 1'26"),
            Some(NaiveDate::from_ymd_opt(2026, 3, 1).unwrap())
        );
    }

    #[test]
    fn test_import_qif_empty_file() {
        let db = setup_db();
        let path = write_temp_qif("!Type:Bank\n");
        let count = import_qif(&db, &path, true).unwrap();
        assert_eq!(count, 0);
        let _ = std::fs::remove_file(&path);
    }
}
