use crate::audit::normalized::OccurrenceDetail;
use crate::i18n::I18n;
use crate::output::explanations::get_explanation;
use crate::output::report_model::{
    classify_criticality_tier, Effort, FindingGroup, FindingPatternCluster,
    RepresentativeOccurrence, Role,
};

use super::super::actions::{
    build_narrative_arc, derive_business_impact, derive_execution_priority, severity_to_priority,
};

pub(super) fn finding_group_from_normalized(
    i18n: &I18n,
    f: &crate::audit::normalized::NormalizedFinding,
) -> FindingGroup {
    let locale = i18n.locale();
    // Try the taxonomy rule_id first (e.g. "a11y.aria_hidden_focus.invalid"),
    // then fall back to the WCAG criterion. Some rules carry their localized
    // explanation under the taxonomy key, not the WCAG number — looking up by
    // wcag_criterion alone left those findings with the raw English fix (#357).
    let explanation = get_explanation(&f.rule_id).or_else(|| get_explanation(&f.wcag_criterion));

    let (
        title,
        customer_desc,
        user_impact_text,
        business_impact,
        typical_cause,
        recommendation,
        technical_note,
        role,
        effort,
        execution_priority,
    ) = if let Some(expl) = explanation {
        (
            expl.customer_title_for(locale).to_string(),
            expl.customer_description_for(locale).to_string(),
            expl.user_impact_for(locale).to_string(),
            derive_business_impact(
                i18n,
                expl.user_impact_for(locale),
                f.dimension.as_str(),
                f.severity,
                Some(f.subcategory.as_str()),
                f.occurrence_count,
            ),
            expl.typical_cause_for(locale).to_string(),
            expl.recommendation_for(locale).to_string(),
            expl.technical_note_for(locale).to_string(),
            expl.responsible_role,
            expl.effort_estimate,
            derive_execution_priority(f.severity, expl.effort_estimate, f.dimension.as_str()),
        )
    } else {
        (
            f.title.clone(),
            f.description.clone(),
            f.user_impact.clone(),
            derive_business_impact(
                i18n,
                &f.user_impact,
                f.dimension.as_str(),
                f.severity,
                Some(f.subcategory.as_str()),
                f.occurrence_count,
            ),
            String::new(),
            f.occurrences
                .first()
                .and_then(|o| o.fix_suggestion.clone())
                .unwrap_or_default(),
            String::new(),
            Role::Development,
            Effort::Medium,
            derive_execution_priority(f.severity, Effort::Medium, f.dimension.as_str()),
        )
    };

    let examples = explanation.map(|e| e.examples()).unwrap_or_default();
    let location_hints = build_location_hints(&f.occurrences);
    let representative_occurrences = build_representative_occurrences(&f.occurrences);
    let pattern_clusters = build_pattern_clusters(&f.occurrences);
    let additional_occurrences = f
        .occurrence_count
        .saturating_sub(representative_occurrences.len());

    let narrative = build_narrative_arc(
        i18n,
        f.occurrence_count,
        f.severity,
        f.dimension.as_str(),
        &customer_desc,
        &user_impact_text,
        &business_impact,
        &typical_cause,
        &recommendation,
        effort,
        role,
    );

    FindingGroup {
        title,
        rule_id: f.rule_id.clone(),
        wcag_criterion: f.wcag_criterion.clone(),
        wcag_level: f.wcag_level.clone(),
        help_url: f.help_url.clone(),
        dimension: Some(f.dimension.clone()),
        subcategory: Some(f.subcategory.clone()),
        issue_class: Some(f.issue_class.clone()),
        severity: f.severity,
        priority: severity_to_priority(f.severity),
        customer_description: customer_desc,
        user_impact: user_impact_text,
        business_impact,
        typical_cause,
        recommendation,
        technical_note,
        confidence: f.confidence.clone(),
        false_positive_risk: f.false_positive_risk.clone(),
        verification: f.verification.clone(),
        complexity: f.complexity.clone(),
        complexity_reason: f.complexity_reason.clone(),
        expected_impact: f.expected_impact.clone(),
        bfsg_relevance: f.bfsg_relevance.clone(),
        remediation_priority: f.remediation_priority.clone(),
        occurrence_count: f.occurrence_count,
        affected_urls: Vec::new(),
        affected_elements: f.occurrence_count,
        additional_occurrences,
        pattern_clusters,
        location_hints,
        representative_occurrences,
        responsible_role: role,
        effort,
        execution_priority,
        examples,
        structural_cause: if f.occurrence_count >= 10 {
            Some(i18n.t_args(
                "finding-structural-cause-component",
                &[("count", f.occurrence_count.to_string())],
            ))
        } else if f.occurrence_count >= 5 {
            Some(i18n.t_args(
                "finding-structural-cause-shared",
                &[("count", f.occurrence_count.to_string())],
            ))
        } else {
            None
        },
        is_component_issue: f.occurrence_count >= 10,
        criticality_tier: classify_criticality_tier(&f.category, &f.wcag_level),
        narrative,
    }
}

fn build_location_hints(occurrences: &[OccurrenceDetail]) -> Vec<String> {
    let mut hints = Vec::new();
    for occ in occurrences {
        let hint = if let Some(selector) = &occ.selector {
            selector.trim().to_string()
        } else {
            format!("AX-Node {}", occ.node_id)
        };
        if !hint.is_empty() && !hints.contains(&hint) {
            hints.push(hint);
        }
        if hints.len() >= 5 {
            break;
        }
    }
    hints
}

fn build_representative_occurrences(
    occurrences: &[OccurrenceDetail],
) -> Vec<RepresentativeOccurrence> {
    let mut ranked: Vec<(usize, i32, &OccurrenceDetail)> = occurrences
        .iter()
        .enumerate()
        .map(|(index, occ)| (index, representative_occurrence_score(occ), occ))
        .collect();
    ranked.sort_by(|a, b| {
        b.1.cmp(&a.1)
            .then_with(|| a.0.cmp(&b.0))
            .then_with(|| b.2.message.len().cmp(&a.2.message.len()))
    });

    let mut items = Vec::new();
    let mut seen = std::collections::HashSet::new();

    for (_, _, occ) in ranked {
        let selector = representative_selector(occ);

        if !seen.insert(selector.to_ascii_lowercase()) {
            continue;
        }

        items.push(RepresentativeOccurrence {
            selector,
            node_id: occ.node_id.clone(),
            message: occ.message.clone(),
            html_snippet: occ.html_snippet.clone(),
            suggested_code: occ.suggested_code.clone(),
        });

        if items.len() >= 3 {
            break;
        }
    }

    items
}

fn build_pattern_clusters(occurrences: &[OccurrenceDetail]) -> Vec<FindingPatternCluster> {
    let mut clusters: std::collections::BTreeMap<String, (String, usize)> =
        std::collections::BTreeMap::new();

    for occ in occurrences {
        let selector = representative_selector(occ);
        let normalized = normalize_selector_cluster(&selector);
        let entry = clusters.entry(normalized).or_insert((selector.clone(), 0));
        entry.1 += 1;

        if selector.len() < entry.0.len() {
            entry.0 = selector;
        }
    }

    let mut items: Vec<FindingPatternCluster> = clusters
        .into_values()
        .map(|(label, occurrences)| FindingPatternCluster { label, occurrences })
        .collect();
    items.sort_by(|a, b| {
        b.occurrences
            .cmp(&a.occurrences)
            .then_with(|| a.label.len().cmp(&b.label.len()))
    });
    items.truncate(3);
    items
}

fn representative_selector(occ: &OccurrenceDetail) -> String {
    occ.selector
        .as_deref()
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .unwrap_or(&occ.node_id)
        .to_string()
}

fn normalize_selector_cluster(selector: &str) -> String {
    let trimmed = selector.trim();
    if trimmed.is_empty() {
        return "unspecified".to_string();
    }

    let normalized: String = trimmed
        .chars()
        .map(|ch| {
            if ch.is_ascii_digit() {
                '#'
            } else if ch.is_whitespace() {
                ' '
            } else {
                ch.to_ascii_lowercase()
            }
        })
        .collect();

    let mut compact = String::new();
    let mut last_was_hash = false;
    let mut last_was_space = false;
    for ch in normalized.chars() {
        match ch {
            '#' => {
                if !last_was_hash {
                    compact.push(ch);
                }
                last_was_hash = true;
                last_was_space = false;
            }
            ' ' => {
                if !last_was_space {
                    compact.push(ch);
                }
                last_was_space = true;
                last_was_hash = false;
            }
            _ => {
                compact.push(ch);
                last_was_hash = false;
                last_was_space = false;
            }
        }
    }

    compact
}

fn representative_occurrence_score(occ: &OccurrenceDetail) -> i32 {
    let mut score = 0;

    let selector = occ
        .selector
        .as_deref()
        .map(str::trim)
        .filter(|value| !value.is_empty());

    if let Some(selector) = selector {
        score += 40;
        score += selector_quality_score(selector);
    } else {
        score -= 12;
    }

    if has_content(&occ.html_snippet) {
        score += 28;
    }
    if has_content(&occ.suggested_code) {
        score += 22;
    }
    if has_content(&occ.fix_suggestion) {
        score += 8;
    }

    let message = occ.message.trim();
    if !message.is_empty() {
        score += 6;
        score += (message.len().min(120) / 20) as i32;
        if message.contains(':') || message.contains('(') {
            score += 3;
        }
        if message.chars().any(|ch| ch.is_ascii_digit()) {
            score += 4;
        }
    }

    if !occ.node_id.trim().is_empty() {
        score += 2;
    }

    score
}

fn selector_quality_score(selector: &str) -> i32 {
    let mut score = 0;

    if selector.contains('#') {
        score += 16;
    }
    if selector.contains('[') {
        score += 10;
    }
    if selector.contains('.') {
        score += 8;
    }
    if selector.contains('>') {
        score += 6;
    }
    if selector.contains(' ') {
        score += 4;
    }
    if selector.starts_with("main")
        || selector.starts_with("header")
        || selector.starts_with("nav")
        || selector.starts_with("footer")
    {
        score += 4;
    }

    score
}

fn has_content(value: &Option<String>) -> bool {
    value.as_deref().is_some_and(|text| !text.trim().is_empty())
}

#[cfg(test)]
mod tests {
    use super::{
        build_pattern_clusters, build_representative_occurrences, normalize_selector_cluster,
    };
    use crate::audit::normalized::OccurrenceDetail;

    #[test]
    fn representative_occurrences_prefer_rich_and_actionable_examples() {
        let occurrences = vec![
            OccurrenceDetail {
                node_id: "node-1".into(),
                message: "Short".into(),
                selector: None,
                fix_suggestion: None,
                html_snippet: None,
                suggested_code: None,
                tags: vec![],
            },
            OccurrenceDetail {
                node_id: "node-2".into(),
                message: "Contrast ratio 1.13:1 for hero headline.".into(),
                selector: Some("main .hero-title".into()),
                fix_suggestion: Some("Increase foreground/background contrast.".into()),
                html_snippet: Some("<h1 class=\"hero-title\">Insights</h1>".into()),
                suggested_code: None,
                tags: vec![],
            },
            OccurrenceDetail {
                node_id: "node-3".into(),
                message: "Contrast ratio 1.00:1 for CTA button text.".into(),
                selector: Some("#cta-primary".into()),
                fix_suggestion: Some("Use darker text color.".into()),
                html_snippet: Some("<a id=\"cta-primary\">Kontakt</a>".into()),
                suggested_code: Some(
                    "<a id=\"cta-primary\" class=\"text-stone-900\">Kontakt</a>".into(),
                ),
                tags: vec![],
            },
            OccurrenceDetail {
                node_id: "node-4".into(),
                message: "Link landmark is outside a region.".into(),
                selector: Some("a.skip-link".into()),
                fix_suggestion: None,
                html_snippet: None,
                suggested_code: None,
                tags: vec![],
            },
        ];

        let selected = build_representative_occurrences(&occurrences);

        assert_eq!(selected.len(), 3);
        assert_eq!(selected[0].selector, "#cta-primary");
        assert_eq!(selected[1].selector, "main .hero-title");
        assert_eq!(selected[2].selector, "a.skip-link");
    }

    #[test]
    fn representative_occurrences_deduplicate_selector_variants() {
        let occurrences = vec![
            OccurrenceDetail {
                node_id: "node-1".into(),
                message: "First duplicate".into(),
                selector: Some("main .hero-title".into()),
                fix_suggestion: None,
                html_snippet: Some("<h1 class=\"hero-title\">One</h1>".into()),
                suggested_code: None,
                tags: vec![],
            },
            OccurrenceDetail {
                node_id: "node-2".into(),
                message: "Second duplicate with richer text".into(),
                selector: Some("MAIN .HERO-TITLE".into()),
                fix_suggestion: Some("Adjust markup.".into()),
                html_snippet: Some("<h1 class=\"hero-title\">Two</h1>".into()),
                suggested_code: Some("<h1 lang=\"de\">Two</h1>".into()),
                tags: vec![],
            },
            OccurrenceDetail {
                node_id: "node-3".into(),
                message: "Independent selector".into(),
                selector: Some("footer .meta a".into()),
                fix_suggestion: None,
                html_snippet: None,
                suggested_code: None,
                tags: vec![],
            },
        ];

        let selected = build_representative_occurrences(&occurrences);

        assert_eq!(selected.len(), 2);
        assert_eq!(selected[0].selector, "MAIN .HERO-TITLE");
        assert_eq!(selected[1].selector, "footer .meta a");
    }

    #[test]
    fn pattern_clusters_group_similar_selector_variants() {
        let occurrences = vec![
            OccurrenceDetail {
                node_id: "node-1".into(),
                message: "One".into(),
                selector: Some("main .card-1 .cta".into()),
                fix_suggestion: None,
                html_snippet: None,
                suggested_code: None,
                tags: vec![],
            },
            OccurrenceDetail {
                node_id: "node-2".into(),
                message: "Two".into(),
                selector: Some("main .card-2 .cta".into()),
                fix_suggestion: None,
                html_snippet: None,
                suggested_code: None,
                tags: vec![],
            },
            OccurrenceDetail {
                node_id: "node-3".into(),
                message: "Three".into(),
                selector: Some("footer .meta a".into()),
                fix_suggestion: None,
                html_snippet: None,
                suggested_code: None,
                tags: vec![],
            },
        ];

        let clusters = build_pattern_clusters(&occurrences);

        assert_eq!(
            normalize_selector_cluster("main .card-1 .cta"),
            "main .card-# .cta"
        );
        assert_eq!(clusters[0].occurrences, 2);
        assert_eq!(clusters[0].label, "main .card-1 .cta");
    }
}
