use crate::models::{Category, ParsedTransaction, TransactionType};

/// Parse a free-form text string into a transaction.
///
/// Supported patterns:
///   "Coffee 4.50"              -> amount=4.50, category=fuzzy("Coffee")
///   "4.50 groceries milk"      -> amount=4.50, category=fuzzy("groceries"), note="milk"
///   "Lunch 12.50 at Subway"    -> amount=12.50, category=fuzzy("Lunch"), payee="Subway"
///   "$25 gas"                  -> amount=25, category=fuzzy("gas")
pub fn parse_transaction(input: &str, categories: &[Category]) -> Option<ParsedTransaction> {
    let input = input.trim();
    if input.is_empty() {
        return None;
    }

    let tokens: Vec<&str> = input.split_whitespace().collect();
    if tokens.is_empty() {
        return None;
    }

    // Find the amount token (first token that parses as a number, with optional $ prefix)
    let mut amount: Option<f64> = None;
    let mut amount_idx: Option<usize> = None;
    for (i, tok) in tokens.iter().enumerate() {
        let cleaned = tok.trim_start_matches('$').replace(',', "");
        if let Ok(val) = cleaned.parse::<f64>() {
            if val > 0.0 {
                amount = Some(val);
                amount_idx = Some(i);
                break;
            }
        }
    }

    let amount = amount?;
    let amount_idx = amount_idx.unwrap();

    // Collect non-amount tokens
    let word_tokens: Vec<&str> = tokens
        .iter()
        .enumerate()
        .filter(|(i, _)| *i != amount_idx)
        .map(|(_, t)| *t)
        .collect();

    // Find payee marker ("at", "from", "to")
    let mut payee: Option<String> = None;
    let mut pre_marker_words: Vec<&str> = Vec::new();
    let mut found_marker = false;
    let mut post_marker_words: Vec<&str> = Vec::new();

    for word in &word_tokens {
        let lower = word.to_lowercase();
        if !found_marker && (lower == "at" || lower == "from" || lower == "to") {
            found_marker = true;
            continue;
        }
        if found_marker {
            post_marker_words.push(word);
        } else {
            pre_marker_words.push(word);
        }
    }

    if !post_marker_words.is_empty() {
        payee = Some(post_marker_words.join(" "));
    }

    // Try to match first pre-marker word(s) to a category
    let mut matched_category: Option<(String, i64)> = None;
    let mut note_words: Vec<&str> = Vec::new();

    if !pre_marker_words.is_empty() {
        // Try matching progressively fewer words from the start
        for len in (1..=pre_marker_words.len()).rev() {
            let candidate = pre_marker_words[..len].join(" ");
            if let Some(cat) = fuzzy_match_category(&candidate, categories) {
                matched_category = Some((cat.name.clone(), cat.id));
                note_words = pre_marker_words[len..].to_vec();
                break;
            }
        }
        // If no match, treat all words as note
        if matched_category.is_none() {
            note_words = pre_marker_words;
        }
    }

    let note = if note_words.is_empty() {
        None
    } else {
        Some(note_words.join(" "))
    };

    Some(ParsedTransaction {
        amount,
        category_name: matched_category.as_ref().map(|(n, _)| n.clone()),
        category_id: matched_category.map(|(_, id)| id),
        note,
        payee,
        transaction_type: TransactionType::Expense,
    })
}

fn fuzzy_match_category<'a>(query: &str, categories: &'a [Category]) -> Option<&'a Category> {
    let query_lower = query.to_lowercase();

    if let Some(cat) = categories
        .iter()
        .find(|c| c.name.to_lowercase() == query_lower)
    {
        return Some(cat);
    }

    if let Some(cat) = categories
        .iter()
        .find(|c| c.name.to_lowercase().starts_with(&query_lower))
    {
        return Some(cat);
    }

    if let Some(cat) = categories
        .iter()
        .find(|c| c.name.to_lowercase().contains(&query_lower))
    {
        return Some(cat);
    }

    None
}

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

    fn test_categories() -> Vec<Category> {
        vec![
            Category {
                id: 1,
                name: "Food and Dining".into(),
                icon: None,
                color: None,
                transaction_type: TransactionType::Expense,
                is_default: false,
                sort_order: 0,
                parent_id: None,
            },
            Category {
                id: 2,
                name: "Transport".into(),
                icon: None,
                color: None,
                transaction_type: TransactionType::Expense,
                is_default: false,
                sort_order: 0,
                parent_id: None,
            },
            Category {
                id: 3,
                name: "Groceries".into(),
                icon: None,
                color: None,
                transaction_type: TransactionType::Expense,
                is_default: false,
                sort_order: 0,
                parent_id: None,
            },
            Category {
                id: 4,
                name: "Gas".into(),
                icon: None,
                color: None,
                transaction_type: TransactionType::Expense,
                is_default: false,
                sort_order: 0,
                parent_id: None,
            },
            Category {
                id: 5,
                name: "Coffee".into(),
                icon: None,
                color: None,
                transaction_type: TransactionType::Expense,
                is_default: false,
                sort_order: 0,
                parent_id: None,
            },
        ]
    }

    #[test]
    fn test_simple_category_amount() {
        let cats = test_categories();
        let result = parse_transaction("Coffee 4.50", &cats).unwrap();
        assert!((result.amount - 4.50).abs() < 0.001);
        assert_eq!(result.category_id, Some(5));
        assert!(result.payee.is_none());
    }

    #[test]
    fn test_amount_first() {
        let cats = test_categories();
        let result = parse_transaction("4.50 groceries milk", &cats).unwrap();
        assert!((result.amount - 4.50).abs() < 0.001);
        assert_eq!(result.category_id, Some(3));
        assert_eq!(result.note.as_deref(), Some("milk"));
    }

    #[test]
    fn test_with_payee() {
        let cats = test_categories();
        let result = parse_transaction("Coffee 12.50 at Starbucks", &cats).unwrap();
        assert!((result.amount - 12.50).abs() < 0.001);
        assert_eq!(result.category_id, Some(5));
        assert_eq!(result.payee.as_deref(), Some("Starbucks"));
    }

    #[test]
    fn test_dollar_sign() {
        let cats = test_categories();
        let result = parse_transaction("$25 gas", &cats).unwrap();
        assert!((result.amount - 25.0).abs() < 0.001);
        assert_eq!(result.category_id, Some(4));
    }

    #[test]
    fn test_no_category_match() {
        let cats = test_categories();
        let result = parse_transaction("15.00 mystery", &cats).unwrap();
        assert!((result.amount - 15.0).abs() < 0.001);
        assert!(result.category_id.is_none());
    }

    #[test]
    fn test_empty_input() {
        let cats = test_categories();
        assert!(parse_transaction("", &cats).is_none());
    }

    #[test]
    fn test_no_amount() {
        let cats = test_categories();
        assert!(parse_transaction("just some words", &cats).is_none());
    }
}
