# src/fmatch/saas/services/ensemble_explanations.py
from __future__ import annotations
from typing import List, Dict, Any
import os

_CONTRIB_T = float(os.getenv("ENSEMBLE_CONTRIB_THRESHOLD", "0.10"))
_MAX_FLAGS = int(os.getenv("ENSEMBLE_FLAG_MAX", "4"))
_MAX_EXPL = int(os.getenv("ENSEMBLE_EXPLANATION_MAX", "5"))


def _field_pair(c: Dict[str, Any]) -> str:
    """Build field pair string using ASCII arrow for Windows compatibility"""
    sf = (
        c.get("src_field")
        or c.get("source_field")
        or c.get("source_column")
        or c.get("source")
        or "?"
    )
    rf = (
        c.get("ref_field")
        or c.get("reference_field")
        or c.get("reference_column")
        or c.get("reference")
        or c.get("target")
        or "?"
    )
    return f"{sf}->{rf}"


def _name_for_flags(name: str, algo: str) -> str:
    """Generate key for flag detection"""
    n = name.lower()
    a = (algo or "").lower()
    return n + "|" + a


def _ensure_contributions(fsd: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
    """Ensure each detail has a 'contribution' value.
    If the engine didn't send contributions, synthesize them from weight*score."""
    if not fsd:
        return []

    # Check if contributions already present
    if fsd and "contribution" in fsd[0]:
        return fsd

    # Calculate total weighted score for normalization
    total_weighted = sum((d.get("weight", 1.0) * d.get("score", 0.0)) for d in fsd)
    if total_weighted <= 0:
        total_weighted = 1.0

    # Add contribution to each field score detail
    out = []
    for d in fsd:
        weight = d.get("weight", 1.0)
        score = d.get("score", 0.0)
        contribution = (weight * score) / total_weighted

        d2 = dict(d)
        d2["contribution"] = round(float(contribution), 3)
        out.append(d2)

    return out


def summarize_evidence(
    fsd: List[Dict[str, Any]], use_multi_algo: bool = False
) -> Dict[str, Any]:
    """
    Summarize field score details into ensemble evidence.
    Returns:
    - ensemble_used: bool - whether ensemble scoring was used
    - signal_count: int - number of contributing signals
    - signal_strength: float - average strength of signals
    - signal_flags: List[str] - notable signal characteristics
    """
    if not fsd:
        return {
            "ensemble_used": False,
            "evidence_count": 0,
            "evidence_strength": 0.0,
            "evidence_flags": [],
        }

    # Ensure contributions exist
    fsd = _ensure_contributions(fsd)

    # Count signals above threshold
    signals = [d for d in fsd if d.get("contribution", 0) >= _CONTRIB_T]
    signal_count = len(signals)

    # Calculate average strength
    signal_strength = 0.0
    if signals:
        signal_strength = sum(d.get("score", 0) for d in signals) / len(signals)

    # Detect signal flags
    flags = []

    # Check for multi-algorithm usage - only count distinct non-empty algorithms
    algos = {(d.get("algorithm") or "").lower() for d in signals if d.get("algorithm")}
    ensemble_used = bool(use_multi_algo or len(algos) > 1)
    if ensemble_used:
        flags.append("multi-algo")

    # Check for exact matches
    exact_matches = [d for d in signals if d.get("score", 0) >= 0.95]
    if exact_matches:
        flags.append(f"{len(exact_matches)} exact")

    # Check for fuzzy matches
    fuzzy_matches = [d for d in signals if 0.7 <= d.get("score", 0) < 0.95]
    if fuzzy_matches:
        flags.append(f"{len(fuzzy_matches)} fuzzy")

    # Check for weak signals
    weak_matches = [d for d in signals if d.get("score", 0) < 0.7]
    if weak_matches:
        flags.append(f"{len(weak_matches)} weak")

    # Limit flags
    flags = flags[:_MAX_FLAGS]

    return {
        "ensemble_used": ensemble_used,
        "evidence_count": signal_count,  # Frontend expects "evidence_" prefix
        "evidence_strength": round(signal_strength, 3),
        "evidence_flags": flags,
    }


def compact_explanations(fsd: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
    """
    Generate compact explanations from field score details.
    Returns a list of explanation objects with:
    - field: str - field pair being compared
    - score: float - match score
    - algorithm: str - algorithm used
    - explanation: str - human-readable explanation
    """
    if not fsd:
        return []

    # Ensure contributions exist
    fsd = _ensure_contributions(fsd)

    # Filter to significant contributions and sort by contribution
    significant = [d for d in fsd if d.get("contribution", 0) >= _CONTRIB_T]
    significant.sort(key=lambda x: x.get("contribution", 0), reverse=True)

    explanations = []
    for d in significant[:_MAX_EXPL]:
        field = _field_pair(d)
        score = d.get("score", 0)
        algo = d.get("algorithm", "WRatio")
        contrib = d.get("contribution", 0)

        # Generate explanation text
        if score >= 0.95:
            explanation = f"Exact match on {field}"
        elif score >= 0.8:
            explanation = f"Strong match on {field} ({int(score*100)}% similar)"
        elif score >= 0.7:
            explanation = f"Good match on {field} ({int(score*100)}% similar)"
        else:
            explanation = f"Partial match on {field} ({int(score*100)}% similar)"

        # Add algorithm if not default
        if algo != "WRatio":
            explanation += f" using {algo}"

        # Add contribution if significant
        if contrib >= 0.25:
            explanation += " (major factor)"
        elif contrib >= 0.15:
            explanation += " (significant factor)"

        explanations.append(
            {
                "field": field,
                "score": round(score, 3),
                "algorithm": algo,
                "contribution": round(contrib, 3),
                "explanation": explanation,
            }
        )

    return explanations


def build_ensemble_summary(
    items: List[Dict[str, Any]],
    present_only_scoring: bool = False,
    multi_algo_enabled: bool = False,
) -> Dict[str, Any]:
    """
    Build a summary of ensemble scoring across multiple items.
    Used for run-level summaries.
    """
    if not items:
        return {
            "ensemble_enabled": multi_algo_enabled,
            "items_with_ensemble": 0,
            "average_signal_count": 0,
            "average_signal_strength": 0,
            "common_flags": [],
        }

    # Count items using ensemble
    ensemble_items = [i for i in items if i.get("ensemble_used", False)]

    # Calculate averages
    avg_count = 0
    avg_strength = 0
    all_flags = []

    if ensemble_items:
        avg_count = sum(i.get("evidence_count", 0) for i in ensemble_items) / len(
            ensemble_items
        )
        avg_strength = sum(i.get("evidence_strength", 0) for i in ensemble_items) / len(
            ensemble_items
        )

        # Collect all flags
        for i in ensemble_items:
            all_flags.extend(i.get("evidence_flags", []))

    # Find most common flags
    flag_counts = {}
    for flag in all_flags:
        flag_counts[flag] = flag_counts.get(flag, 0) + 1

    common_flags = sorted(
        flag_counts.keys(), key=lambda x: flag_counts[x], reverse=True
    )[:3]

    return {
        "ensemble_enabled": multi_algo_enabled,
        "items_with_ensemble": len(ensemble_items),
        "average_signal_count": round(avg_count, 1),
        "average_signal_strength": round(avg_strength, 3),
        "common_flags": common_flags,
    }
