use crate::db::Database;
use chrono::NaiveDate;
use csv::Writer;
use std::io::Write;

#[derive(Debug)]
pub enum ExportError {
    Db(rusqlite::Error),
    Csv(csv::Error),
}

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

impl From<csv::Error> for ExportError {
    fn from(e: csv::Error) -> Self {
        ExportError::Csv(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::Csv(e) => write!(f, "CSV error: {}", e),
        }
    }
}

pub fn export_transactions_csv<W: Write>(
    db: &Database,
    writer: W,
    from: Option<NaiveDate>,
    to: Option<NaiveDate>,
) -> Result<usize, ExportError> {
    let transactions = db.list_all_transactions(from, to)?;
    let mut wtr = Writer::from_writer(writer);

    wtr.write_record(["Date", "Type", "Category", "Amount", "Currency", "Exchange Rate", "Note", "Payee"])?;

    for txn in &transactions {
        let cat_name = db
            .get_category(txn.category_id)
            .map(|c| c.name)
            .unwrap_or_else(|_| "Unknown".to_string());

        wtr.write_record(&[
            txn.date.format("%Y-%m-%d").to_string(),
            txn.transaction_type.as_str().to_string(),
            cat_name,
            format!("{:.2}", txn.amount),
            txn.currency.clone(),
            format!("{:.4}", txn.exchange_rate),
            txn.note.clone().unwrap_or_default(),
            txn.payee.clone().unwrap_or_default(),
        ])?;
    }

    wtr.flush().map_err(|e| ExportError::Csv(e.into()))?;
    Ok(transactions.len())
}

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

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

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

        let txn = NewTransaction {
            amount: 42.50,
            transaction_type: TransactionType::Expense,
            category_id: cat.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: None,
        };
        db.insert_transaction(&txn).unwrap();

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

        let output = String::from_utf8(buf).unwrap();
        let lines: Vec<&str> = output.trim().lines().collect();
        assert_eq!(lines.len(), 2);
        assert_eq!(lines[0], "Date,Type,Category,Amount,Currency,Exchange Rate,Note,Payee");
        assert!(lines[1].contains("2026-03-01"));
        assert!(lines[1].contains("expense"));
        assert!(lines[1].contains("42.50"));
        assert!(lines[1].contains("Lunch"));
    }

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

        let output = String::from_utf8(buf).unwrap();
        let lines: Vec<&str> = output.trim().lines().collect();
        assert_eq!(lines.len(), 1); // header only
    }

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

        for day in 1..=5 {
            let txn = NewTransaction {
                amount: 10.0 * day as f64,
                transaction_type: TransactionType::Expense,
                category_id: cat_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();
        }

        // Filter from Jan 2 to Jan 4
        let mut buf = Vec::new();
        let count = export_transactions_csv(
            &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_csv_export_multiple_types() {
        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: 50.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, 2, 1).unwrap(),
            recurring_id: None,
            payee: None,
        };
        let txn2 = NewTransaction {
            amount: 1000.0,
            transaction_type: TransactionType::Income,
            category_id: income_cats[0].id,
            currency: "EUR".to_string(),
            exchange_rate: 0.92,
            note: Some("Salary".to_string()),
            date: NaiveDate::from_ymd_opt(2026, 2, 1).unwrap(),
            recurring_id: None,
            payee: None,
        };
        db.insert_transaction(&txn1).unwrap();
        db.insert_transaction(&txn2).unwrap();

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

        let output = String::from_utf8(buf).unwrap();
        assert!(output.contains("expense"));
        assert!(output.contains("income"));
        assert!(output.contains("EUR"));
        assert!(output.contains("0.9200"));
    }
}
