use regex::Regex;

use crate::normalize::{author_family_names, normalize_title, normalize_venue};
use crate::types::{ReferenceFields, ScoreBreakdown, ScoreConfig, ScoreResult};

fn token_re() -> Regex {
    Regex::new(r"[a-z0-9]+").expect("valid regex")
}

fn tokenize(text: &str) -> Vec<String> {
    let re = token_re();
    re.find_iter(text)
        .map(|m| m.as_str().to_string())
        .collect()
}

pub fn title_similarity(a: &str, b: &str) -> f64 {
    let norm_a = normalize_title(a);
    let norm_b = normalize_title(b);
    if norm_a.is_empty() || norm_b.is_empty() {
        return 0.0;
    }
    let tokens_a: std::collections::HashSet<_> = tokenize(&norm_a).into_iter().collect();
    let tokens_b: std::collections::HashSet<_> = tokenize(&norm_b).into_iter().collect();
    if tokens_a.is_empty() || tokens_b.is_empty() {
        return 0.0;
    }
    let intersection = tokens_a.intersection(&tokens_b).count() as f64;
    let union = tokens_a.union(&tokens_b).count() as f64;
    let jaccard = intersection / union;
    if tokens_a.len() <= 4 || tokens_b.len() <= 4 {
        if tokens_a.is_subset(&tokens_b) || tokens_b.is_subset(&tokens_a) {
            return jaccard.max(0.8);
        }
    }
    jaccard
}

pub fn author_overlap(a: &[String], b: &[String]) -> f64 {
    let fam_a: std::collections::HashSet<_> = author_family_names(a).into_iter().collect();
    let fam_b: std::collections::HashSet<_> = author_family_names(b).into_iter().collect();
    if fam_a.is_empty() || fam_b.is_empty() {
        return 0.0;
    }
    let intersection = fam_a.intersection(&fam_b).count() as f64;
    let max_len = fam_a.len().max(fam_b.len()) as f64;
    intersection / max_len
}

pub fn year_score(a: Option<i32>, b: Option<i32>) -> f64 {
    match (a, b) {
        (Some(year_a), Some(year_b)) => {
            let diff = (year_a - year_b).abs();
            if diff == 0 {
                1.0
            } else if diff == 1 {
                0.5
            } else if diff == 2 {
                0.2
            } else {
                0.0
            }
        }
        _ => 0.0,
    }
}

pub fn venue_similarity(a: &str, b: &str) -> f64 {
    let norm_a = normalize_venue(a);
    let norm_b = normalize_venue(b);
    if norm_a.is_empty() || norm_b.is_empty() {
        return 0.0;
    }
    let tokens_a: std::collections::HashSet<_> = tokenize(&norm_a).into_iter().collect();
    let tokens_b: std::collections::HashSet<_> = tokenize(&norm_b).into_iter().collect();
    if tokens_a.is_empty() || tokens_b.is_empty() {
        return 0.0;
    }
    let intersection = tokens_a.intersection(&tokens_b).count() as f64;
    let union = tokens_a.union(&tokens_b).count() as f64;
    intersection / union
}

pub fn score_candidate(
    reference: &ReferenceFields,
    candidate: &ReferenceFields,
    config: Option<&ScoreConfig>,
) -> ScoreResult {
    let cfg = config.cloned().unwrap_or_else(ScoreConfig::new);
    let title_score = title_similarity(
        reference.title.as_deref().unwrap_or(""),
        candidate.title.as_deref().unwrap_or(""),
    );
    let author_score = author_overlap(&reference.authors, &candidate.authors);
    let year_val = year_score(reference.year, candidate.year);
    let venue_score = venue_similarity(
        reference.venue.as_deref().unwrap_or(""),
        candidate.venue.as_deref().unwrap_or(""),
    );

    let score = title_score * cfg.title_weight
        + author_score * cfg.author_weight
        + year_val * cfg.year_weight
        + venue_score * cfg.venue_weight;

    ScoreResult {
        score,
        breakdown: ScoreBreakdown {
            title: title_score,
            authors: author_score,
            year: year_val,
            venue: venue_score,
        },
    }
}
