use crate::db::Database;
use crate::models::TransactionType;
use chrono::NaiveDate;
use std::io::Write;

#[derive(Debug)]
pub enum ExportError {
    Db(rusqlite::Error),
    Io(std::io::Error),
}

impl From<rusqlite::Error> for ExportError {
    fn from(e: rusqlite::Error) -> Self {
        ExportError::Db(e)
    }
}

impl From<std::io::Error> for ExportError {
    fn from(e: std::io::Error) -> Self {
        ExportError::Io(e)
    }
}

impl std::fmt::Display for ExportError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ExportError::Db(e) => write!(f, "Database error: {}", e),
            ExportError::Io(e) => write!(f, "IO error: {}", e),
        }
    }
}

/// Build a QIF-style category path like "Food:Groceries" for subcategories.
fn category_path(db: &Database, category_id: i64) -> String {
    let cat = match db.get_category(category_id) {
        Ok(c) => c,
        Err(_) => return "Unknown".to_string(),
    };
    if let Some(parent_id) = cat.parent_id {
        if let Ok(parent) = db.get_category(parent_id) {
            return format!("{}:{}", parent.name, cat.name);
        }
    }
    cat.name
}

/// Export transactions to QIF (Quicken Interchange Format).
///
/// Produces `!Type:Bank` records with support for splits.
/// Dates use MM/DD/YYYY as per the QIF specification.
pub fn export_qif<W: Write>(
    db: &Database,
    writer: &mut W,
    from: Option<NaiveDate>,
    to: Option<NaiveDate>,
) -> Result<usize, ExportError> {
    let transactions = db.list_all_transactions(from, to)?;

    writeln!(writer, "!Type:Bank")?;

    for txn in &transactions {
        // D - date in MM/DD/YYYY
        let date_str = txn.date.format("%m/%d/%Y").to_string();
        writeln!(writer, "D{}", date_str)?;

        // T - amount (negative for expenses)
        let amount = match txn.transaction_type {
            TransactionType::Expense => -txn.amount,
            TransactionType::Income => txn.amount,
        };
        writeln!(writer, "T{:.2}", amount)?;

        // P - payee
        if let Some(ref payee) = txn.payee {
            if !payee.is_empty() {
                writeln!(writer, "P{}", payee)?;
            }
        }

        // Check for splits
        let splits = db.get_splits(txn.id).unwrap_or_default();

        if splits.is_empty() {
            // L - category
            let cat_path = category_path(db, txn.category_id);
            writeln!(writer, "L{}", cat_path)?;
        } else {
            // Split lines: S for category, $ for amount, E for memo
            for split in &splits {
                let split_cat = category_path(db, split.category_id);
                writeln!(writer, "S{}", split_cat)?;

                let split_amount = match txn.transaction_type {
                    TransactionType::Expense => -split.amount,
                    TransactionType::Income => split.amount,
                };
                writeln!(writer, "${:.2}", split_amount)?;

                if let Some(ref note) = split.note {
                    if !note.is_empty() {
                        writeln!(writer, "E{}", note)?;
                    }
                }
            }
        }

        // M - memo/note
        if let Some(ref note) = txn.note {
            if !note.is_empty() {
                writeln!(writer, "M{}", note)?;
            }
        }

        // ^ - end of record
        writeln!(writer, "^")?;
    }

    Ok(transactions.len())
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::models::NewTransaction;

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

    #[test]
    fn test_qif_header() {
        let db = setup_db();
        let mut buf = Vec::new();
        export_qif(&db, &mut buf, None, None).unwrap();
        let output = String::from_utf8(buf).unwrap();
        assert!(output.starts_with("!Type:Bank"));
    }

    #[test]
    fn test_qif_expense_negative_amount() {
        let db = setup_db();
        let cats = db.list_categories(Some(TransactionType::Expense)).unwrap();

        let txn = NewTransaction {
            amount: 42.50,
            transaction_type: TransactionType::Expense,
            category_id: cats[0].id,
            currency: "USD".to_string(),
            exchange_rate: 1.0,
            note: Some("Lunch".to_string()),
            date: NaiveDate::from_ymd_opt(2026, 3, 1).unwrap(),
            recurring_id: None,
            payee: Some("Cafe".to_string()),
        };
        db.insert_transaction(&txn).unwrap();

        let mut buf = Vec::new();
        let count = export_qif(&db, &mut buf, None, None).unwrap();
        assert_eq!(count, 1);

        let output = String::from_utf8(buf).unwrap();
        assert!(output.contains("D03/01/2026"));
        assert!(output.contains("T-42.50"));
        assert!(output.contains("PCafe"));
        assert!(output.contains("MLunch"));
        assert!(output.contains("^"));
    }

    #[test]
    fn test_qif_income_positive_amount() {
        let db = setup_db();
        let cats = db.list_categories(Some(TransactionType::Income)).unwrap();

        let txn = NewTransaction {
            amount: 1000.0,
            transaction_type: TransactionType::Income,
            category_id: cats[0].id,
            currency: "USD".to_string(),
            exchange_rate: 1.0,
            note: Some("Salary".to_string()),
            date: NaiveDate::from_ymd_opt(2026, 2, 15).unwrap(),
            recurring_id: None,
            payee: None,
        };
        db.insert_transaction(&txn).unwrap();

        let mut buf = Vec::new();
        export_qif(&db, &mut buf, None, None).unwrap();
        let output = String::from_utf8(buf).unwrap();
        assert!(output.contains("T1000.00"));
        assert!(output.contains("MSalary"));
    }

    #[test]
    fn test_qif_record_separator() {
        let db = setup_db();
        let cats = db.list_categories(Some(TransactionType::Expense)).unwrap();

        for day in 1..=3 {
            let txn = NewTransaction {
                amount: 10.0,
                transaction_type: TransactionType::Expense,
                category_id: cats[0].id,
                currency: "USD".to_string(),
                exchange_rate: 1.0,
                note: None,
                date: NaiveDate::from_ymd_opt(2026, 1, day).unwrap(),
                recurring_id: None,
                payee: None,
            };
            db.insert_transaction(&txn).unwrap();
        }

        let mut buf = Vec::new();
        let count = export_qif(&db, &mut buf, None, None).unwrap();
        assert_eq!(count, 3);

        let output = String::from_utf8(buf).unwrap();
        let separators = output.lines().filter(|l| *l == "^").count();
        assert_eq!(separators, 3);
    }

    #[test]
    fn test_qif_date_range_filter() {
        let db = setup_db();
        let cats = db.list_categories(Some(TransactionType::Expense)).unwrap();

        for day in 1..=5 {
            let txn = NewTransaction {
                amount: 10.0,
                transaction_type: TransactionType::Expense,
                category_id: cats[0].id,
                currency: "USD".to_string(),
                exchange_rate: 1.0,
                note: None,
                date: NaiveDate::from_ymd_opt(2026, 1, day).unwrap(),
                recurring_id: None,
                payee: None,
            };
            db.insert_transaction(&txn).unwrap();
        }

        let mut buf = Vec::new();
        let count = export_qif(
            &db,
            &mut buf,
            Some(NaiveDate::from_ymd_opt(2026, 1, 2).unwrap()),
            Some(NaiveDate::from_ymd_opt(2026, 1, 4).unwrap()),
        )
        .unwrap();
        assert_eq!(count, 3);
    }

    #[test]
    fn test_qif_empty_export() {
        let db = setup_db();
        let mut buf = Vec::new();
        let count = export_qif(&db, &mut buf, None, None).unwrap();
        assert_eq!(count, 0);

        let output = String::from_utf8(buf).unwrap();
        assert_eq!(output.trim(), "!Type:Bank");
    }
}
