use crate::models::PdfParsedRow;
use chrono::NaiveDate;

/// Extract transactions from a PDF bank statement.
/// Tries text extraction first, falls back to OCR if no text found.
pub fn extract_transactions_from_pdf(bytes: &[u8]) -> Result<Vec<PdfParsedRow>, String> {
    // Try text-based extraction first
    match extract_text_based(bytes) {
        Ok(rows) if !rows.is_empty() => return Ok(rows),
        _ => {}
    }

    // Fall back to OCR
    if crate::ocr::is_available() {
        return extract_ocr_based(bytes);
    }

    Err("No text found in PDF and OCR is not available".to_string())
}

fn extract_text_based(bytes: &[u8]) -> Result<Vec<PdfParsedRow>, String> {
    let text = pdf_extract::extract_text_from_mem(bytes)
        .map_err(|e| format!("PDF text extraction failed: {}", e))?;

    if text.trim().is_empty() {
        return Ok(Vec::new());
    }

    let mut rows = Vec::new();
    for line in text.lines() {
        let line = line.trim();
        if line.is_empty() {
            continue;
        }
        if let Some(row) = parse_statement_line(line) {
            rows.push(row);
        }
    }
    Ok(rows)
}

fn extract_ocr_based(bytes: &[u8]) -> Result<Vec<PdfParsedRow>, String> {
    let amounts: Vec<(f64, String)> = crate::ocr::extract_amounts_from_image(bytes)
        .ok_or_else(|| "OCR extraction returned no results".to_string())?;

    let rows: Vec<PdfParsedRow> = amounts
        .into_iter()
        .map(|(amount, source_line)| PdfParsedRow {
            date: None,
            description: source_line,
            amount: amount.abs(),
            is_credit: amount > 0.0,
        })
        .collect();

    Ok(rows)
}

/// Try to parse a single line from a bank statement.
/// Common formats:
///   "01/15/2026  GROCERY STORE    -45.67"
///   "2026-01-15  SALARY           +2500.00"
///   "15 Jan  Coffee Shop  12.50"
fn parse_statement_line(line: &str) -> Option<PdfParsedRow> {
    let tokens: Vec<&str> = line.split_whitespace().collect();
    if tokens.len() < 2 {
        return None;
    }

    // Try to find a date at the start
    let (date, desc_start) = try_parse_date_prefix(&tokens);

    // Try to find an amount at the end
    let (amount, is_credit, desc_end) = try_parse_amount_suffix(&tokens)?;

    // Everything between date and amount is description
    if desc_start >= desc_end {
        return None;
    }
    let description = tokens[desc_start..desc_end].join(" ");
    if description.is_empty() {
        return None;
    }

    Some(PdfParsedRow {
        date,
        description,
        amount,
        is_credit,
    })
}

fn try_parse_date_prefix(tokens: &[&str]) -> (Option<NaiveDate>, usize) {
    if tokens.is_empty() {
        return (None, 0);
    }

    // Try single token: "2026-01-15", "01/15/2026", "15/01/2026"
    if let Some(d) = parse_date_flexible(tokens[0]) {
        return (Some(d), 1);
    }

    // Try two tokens: "15 Jan", "Jan 15"
    if tokens.len() >= 2 {
        let combined = format!("{} {}", tokens[0], tokens[1]);
        if let Some(d) = parse_date_flexible(&combined) {
            return (Some(d), 2);
        }
        // Try three tokens: "15 Jan 2026"
        if tokens.len() >= 3 {
            let combined3 = format!("{} {} {}", tokens[0], tokens[1], tokens[2]);
            if let Some(d) = parse_date_flexible(&combined3) {
                return (Some(d), 3);
            }
        }
    }

    (None, 0)
}

fn try_parse_amount_suffix(tokens: &[&str]) -> Option<(f64, bool, usize)> {
    for i in (0..tokens.len()).rev() {
        let tok = tokens[i];
        let cleaned = tok.replace(',', "").replace('$', "");
        if let Ok(val) = cleaned.parse::<f64>() {
            let is_credit = val > 0.0 || tok.starts_with('+');
            return Some((val.abs(), is_credit, i));
        }
    }
    None
}

fn parse_date_flexible(s: &str) -> Option<NaiveDate> {
    let formats = [
        "%Y-%m-%d",
        "%m/%d/%Y",
        "%d/%m/%Y",
        "%m-%d-%Y",
        "%d %b %Y",
        "%b %d %Y",
        "%d %b",
        "%b %d",
    ];
    for fmt in &formats {
        if let Ok(d) = NaiveDate::parse_from_str(s, fmt) {
            return Some(d);
        }
    }
    None
}

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

    #[test]
    fn test_parse_line_with_date_and_amount() {
        let row = parse_statement_line("2026-01-15  GROCERY STORE  -45.67").unwrap();
        assert_eq!(
            row.date,
            Some(NaiveDate::from_ymd_opt(2026, 1, 15).unwrap())
        );
        assert_eq!(row.description, "GROCERY STORE");
        assert!((row.amount - 45.67).abs() < 0.01);
        assert!(!row.is_credit);
    }

    #[test]
    fn test_parse_line_credit() {
        let row = parse_statement_line("2026-01-15  SALARY  +2500.00").unwrap();
        assert!((row.amount - 2500.0).abs() < 0.01);
        assert!(row.is_credit);
    }

    #[test]
    fn test_parse_line_no_date() {
        let row = parse_statement_line("COFFEE SHOP  12.50").unwrap();
        assert!(row.date.is_none());
        assert_eq!(row.description, "COFFEE SHOP");
        assert!((row.amount - 12.50).abs() < 0.01);
    }

    #[test]
    fn test_parse_line_too_short() {
        assert!(parse_statement_line("hello").is_none());
    }
}
