use crate::db::Database;
use crate::models::TransactionType;
use chrono::{Local, 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),
        }
    }
}

/// Escape special characters for OFX SGML content.
fn ofx_escape(s: &str) -> String {
    s.replace('&', "&amp;")
        .replace('<', "&lt;")
        .replace('>', "&gt;")
}

/// Export transactions to OFX 1.6 SGML format.
///
/// This produces a bank statement download file compatible with
/// Quicken, GnuCash, and other personal finance applications.
pub fn export_ofx<W: Write>(
    db: &Database,
    writer: &mut W,
    from: Option<NaiveDate>,
    to: Option<NaiveDate>,
) -> Result<usize, ExportError> {
    let transactions = db.list_all_transactions(from, to)?;
    let now = Local::now();
    let dtserver = now.format("%Y%m%d%H%M%S").to_string();

    // Determine date range for the statement
    let start_date = transactions
        .first()
        .map(|t| t.date)
        .unwrap_or_else(|| now.date_naive());
    let end_date = transactions
        .last()
        .map(|t| t.date)
        .unwrap_or_else(|| now.date_naive());

    let base_currency = db
        .get_setting("base_currency")
        .ok()
        .flatten()
        .unwrap_or_else(|| "USD".to_string());

    // OFX SGML headers
    writeln!(writer, "OFXHEADER:100")?;
    writeln!(writer, "DATA:OFXSGML")?;
    writeln!(writer, "VERSION:160")?;
    writeln!(writer, "SECURITY:NONE")?;
    writeln!(writer, "ENCODING:USASCII")?;
    writeln!(writer, "CHARSET:1252")?;
    writeln!(writer, "COMPRESSION:NONE")?;
    writeln!(writer, "OLDFILEUID:NONE")?;
    writeln!(writer, "NEWFILEUID:NONE")?;
    writeln!(writer)?;

    // OFX body
    writeln!(writer, "<OFX>")?;
    writeln!(writer, "<SIGNONMSGSRSV1>")?;
    writeln!(writer, "<SONRS>")?;
    writeln!(writer, "<STATUS>")?;
    writeln!(writer, "<CODE>0")?;
    writeln!(writer, "<SEVERITY>INFO")?;
    writeln!(writer, "</STATUS>")?;
    writeln!(writer, "<DTSERVER>{}", dtserver)?;
    writeln!(writer, "<LANGUAGE>ENG")?;
    writeln!(writer, "</SONRS>")?;
    writeln!(writer, "</SIGNONMSGSRSV1>")?;

    writeln!(writer, "<BANKMSGSRSV1>")?;
    writeln!(writer, "<STMTTRNRS>")?;
    writeln!(writer, "<TRNUID>0")?;
    writeln!(writer, "<STATUS>")?;
    writeln!(writer, "<CODE>0")?;
    writeln!(writer, "<SEVERITY>INFO")?;
    writeln!(writer, "</STATUS>")?;
    writeln!(writer, "<STMTRS>")?;
    writeln!(writer, "<CURDEF>{}", base_currency)?;
    writeln!(writer, "<BANKACCTFROM>")?;
    writeln!(writer, "<BANKID>0")?;
    writeln!(writer, "<ACCTID>OUTLAY")?;
    writeln!(writer, "<ACCTTYPE>CHECKING")?;
    writeln!(writer, "</BANKACCTFROM>")?;

    writeln!(writer, "<BANKTRANLIST>")?;
    writeln!(
        writer,
        "<DTSTART>{}",
        start_date.format("%Y%m%d")
    )?;
    writeln!(
        writer,
        "<DTEND>{}",
        end_date.format("%Y%m%d")
    )?;

    for txn in &transactions {
        let trntype = match txn.transaction_type {
            TransactionType::Expense => "DEBIT",
            TransactionType::Income => "CREDIT",
        };
        let amount = match txn.transaction_type {
            TransactionType::Expense => -txn.amount,
            TransactionType::Income => txn.amount,
        };
        let cat_name = db
            .get_category(txn.category_id)
            .map(|c| c.name)
            .unwrap_or_else(|_| "Unknown".to_string());

        let name = if let Some(ref payee) = txn.payee {
            if !payee.is_empty() {
                ofx_escape(payee)
            } else {
                ofx_escape(&cat_name)
            }
        } else {
            ofx_escape(&cat_name)
        };

        writeln!(writer, "<STMTTRN>")?;
        writeln!(writer, "<TRNTYPE>{}", trntype)?;
        writeln!(
            writer,
            "<DTPOSTED>{}",
            txn.date.format("%Y%m%d")
        )?;
        writeln!(writer, "<TRNAMT>{:.2}", amount)?;
        writeln!(writer, "<FITID>{}", txn.id)?;
        writeln!(writer, "<NAME>{}", name)?;

        if let Some(ref note) = txn.note {
            if !note.is_empty() {
                writeln!(writer, "<MEMO>{}", ofx_escape(note))?;
            }
        }

        writeln!(writer, "</STMTTRN>")?;
    }

    writeln!(writer, "</BANKTRANLIST>")?;

    // Ledger balance (sum of all exported transactions)
    let balance: f64 = transactions.iter().map(|t| match t.transaction_type {
        TransactionType::Expense => -t.amount,
        TransactionType::Income => t.amount,
    }).sum();

    writeln!(writer, "<LEDGERBAL>")?;
    writeln!(writer, "<BALAMT>{:.2}", balance)?;
    writeln!(
        writer,
        "<DTASOF>{}",
        end_date.format("%Y%m%d")
    )?;
    writeln!(writer, "</LEDGERBAL>")?;

    writeln!(writer, "</STMTRS>")?;
    writeln!(writer, "</STMTTRNRS>")?;
    writeln!(writer, "</BANKMSGSRSV1>")?;
    writeln!(writer, "</OFX>")?;

    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_ofx_header() {
        let db = setup_db();
        let mut buf = Vec::new();
        export_ofx(&db, &mut buf, None, None).unwrap();
        let output = String::from_utf8(buf).unwrap();
        assert!(output.starts_with("OFXHEADER:100"));
        assert!(output.contains("VERSION:160"));
        assert!(output.contains("<OFX>"));
        assert!(output.contains("</OFX>"));
    }

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

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

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

        let output = String::from_utf8(buf).unwrap();
        assert!(output.contains("<TRNTYPE>DEBIT"));
        assert!(output.contains("<TRNAMT>-25.99"));
        assert!(output.contains("<NAME>Amazon"));
        assert!(output.contains("<MEMO>Books"));
        assert!(output.contains("<DTPOSTED>20260301"));
    }

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

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

        let mut buf = Vec::new();
        export_ofx(&db, &mut buf, None, None).unwrap();
        let output = String::from_utf8(buf).unwrap();
        assert!(output.contains("<TRNTYPE>CREDIT"));
        assert!(output.contains("<TRNAMT>5000.00"));
    }

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

        let txn = NewTransaction {
            amount: 10.0,
            transaction_type: TransactionType::Expense,
            category_id: cats[0].id,
            currency: "USD".to_string(),
            exchange_rate: 1.0,
            note: Some("Tom & Jerry's <shop>".to_string()),
            date: NaiveDate::from_ymd_opt(2026, 1, 15).unwrap(),
            recurring_id: None,
            payee: Some("A&B Store".to_string()),
        };
        db.insert_transaction(&txn).unwrap();

        let mut buf = Vec::new();
        export_ofx(&db, &mut buf, None, None).unwrap();
        let output = String::from_utf8(buf).unwrap();
        assert!(output.contains("<NAME>A&amp;B Store"));
        assert!(output.contains("Tom &amp; Jerry's &lt;shop&gt;"));
    }

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

        let output = String::from_utf8(buf).unwrap();
        assert!(output.contains("<BANKTRANLIST>"));
        assert!(output.contains("</BANKTRANLIST>"));
    }

    #[test]
    fn test_ofx_ledger_balance() {
        let db = setup_db();
        let expense_cats = db
            .list_categories(Some(TransactionType::Expense))
            .unwrap();
        let income_cats = db
            .list_categories(Some(TransactionType::Income))
            .unwrap();

        let txn1 = NewTransaction {
            amount: 100.0,
            transaction_type: TransactionType::Income,
            category_id: income_cats[0].id,
            currency: "USD".to_string(),
            exchange_rate: 1.0,
            note: None,
            date: NaiveDate::from_ymd_opt(2026, 1, 1).unwrap(),
            recurring_id: None,
            payee: None,
        };
        let txn2 = NewTransaction {
            amount: 30.0,
            transaction_type: TransactionType::Expense,
            category_id: expense_cats[0].id,
            currency: "USD".to_string(),
            exchange_rate: 1.0,
            note: None,
            date: NaiveDate::from_ymd_opt(2026, 1, 2).unwrap(),
            recurring_id: None,
            payee: None,
        };
        db.insert_transaction(&txn1).unwrap();
        db.insert_transaction(&txn2).unwrap();

        let mut buf = Vec::new();
        export_ofx(&db, &mut buf, None, None).unwrap();
        let output = String::from_utf8(buf).unwrap();
        // Balance should be 100 - 30 = 70
        assert!(output.contains("<BALAMT>70.00"));
    }
}
