"""Event and relation macro/micro F1 evaluation metrics.

Implements evaluation metrics for event classification and relation extraction
quality against a gold standard corpus. Supports both macro-F1 (average across
classes) and micro-F1 (global TP/FP/FN) with per-class breakdowns.

Matching logic:
- Events match if they share the same event_class AND have overlapping evidence
  spans OR the same primary company.
- Relations match if they share the same relation_type, source_id, and target_id.

Validates: Requirements 16.3, 16.4
"""
from __future__ import annotations

from pydantic import BaseModel, Field

from services.intelligence_pipeline_v3.schemas.annotations import (
    EventClass,
    RelationType,
)

# ---------------------------------------------------------------------------
# Input Models
# ---------------------------------------------------------------------------


class PredictedEvent(BaseModel):
    """A predicted event for evaluation."""

    event_class: EventClass
    evidence_ids: list[str] = Field(default_factory=list)
    primary_company_ids: list[str] = Field(default_factory=list)
    confidence: float = Field(ge=0.0, le=1.0, default=1.0)


class GoldEvent(BaseModel):
    """A gold standard event for evaluation."""

    event_class: EventClass
    evidence_ids: list[str] = Field(default_factory=list)
    primary_company_ids: list[str] = Field(default_factory=list)


class PredictedRelation(BaseModel):
    """A predicted relation for evaluation."""

    relation_type: RelationType
    source_id: str
    target_id: str
    confidence: float = Field(ge=0.0, le=1.0, default=1.0)


class GoldRelation(BaseModel):
    """A gold standard relation for evaluation."""

    relation_type: RelationType
    source_id: str
    target_id: str


# ---------------------------------------------------------------------------
# Result Models
# ---------------------------------------------------------------------------


class PRF1(BaseModel):
    """Precision, recall, F1 triple."""

    precision: float = Field(ge=0.0, le=1.0)
    recall: float = Field(ge=0.0, le=1.0)
    f1: float = Field(ge=0.0, le=1.0)
    support_predicted: int = Field(ge=0)
    support_gold: int = Field(ge=0)


class EventMetricsResult(BaseModel):
    """Full event evaluation result with macro/micro F1 and per-class breakdown."""

    macro_f1: float = Field(ge=0.0, le=1.0)
    micro: PRF1
    per_class: dict[str, PRF1]


class RelationMetricsResult(BaseModel):
    """Full relation evaluation result with macro/micro F1 and per-type breakdown."""

    macro_f1: float = Field(ge=0.0, le=1.0)
    micro: PRF1
    per_type: dict[str, PRF1]


class EventRelationEvaluationReport(BaseModel):
    """Complete event and relation evaluation report."""

    event_metrics: EventMetricsResult
    relation_metrics: RelationMetricsResult
    document_count: int = Field(ge=0)


# ---------------------------------------------------------------------------
# Matching Logic
# ---------------------------------------------------------------------------


def _events_match(pred: PredictedEvent, gold: GoldEvent) -> bool:
    """Events match if same event_class AND overlapping evidence OR same primary company.

    Overlap means at least one evidence_id in common, OR at least one
    primary_company_id in common.
    """
    if pred.event_class != gold.event_class:
        return False

    # Check overlapping evidence spans
    if pred.evidence_ids and gold.evidence_ids:
        if set(pred.evidence_ids) & set(gold.evidence_ids):
            return True

    # Check same primary company
    if pred.primary_company_ids and gold.primary_company_ids:
        if set(pred.primary_company_ids) & set(gold.primary_company_ids):
            return True

    return False


def _relations_match(pred: PredictedRelation, gold: GoldRelation) -> bool:
    """Relations match if same type, source, and target."""
    return (
        pred.relation_type == gold.relation_type
        and pred.source_id == gold.source_id
        and pred.target_id == gold.target_id
    )


# ---------------------------------------------------------------------------
# Core Metric Computation
# ---------------------------------------------------------------------------


def _compute_prf1_greedy(
    predicted: list,
    gold: list,
    match_fn: object,
) -> PRF1:
    """Compute precision, recall, F1 using greedy bipartite matching.

    Each predicted item can match at most one gold item and vice versa.
    """
    n_pred = len(predicted)
    n_gold = len(gold)

    if n_pred == 0 and n_gold == 0:
        return PRF1(
            precision=1.0, recall=1.0, f1=1.0,
            support_predicted=0, support_gold=0,
        )

    if n_pred == 0:
        return PRF1(
            precision=1.0, recall=0.0, f1=0.0,
            support_predicted=0, support_gold=n_gold,
        )

    if n_gold == 0:
        return PRF1(
            precision=0.0, recall=1.0, f1=0.0,
            support_predicted=n_pred, support_gold=0,
        )

    matched_gold: set[int] = set()
    true_positives = 0

    for p in predicted:
        for g_idx, g in enumerate(gold):
            if g_idx in matched_gold:
                continue
            if match_fn(p, g):  # type: ignore[operator]
                true_positives += 1
                matched_gold.add(g_idx)
                break

    precision = true_positives / n_pred if n_pred > 0 else 0.0
    recall = true_positives / n_gold if n_gold > 0 else 0.0

    if precision + recall > 0:
        f1 = 2 * precision * recall / (precision + recall)
    else:
        f1 = 0.0

    return PRF1(
        precision=precision,
        recall=recall,
        f1=f1,
        support_predicted=n_pred,
        support_gold=n_gold,
    )


def _compute_micro_prf1(
    predicted: list,
    gold: list,
    match_fn: object,
    class_key_pred: object,
    class_key_gold: object,
    all_classes: set[str],
) -> PRF1:
    """Compute micro-averaged PRF1 by summing TP/FP/FN across all classes."""
    total_tp = 0
    total_pred = 0
    total_gold = 0

    for cls in all_classes:
        cls_predicted = [p for p in predicted if class_key_pred(p) == cls]
        cls_gold = [g for g in gold if class_key_gold(g) == cls]

        total_pred += len(cls_predicted)
        total_gold += len(cls_gold)

        # Greedy match within this class
        matched_gold: set[int] = set()
        for p in cls_predicted:
            for g_idx, g in enumerate(cls_gold):
                if g_idx in matched_gold:
                    continue
                if match_fn(p, g):  # type: ignore[operator]
                    total_tp += 1
                    matched_gold.add(g_idx)
                    break

    if total_pred == 0 and total_gold == 0:
        return PRF1(
            precision=1.0, recall=1.0, f1=1.0,
            support_predicted=0, support_gold=0,
        )

    precision = total_tp / total_pred if total_pred > 0 else 0.0
    recall = total_tp / total_gold if total_gold > 0 else 0.0

    if precision + recall > 0:
        f1 = 2 * precision * recall / (precision + recall)
    else:
        f1 = 0.0

    return PRF1(
        precision=precision,
        recall=recall,
        f1=f1,
        support_predicted=total_pred,
        support_gold=total_gold,
    )


# ---------------------------------------------------------------------------
# Public API — Events
# ---------------------------------------------------------------------------


def compute_event_metrics(
    predicted: list[PredictedEvent],
    gold: list[GoldEvent],
) -> EventMetricsResult:
    """Compute event macro-F1, micro-F1, and per-class F1.

    Args:
        predicted: Predicted events.
        gold: Gold standard events.

    Returns:
        EventMetricsResult with macro, micro, and per-class breakdowns.
    """
    all_classes = {e.value for e in EventClass}

    # Per-class breakdown
    per_class: dict[str, PRF1] = {}
    f1_scores: list[float] = []

    for cls in sorted(all_classes):
        cls_predicted = [p for p in predicted if p.event_class.value == cls]
        cls_gold = [g for g in gold if g.event_class.value == cls]
        prf1 = _compute_prf1_greedy(cls_predicted, cls_gold, _events_match)
        per_class[cls] = prf1
        f1_scores.append(prf1.f1)

    # Macro-F1: average F1 across all event classes
    macro_f1 = sum(f1_scores) / len(f1_scores) if f1_scores else 0.0

    # Micro-F1: global TP/FP/FN
    micro = _compute_micro_prf1(
        predicted, gold, _events_match,
        lambda p: p.event_class.value,
        lambda g: g.event_class.value,
        all_classes,
    )

    return EventMetricsResult(
        macro_f1=macro_f1,
        micro=micro,
        per_class=per_class,
    )


# ---------------------------------------------------------------------------
# Public API — Relations
# ---------------------------------------------------------------------------


def compute_relation_metrics(
    predicted: list[PredictedRelation],
    gold: list[GoldRelation],
) -> RelationMetricsResult:
    """Compute relation macro-F1, micro-F1, and per-type F1.

    Args:
        predicted: Predicted relations.
        gold: Gold standard relations.

    Returns:
        RelationMetricsResult with macro, micro, and per-type breakdowns.
    """
    all_types = {r.value for r in RelationType}

    # Per-type breakdown
    per_type: dict[str, PRF1] = {}
    f1_scores: list[float] = []

    for rtype in sorted(all_types):
        type_predicted = [p for p in predicted if p.relation_type.value == rtype]
        type_gold = [g for g in gold if g.relation_type.value == rtype]
        prf1 = _compute_prf1_greedy(type_predicted, type_gold, _relations_match)
        per_type[rtype] = prf1
        f1_scores.append(prf1.f1)

    # Macro-F1: average F1 across all relation types
    macro_f1 = sum(f1_scores) / len(f1_scores) if f1_scores else 0.0

    # Micro-F1: global TP/FP/FN
    micro = _compute_micro_prf1(
        predicted, gold, _relations_match,
        lambda p: p.relation_type.value,
        lambda g: g.relation_type.value,
        all_types,
    )

    return RelationMetricsResult(
        macro_f1=macro_f1,
        micro=micro,
        per_type=per_type,
    )


# ---------------------------------------------------------------------------
# Public API — Combined Report
# ---------------------------------------------------------------------------


def evaluate_events_and_relations(
    predicted_events: list[PredictedEvent],
    gold_events: list[GoldEvent],
    predicted_relations: list[PredictedRelation],
    gold_relations: list[GoldRelation],
    document_count: int = 1,
) -> EventRelationEvaluationReport:
    """Run full event and relation evaluation producing a complete report.

    Args:
        predicted_events: All predicted events.
        gold_events: All gold standard events.
        predicted_relations: All predicted relations.
        gold_relations: All gold standard relations.
        document_count: Number of documents evaluated.

    Returns:
        EventRelationEvaluationReport with event metrics, relation metrics.
    """
    event_metrics = compute_event_metrics(predicted_events, gold_events)
    relation_metrics = compute_relation_metrics(predicted_relations, gold_relations)

    return EventRelationEvaluationReport(
        event_metrics=event_metrics,
        relation_metrics=relation_metrics,
        document_count=document_count,
    )
