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

/// Import transactions from an OFX 1.6 SGML file.
///
/// Parses STMTTRN records looking for:
/// - TRNTYPE (DEBIT/CREDIT)
/// - DTPOSTED (YYYYMMDD date)
/// - TRNAMT (signed amount)
/// - NAME (payee/description)
/// - MEMO (note)
///
/// Since OFX does not carry category information, all imported
/// transactions are assigned to the first available category
/// of the matching type (expense for DEBIT, income for CREDIT).
pub fn import_ofx(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 base_currency = db
        .get_setting("base_currency")
        .ok()
        .flatten()
        .unwrap_or_else(|| "USD".to_string());

    let mut count = 0;

    // Parse STMTTRN blocks
    let mut pos = 0;
    let upper = content.to_uppercase();
    while let Some(start) = upper[pos..].find("<STMTTRN>") {
        let block_start = pos + start;
        let block_end = if let Some(end) = upper[block_start..].find("</STMTTRN>") {
            block_start + end + "</STMTTRN>".len()
        } else {
            // No closing tag - take until next STMTTRN or end
            if let Some(next) = upper[block_start + 9..].find("<STMTTRN>") {
                block_start + 9 + next
            } else {
                content.len()
            }
        };

        let block = &content[block_start..block_end];

        let trntype = extract_tag_value(block, "TRNTYPE");
        let dtposted = extract_tag_value(block, "DTPOSTED");
        let trnamt = extract_tag_value(block, "TRNAMT");
        let name = extract_tag_value(block, "NAME");
        let memo = extract_tag_value(block, "MEMO");

        if let Some(amt_str) = &trnamt {
            if let Ok(amt) = amt_str.replace(',', "").parse::<f64>() {
                let txn_type = if let Some(ref tt) = trntype {
                    match tt.to_uppercase().as_str() {
                        "CREDIT" => TransactionType::Income,
                        _ => TransactionType::Expense,
                    }
                } else if amt < 0.0 {
                    TransactionType::Expense
                } else {
                    TransactionType::Income
                };

                let abs_amount = amt.abs();
                let date = dtposted
                    .as_ref()
                    .and_then(|d| parse_ofx_date(d))
                    .unwrap_or_else(|| chrono::Local::now().date_naive());

                let category_id = match txn_type {
                    TransactionType::Expense => default_expense_id,
                    TransactionType::Income => default_income_id,
                };

                let payee = name.as_ref().map(|n| ofx_unescape(n));
                let note = memo.as_ref().map(|m| ofx_unescape(m));

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

        pos = block_end;
    }

    Ok(count)
}

/// Extract the value of an OFX SGML tag from a block of text.
/// OFX 1.6 SGML tags look like: <TAGNAME>value
/// The value runs until the next < or newline.
fn extract_tag_value(block: &str, tag: &str) -> Option<String> {
    let upper_block = block.to_uppercase();
    let search = format!("<{}>", tag.to_uppercase());
    let start = upper_block.find(&search)?;
    let value_start = start + search.len();
    let remaining = &block[value_start..];

    // Value ends at next '<' or newline
    let end = remaining
        .find(|c: char| c == '<' || c == '\n' || c == '\r')
        .unwrap_or(remaining.len());

    let value = remaining[..end].trim().to_string();
    if value.is_empty() {
        None
    } else {
        Some(value)
    }
}

/// Parse an OFX date string (YYYYMMDD or YYYYMMDDHHMMSS).
fn parse_ofx_date(s: &str) -> Option<NaiveDate> {
    let s = s.trim();
    // Take just the first 8 chars (YYYYMMDD)
    if s.len() < 8 {
        return None;
    }
    let date_part = &s[..8];
    NaiveDate::parse_from_str(date_part, "%Y%m%d").ok()
}

/// Unescape OFX SGML entities.
fn ofx_unescape(s: &str) -> String {
    s.replace("&amp;", "&")
        .replace("&lt;", "<")
        .replace("&gt;", ">")
        .replace("&quot;", "\"")
        .replace("&apos;", "'")
}

#[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_ofx(content: &str) -> std::path::PathBuf {
        let n = COUNTER.fetch_add(1, Ordering::SeqCst);
        let path = std::env::temp_dir().join(format!("outlay_test_ofx_{}.ofx", n));
        let mut f = std::fs::File::create(&path).unwrap();
        f.write_all(content.as_bytes()).unwrap();
        f.flush().unwrap();
        path
    }

    fn minimal_ofx(transactions: &str) -> String {
        format!(
            "OFXHEADER:100\nDATA:OFXSGML\nVERSION:160\n\n\
             <OFX><BANKMSGSRSV1><STMTTRNRS><STMTRS>\n\
             <BANKTRANLIST>\n{}\n</BANKTRANLIST>\n\
             </STMTRS></STMTTRNRS></BANKMSGSRSV1></OFX>",
            transactions
        )
    }

    #[test]
    fn test_import_ofx_expense() {
        let db = setup_db();
        let path = write_temp_ofx(&minimal_ofx(
            "<STMTTRN>\n<TRNTYPE>DEBIT\n<DTPOSTED>20260301\n<TRNAMT>-42.50\n<NAME>Cafe\n<MEMO>Lunch\n</STMTTRN>",
        ));
        let count = import_ofx(&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_ofx_income() {
        let db = setup_db();
        let path = write_temp_ofx(&minimal_ofx(
            "<STMTTRN>\n<TRNTYPE>CREDIT\n<DTPOSTED>20260215\n<TRNAMT>5000.00\n<NAME>Employer\n</STMTTRN>",
        ));
        let count = import_ofx(&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, 5000.0);
        let _ = std::fs::remove_file(&path);
    }

    #[test]
    fn test_import_ofx_multiple() {
        let db = setup_db();
        let path = write_temp_ofx(&minimal_ofx(
            "<STMTTRN>\n<TRNTYPE>DEBIT\n<DTPOSTED>20260101\n<TRNAMT>-10.00\n</STMTTRN>\n\
             <STMTTRN>\n<TRNTYPE>DEBIT\n<DTPOSTED>20260102\n<TRNAMT>-20.00\n</STMTTRN>\n\
             <STMTTRN>\n<TRNTYPE>CREDIT\n<DTPOSTED>20260103\n<TRNAMT>50.00\n</STMTTRN>",
        ));
        let count = import_ofx(&db, &path, true).unwrap();
        assert_eq!(count, 3);
        let _ = std::fs::remove_file(&path);
    }

    #[test]
    fn test_import_ofx_merge_deduplication() {
        let db = setup_db();
        let path = write_temp_ofx(&minimal_ofx(
            "<STMTTRN>\n<TRNTYPE>DEBIT\n<DTPOSTED>20260301\n<TRNAMT>-42.50\n</STMTTRN>",
        ));
        let count1 = import_ofx(&db, &path, true).unwrap();
        assert_eq!(count1, 1);

        let count2 = import_ofx(&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_ofx_unescapes_entities() {
        let db = setup_db();
        let path = write_temp_ofx(&minimal_ofx(
            "<STMTTRN>\n<TRNTYPE>DEBIT\n<DTPOSTED>20260101\n<TRNAMT>-10.00\n<NAME>A&amp;B Store\n<MEMO>Tom &amp; Jerry&apos;s\n</STMTTRN>",
        ));
        let count = import_ofx(&db, &path, true).unwrap();
        assert_eq!(count, 1);

        let txns = db.list_all_transactions(None, None).unwrap();
        assert_eq!(txns[0].payee.as_deref(), Some("A&B Store"));
        assert_eq!(txns[0].note.as_deref(), Some("Tom & Jerry's"));
        let _ = std::fs::remove_file(&path);
    }

    #[test]
    fn test_import_ofx_empty() {
        let db = setup_db();
        let path = write_temp_ofx(&minimal_ofx(""));
        let count = import_ofx(&db, &path, true).unwrap();
        assert_eq!(count, 0);
        let _ = std::fs::remove_file(&path);
    }

    #[test]
    fn test_extract_tag_value() {
        let block = "<STMTTRN>\n<TRNTYPE>DEBIT\n<DTPOSTED>20260301\n<TRNAMT>-42.50\n</STMTTRN>";
        assert_eq!(extract_tag_value(block, "TRNTYPE"), Some("DEBIT".to_string()));
        assert_eq!(extract_tag_value(block, "DTPOSTED"), Some("20260301".to_string()));
        assert_eq!(extract_tag_value(block, "TRNAMT"), Some("-42.50".to_string()));
        assert_eq!(extract_tag_value(block, "FITID"), None);
    }

    #[test]
    fn test_parse_ofx_date() {
        assert_eq!(
            parse_ofx_date("20260301"),
            Some(NaiveDate::from_ymd_opt(2026, 3, 1).unwrap())
        );
        assert_eq!(
            parse_ofx_date("20260301120000"),
            Some(NaiveDate::from_ymd_opt(2026, 3, 1).unwrap())
        );
        assert_eq!(parse_ofx_date("2026"), None);
    }
}
