"""
Segmentation metrics for evaluating temporal boundaries.

This module provides comprehensive evaluation metrics for temporal segmentation,
including boundary detection, label accuracy, segmentation rates, and detailed
per-segment and per-class analysis.
"""

from __future__ import annotations

from collections import Counter
from dataclasses import dataclass, field
from typing import Any

import numpy as np

from sltk.data.core import Segment, SegmentList


@dataclass
class SegmentMatch:
    """A matched pair of predicted and reference segments."""

    pred_idx: int
    ref_idx: int
    pred_segment: Segment
    ref_segment: Segment
    iou: float
    label_match: bool

    @property
    def boundary_error_start(self) -> float:
        """Absolute error of start boundary in seconds."""
        return abs(self.pred_segment.start - self.ref_segment.start)

    @property
    def boundary_error_end(self) -> float:
        """Absolute error of end boundary in seconds."""
        return abs(self.pred_segment.end - self.ref_segment.end)


@dataclass
class SegmentationResult:
    """
    Comprehensive segmentation evaluation results.

    Contains all metrics computed during evaluation, including boundary metrics,
    label accuracy, segmentation rates, per-segment details, and confusion matrix.
    """

    # Basic counts
    num_predictions: int = 0
    num_references: int = 0
    num_matched: int = 0

    # Boundary metrics (default tolerance)
    boundary_precision: float = 0.0
    boundary_recall: float = 0.0
    boundary_f1: float = 0.0

    # Boundary metrics at multiple tolerances
    boundary_f1_50ms: float = 0.0
    boundary_f1_100ms: float = 0.0
    boundary_f1_200ms: float = 0.0

    # IoU metrics
    mean_iou: float = 0.0
    per_segment_iou: list[float] = field(default_factory=list)

    # Label accuracy
    label_accuracy: float = 0.0
    label_precision: float = 0.0
    label_recall: float = 0.0
    label_f1: float = 0.0

    # Segmentation rates
    over_segmentation_rate: float = 0.0
    under_segmentation_rate: float = 0.0
    fragmentation_rate: float = 0.0

    # Per-class metrics
    per_class_precision: dict[str, float] = field(default_factory=dict)
    per_class_recall: dict[str, float] = field(default_factory=dict)
    per_class_f1: dict[str, float] = field(default_factory=dict)
    per_class_iou: dict[str, float] = field(default_factory=dict)
    per_class_count: dict[str, int] = field(default_factory=dict)

    # Confusion matrix: confusion_matrix[pred_label][ref_label] = count
    confusion_matrix: dict[str, dict[str, int]] = field(default_factory=dict)

    # Detailed matches
    matches: list[SegmentMatch] = field(default_factory=list)
    unmatched_predictions: list[int] = field(default_factory=list)
    unmatched_references: list[int] = field(default_factory=list)

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary for API responses."""
        return {
            # Basic counts
            "num_predictions": self.num_predictions,
            "num_references": self.num_references,
            "num_matched": self.num_matched,
            # Boundary metrics
            "boundary_precision": self.boundary_precision,
            "boundary_recall": self.boundary_recall,
            "boundary_f1": self.boundary_f1,
            "boundary_f1_50ms": self.boundary_f1_50ms,
            "boundary_f1_100ms": self.boundary_f1_100ms,
            "boundary_f1_200ms": self.boundary_f1_200ms,
            # IoU metrics
            "mean_iou": self.mean_iou,
            "per_segment_iou": self.per_segment_iou,
            # Label accuracy
            "label_accuracy": self.label_accuracy,
            "label_precision": self.label_precision,
            "label_recall": self.label_recall,
            "label_f1": self.label_f1,
            # Segmentation rates
            "over_segmentation_rate": self.over_segmentation_rate,
            "under_segmentation_rate": self.under_segmentation_rate,
            "fragmentation_rate": self.fragmentation_rate,
            # Per-class metrics
            "per_class_precision": self.per_class_precision,
            "per_class_recall": self.per_class_recall,
            "per_class_f1": self.per_class_f1,
            "per_class_iou": self.per_class_iou,
            "per_class_count": self.per_class_count,
            # Confusion matrix
            "confusion_matrix": self.confusion_matrix,
            # Unmatched indices
            "unmatched_predictions": self.unmatched_predictions,
            "unmatched_references": self.unmatched_references,
        }

    def to_summary_dict(self) -> dict[str, float]:
        """Convert to summary dictionary with key metrics only."""
        return {
            "boundary_precision": self.boundary_precision,
            "boundary_recall": self.boundary_recall,
            "boundary_f1": self.boundary_f1,
            "mean_iou": self.mean_iou,
            "label_accuracy": self.label_accuracy,
            "over_segmentation_rate": self.over_segmentation_rate,
            "under_segmentation_rate": self.under_segmentation_rate,
        }


def segmentation_metrics(
    predictions: SegmentList,
    references: SegmentList,
    tolerance_sec: float = 0.1,
) -> dict[str, float]:
    """
    Compute segmentation quality metrics.

    Args:
        predictions: Predicted segments
        references: Ground truth segments
        tolerance_sec: Boundary tolerance in seconds for "correct" boundaries

    Returns:
        Dictionary with metrics:
        - boundary_precision: Fraction of predicted boundaries near a reference
        - boundary_recall: Fraction of reference boundaries near a prediction
        - boundary_f1: F1 score of boundary detection
        - mean_iou: Mean intersection-over-union of matched segments

    Example:
        >>> preds = SegmentList([Segment(0, 1), Segment(1.5, 2.5)])
        >>> refs = SegmentList([Segment(0, 1.1), Segment(1.4, 2.6)])
        >>> scores = segmentation_metrics(preds, refs, tolerance_sec=0.2)
        >>> print(f"Boundary F1: {scores['boundary_f1']:.2f}")
    """
    pred_boundaries = _get_boundaries(predictions)
    ref_boundaries = _get_boundaries(references)

    # Boundary metrics with proper tracking to avoid double-counting
    b_precision, matched_refs = _boundary_precision_with_tracking(
        pred_boundaries, ref_boundaries, tolerance_sec
    )
    b_recall, matched_preds = _boundary_precision_with_tracking(
        ref_boundaries, pred_boundaries, tolerance_sec
    )
    b_f1 = _f1(b_precision, b_recall)

    # Segment IoU
    mean_iou = segment_iou(predictions, references)

    return {
        "boundary_precision": b_precision,
        "boundary_recall": b_recall,
        "boundary_f1": b_f1,
        "mean_iou": mean_iou,
    }


def boundary_f1(
    predictions: SegmentList,
    references: SegmentList,
    tolerance_sec: float = 0.1,
) -> float:
    """
    Compute boundary detection F1 score.

    Args:
        predictions: Predicted segments
        references: Ground truth segments
        tolerance_sec: Time tolerance for matching boundaries

    Returns:
        F1 score (0-1)
    """
    pred_boundaries = _get_boundaries(predictions)
    ref_boundaries = _get_boundaries(references)

    # Use tracking version to properly compute precision and recall
    precision, _ = _boundary_precision_with_tracking(pred_boundaries, ref_boundaries, tolerance_sec)
    recall, _ = _boundary_precision_with_tracking(ref_boundaries, pred_boundaries, tolerance_sec)

    return _f1(precision, recall)


def segment_iou(
    predictions: SegmentList,
    references: SegmentList,
    match_labels: bool = False,
) -> float:
    """
    Compute mean IoU between matched segments.

    Uses greedy matching to pair predicted and reference segments,
    then computes mean IoU over matched pairs.

    Args:
        predictions: Predicted segments
        references: Ground truth segments
        match_labels: If True, only match segments with same label

    Returns:
        Mean IoU score (0-1)
    """
    if len(predictions) == 0 or len(references) == 0:
        return 0.0

    # Greedy matching by IoU
    ious = []
    used_refs = set()

    for pred in predictions:
        best_iou = 0.0
        best_ref_idx = None

        for i, ref in enumerate(references):
            if i in used_refs:
                continue

            # Optionally check label match
            if match_labels and pred.label != ref.label:
                continue

            iou = pred.iou(ref)
            if iou > best_iou:
                best_iou = iou
                best_ref_idx = i

        if best_ref_idx is not None and best_iou > 0:
            ious.append(best_iou)
            used_refs.add(best_ref_idx)

    return float(np.mean(ious)) if ious else 0.0


def frame_accuracy(
    predictions: SegmentList,
    references: SegmentList,
    fps: float,
    total_frames: int,
) -> float:
    """
    Compute frame-level accuracy.

    Each frame is classified as "in segment" or "not in segment",
    and accuracy is the fraction of frames with matching classification.

    Args:
        predictions: Predicted segments
        references: Ground truth segments
        fps: Frames per second
        total_frames: Total number of frames

    Returns:
        Accuracy (0-1)
    """
    pred_mask = _segments_to_frame_mask(predictions, fps, total_frames)
    ref_mask = _segments_to_frame_mask(references, fps, total_frames)

    return float(np.mean(pred_mask == ref_mask))


def _get_boundaries(segments: SegmentList) -> np.ndarray:
    """Extract all boundary times from segments."""
    boundaries = set()
    for seg in segments:
        boundaries.add(seg.start)
        boundaries.add(seg.end)
    return np.array(sorted(boundaries))


def _boundary_precision(
    predicted: np.ndarray,
    reference: np.ndarray,
    tolerance: float,
) -> float:
    """Fraction of predicted boundaries within tolerance of a reference."""
    precision, _ = _boundary_precision_with_tracking(predicted, reference, tolerance)
    return precision


def _boundary_precision_with_tracking(
    predicted: np.ndarray,
    reference: np.ndarray,
    tolerance: float,
) -> tuple[float, set[int]]:
    """
    Compute precision with tracking of matched references.

    Each reference boundary can only be matched once to avoid double-counting.
    Uses greedy matching to pair predictions with nearest unmatched reference.

    Args:
        predicted: Array of predicted boundary times
        reference: Array of reference boundary times
        tolerance: Maximum time difference for a match

    Returns:
        Tuple of (precision score, set of matched reference indices)
    """
    if len(predicted) == 0:
        return 0.0, set()
    if len(reference) == 0:
        return 0.0, set()

    correct = 0
    matched_refs = set()

    for p in predicted:
        # Find distances to all reference boundaries
        distances = np.abs(reference - p)

        # Find the closest unmatched reference within tolerance
        best_ref_idx = None
        best_distance = float("inf")

        for i, dist in enumerate(distances):
            if i not in matched_refs and dist <= tolerance and dist < best_distance:
                best_ref_idx = i
                best_distance = dist

        if best_ref_idx is not None:
            correct += 1
            matched_refs.add(best_ref_idx)

    return correct / len(predicted), matched_refs


def _f1(precision: float, recall: float) -> float:
    """Compute F1 score from precision and recall."""
    if precision + recall == 0:
        return 0.0
    return 2 * precision * recall / (precision + recall)


def _segments_to_frame_mask(
    segments: SegmentList,
    fps: float,
    total_frames: int,
) -> np.ndarray:
    """Convert segments to binary frame mask."""
    mask = np.zeros(total_frames, dtype=bool)
    for seg in segments:
        start_frame = int(seg.start * fps)
        end_frame = int(seg.end * fps)
        mask[start_frame : min(end_frame, total_frames)] = True
    return mask


# =============================================================================
# Comprehensive Evaluation Functions
# =============================================================================


def comprehensive_segmentation_metrics(
    predictions: SegmentList,
    references: SegmentList,
    tolerance_sec: float = 0.1,
    match_labels: bool = True,
    iou_threshold: float = 0.3,
) -> SegmentationResult:
    """
    Compute comprehensive segmentation evaluation metrics.

    This function provides detailed analysis including boundary detection,
    label accuracy, segmentation rates, per-class breakdown, and confusion matrix.

    Args:
        predictions: Predicted segments
        references: Ground truth segments
        tolerance_sec: Boundary tolerance in seconds for "correct" boundaries
        match_labels: If True, require label match for segment matching
        iou_threshold: Minimum IoU for a valid segment match

    Returns:
        SegmentationResult with all computed metrics

    Example:
        >>> preds = SegmentList([Segment(0, 1, "A"), Segment(1.5, 2.5, "B")])
        >>> refs = SegmentList([Segment(0, 1.1, "A"), Segment(1.4, 2.6, "B")])
        >>> result = comprehensive_segmentation_metrics(preds, refs)
        >>> print(f"Boundary F1: {result.boundary_f1:.2f}")
        >>> print(f"Label Accuracy: {result.label_accuracy:.2f}")
    """
    result = SegmentationResult(
        num_predictions=len(predictions),
        num_references=len(references),
    )

    if len(predictions) == 0 and len(references) == 0:
        # Perfect score for empty segments
        result.boundary_precision = 1.0
        result.boundary_recall = 1.0
        result.boundary_f1 = 1.0
        result.boundary_f1_50ms = 1.0
        result.boundary_f1_100ms = 1.0
        result.boundary_f1_200ms = 1.0
        result.mean_iou = 1.0
        result.label_accuracy = 1.0
        return result

    # Compute boundary metrics at multiple tolerances
    pred_boundaries = _get_boundaries(predictions)
    ref_boundaries = _get_boundaries(references)

    # Default tolerance
    b_precision, _ = _boundary_precision_with_tracking(
        pred_boundaries, ref_boundaries, tolerance_sec
    )
    b_recall, _ = _boundary_precision_with_tracking(ref_boundaries, pred_boundaries, tolerance_sec)
    result.boundary_precision = b_precision
    result.boundary_recall = b_recall
    result.boundary_f1 = _f1(b_precision, b_recall)

    # Multiple tolerances
    result.boundary_f1_50ms = boundary_f1(predictions, references, tolerance_sec=0.05)
    result.boundary_f1_100ms = boundary_f1(predictions, references, tolerance_sec=0.1)
    result.boundary_f1_200ms = boundary_f1(predictions, references, tolerance_sec=0.2)

    # Match segments using greedy IoU matching
    matches, unmatched_preds, unmatched_refs = _match_segments(
        predictions, references, iou_threshold=iou_threshold, match_labels=match_labels
    )

    result.matches = matches
    result.unmatched_predictions = unmatched_preds
    result.unmatched_references = unmatched_refs
    result.num_matched = len(matches)

    # Compute IoU metrics
    if matches:
        ious = [m.iou for m in matches]
        result.mean_iou = float(np.mean(ious))
        result.per_segment_iou = ious
    else:
        result.mean_iou = 0.0
        result.per_segment_iou = []

    # Compute label accuracy
    _compute_label_metrics(result, predictions, references, matches)

    # Compute segmentation rates
    _compute_segmentation_rates(result, predictions, references, matches)

    # Compute per-class metrics
    _compute_per_class_metrics(result, predictions, references, matches, iou_threshold)

    # Build confusion matrix
    _build_confusion_matrix(result, predictions, references, matches)

    return result


def boundary_f1_multi_tolerance(
    predictions: SegmentList,
    references: SegmentList,
    tolerances_sec: list[float] | None = None,
) -> dict[str, float]:
    """
    Compute boundary F1 scores at multiple tolerance levels.

    Args:
        predictions: Predicted segments
        references: Ground truth segments
        tolerances_sec: List of tolerance values in seconds.
                       Defaults to [0.05, 0.1, 0.2] (50ms, 100ms, 200ms)

    Returns:
        Dictionary mapping tolerance names to F1 scores

    Example:
        >>> scores = boundary_f1_multi_tolerance(preds, refs)
        >>> print(f"F1 @ 100ms: {scores['f1_100ms']:.3f}")
    """
    if tolerances_sec is None:
        tolerances_sec = [0.05, 0.1, 0.2]

    result = {}
    for tol in tolerances_sec:
        # Convert to ms for key name
        ms = int(tol * 1000)
        key = f"f1_{ms}ms"
        result[key] = boundary_f1(predictions, references, tolerance_sec=tol)

    return result


def label_accuracy(
    predictions: SegmentList,
    references: SegmentList,
    iou_threshold: float = 0.3,
) -> float:
    """
    Compute label accuracy for matched segments.

    A prediction is considered correct if it overlaps sufficiently (IoU >= threshold)
    with a reference segment AND has the same label.

    Args:
        predictions: Predicted segments
        references: Ground truth segments
        iou_threshold: Minimum IoU for matching segments

    Returns:
        Fraction of matched segments with correct labels (0-1)
    """
    if len(predictions) == 0 or len(references) == 0:
        return 0.0

    matches, _, _ = _match_segments(
        predictions, references, iou_threshold=iou_threshold, match_labels=False
    )

    if not matches:
        return 0.0

    correct = sum(1 for m in matches if m.label_match)
    return correct / len(matches)


def over_segmentation_rate(
    predictions: SegmentList,
    references: SegmentList,
    iou_threshold: float = 0.1,
) -> float:
    """
    Compute over-segmentation rate.

    Over-segmentation occurs when a single reference segment is covered by
    multiple predicted segments. Rate is computed as the average number of
    predictions per reference minus 1.

    Args:
        predictions: Predicted segments
        references: Ground truth segments
        iou_threshold: Minimum IoU to consider a prediction overlaps a reference

    Returns:
        Over-segmentation rate (0 = perfect, higher = more over-segmentation)
    """
    if len(references) == 0:
        return 0.0

    # For each reference, count how many predictions overlap it
    overlaps_per_ref = []
    for ref in references:
        count = 0
        for pred in predictions:
            iou = pred.iou(ref)
            if iou >= iou_threshold:
                count += 1
        overlaps_per_ref.append(count)

    if not overlaps_per_ref:
        return 0.0

    # Average overlaps - 1 (perfect is 1 prediction per reference)
    avg_overlaps = float(np.mean(overlaps_per_ref))
    return max(0.0, avg_overlaps - 1.0)


def under_segmentation_rate(
    predictions: SegmentList,
    references: SegmentList,
    iou_threshold: float = 0.1,
) -> float:
    """
    Compute under-segmentation rate.

    Under-segmentation occurs when a single predicted segment covers multiple
    reference segments. Rate is computed as the average number of references
    per prediction minus 1.

    Args:
        predictions: Predicted segments
        references: Ground truth segments
        iou_threshold: Minimum IoU to consider a prediction overlaps a reference

    Returns:
        Under-segmentation rate (0 = perfect, higher = more under-segmentation)
    """
    if len(predictions) == 0:
        return 0.0

    # For each prediction, count how many references it overlaps
    overlaps_per_pred = []
    for pred in predictions:
        count = 0
        for ref in references:
            iou = pred.iou(ref)
            if iou >= iou_threshold:
                count += 1
        overlaps_per_pred.append(count)

    if not overlaps_per_pred:
        return 0.0

    # Average overlaps - 1 (perfect is 1 reference per prediction)
    avg_overlaps = float(np.mean(overlaps_per_pred))
    return max(0.0, avg_overlaps - 1.0)


def per_segment_iou(
    predictions: SegmentList,
    references: SegmentList,
    match_labels: bool = False,
) -> list[dict[str, Any]]:
    """
    Compute IoU for each matched segment pair with detailed information.

    Args:
        predictions: Predicted segments
        references: Ground truth segments
        match_labels: If True, only match segments with same label

    Returns:
        List of dictionaries with segment details and IoU values

    Example:
        >>> details = per_segment_iou(preds, refs)
        >>> for d in details:
        ...     print(f"Pred [{d['pred_start']:.1f}-{d['pred_end']:.1f}] "
        ...           f"vs Ref [{d['ref_start']:.1f}-{d['ref_end']:.1f}]: "
        ...           f"IoU={d['iou']:.3f}")
    """
    matches, unmatched_preds, unmatched_refs = _match_segments(
        predictions, references, iou_threshold=0.0, match_labels=match_labels
    )

    results = []

    # Matched segments
    for m in matches:
        results.append(
            {
                "pred_idx": m.pred_idx,
                "ref_idx": m.ref_idx,
                "pred_start": m.pred_segment.start,
                "pred_end": m.pred_segment.end,
                "pred_label": m.pred_segment.label,
                "ref_start": m.ref_segment.start,
                "ref_end": m.ref_segment.end,
                "ref_label": m.ref_segment.label,
                "iou": m.iou,
                "label_match": m.label_match,
                "boundary_error_start": m.boundary_error_start,
                "boundary_error_end": m.boundary_error_end,
                "matched": True,
            }
        )

    # Unmatched predictions (false positives)
    for idx in unmatched_preds:
        pred = predictions[idx]
        results.append(
            {
                "pred_idx": idx,
                "ref_idx": None,
                "pred_start": pred.start,
                "pred_end": pred.end,
                "pred_label": pred.label,
                "ref_start": None,
                "ref_end": None,
                "ref_label": None,
                "iou": 0.0,
                "label_match": False,
                "boundary_error_start": None,
                "boundary_error_end": None,
                "matched": False,
            }
        )

    # Unmatched references (false negatives)
    for idx in unmatched_refs:
        ref = references[idx]
        results.append(
            {
                "pred_idx": None,
                "ref_idx": idx,
                "pred_start": None,
                "pred_end": None,
                "pred_label": None,
                "ref_start": ref.start,
                "ref_end": ref.end,
                "ref_label": ref.label,
                "iou": 0.0,
                "label_match": False,
                "boundary_error_start": None,
                "boundary_error_end": None,
                "matched": False,
            }
        )

    return results


def per_class_metrics(
    predictions: SegmentList,
    references: SegmentList,
    iou_threshold: float = 0.3,
) -> dict[str, dict[str, float]]:
    """
    Compute precision, recall, F1, and IoU for each label class.

    Args:
        predictions: Predicted segments
        references: Ground truth segments
        iou_threshold: Minimum IoU for a valid segment match

    Returns:
        Dictionary mapping each label to its metrics

    Example:
        >>> metrics = per_class_metrics(preds, refs)
        >>> for label, m in metrics.items():
        ...     print(f"{label}: P={m['precision']:.2f} R={m['recall']:.2f} F1={m['f1']:.2f}")
    """
    # Get all unique labels
    all_labels = set()
    for seg in predictions:
        if seg.label is not None:
            all_labels.add(seg.label)
    for seg in references:
        if seg.label is not None:
            all_labels.add(seg.label)

    results = {}

    for label in sorted(all_labels):
        # Filter by label
        pred_label = predictions.filter_by_label(label)
        ref_label = references.filter_by_label(label)

        num_pred = len(pred_label)
        num_ref = len(ref_label)

        if num_pred == 0 and num_ref == 0:
            results[label] = {
                "precision": 1.0,
                "recall": 1.0,
                "f1": 1.0,
                "iou": 1.0,
                "count": 0,
            }
            continue

        # Match segments for this label
        matches, _, _ = _match_segments(
            pred_label, ref_label, iou_threshold=iou_threshold, match_labels=False
        )

        tp = len(matches)
        fp = num_pred - tp
        fn = num_ref - tp

        precision = tp / (tp + fp) if (tp + fp) > 0 else 0.0
        recall = tp / (tp + fn) if (tp + fn) > 0 else 0.0
        f1 = _f1(precision, recall)

        mean_iou = float(np.mean([m.iou for m in matches])) if matches else 0.0

        results[label] = {
            "precision": precision,
            "recall": recall,
            "f1": f1,
            "iou": mean_iou,
            "count": num_ref,
        }

    return results


def confusion_matrix(
    predictions: SegmentList,
    references: SegmentList,
    iou_threshold: float = 0.3,
) -> dict[str, dict[str, int]]:
    """
    Build a confusion matrix for segment labels.

    Matrix[pred_label][ref_label] = count of segments where a prediction
    with pred_label was matched to a reference with ref_label.

    Args:
        predictions: Predicted segments
        references: Ground truth segments
        iou_threshold: Minimum IoU for segment matching

    Returns:
        Nested dictionary representing confusion matrix

    Example:
        >>> cm = confusion_matrix(preds, refs)
        >>> print(f"Predicted A, Actual B: {cm.get('A', {}).get('B', 0)}")
    """
    matches, unmatched_preds, unmatched_refs = _match_segments(
        predictions, references, iou_threshold=iou_threshold, match_labels=False
    )

    # Initialize matrix
    matrix: dict[str, dict[str, int]] = {}

    # Add all labels
    all_labels = set()
    for seg in predictions:
        all_labels.add(seg.label if seg.label else "<NONE>")
    for seg in references:
        all_labels.add(seg.label if seg.label else "<NONE>")
    all_labels.add("<FP>")  # False positive (no match)
    all_labels.add("<FN>")  # False negative (no match)

    for label in all_labels:
        matrix[label] = {l: 0 for l in all_labels}

    # Fill matrix with matched segments
    for m in matches:
        pred_label = m.pred_segment.label if m.pred_segment.label else "<NONE>"
        ref_label = m.ref_segment.label if m.ref_segment.label else "<NONE>"
        matrix[pred_label][ref_label] += 1

    # Add false positives
    for idx in unmatched_preds:
        pred_seg = predictions[idx]
        pred_label = pred_seg.label if pred_seg.label else "<NONE>"
        matrix[pred_label]["<FN>"] += 1

    # Add false negatives
    for idx in unmatched_refs:
        ref_seg = references[idx]
        ref_label = ref_seg.label if ref_seg.label else "<NONE>"
        matrix["<FP>"][ref_label] += 1

    return matrix


def segmentation_summary(
    predictions: SegmentList,
    references: SegmentList,
    fps: float | None = None,
    total_duration: float | None = None,
) -> dict[str, Any]:
    """
    Compute summary statistics for segmentation results.

    Args:
        predictions: Predicted segments
        references: Ground truth segments
        fps: Frames per second (for frame-level metrics)
        total_duration: Total video duration in seconds

    Returns:
        Dictionary with overall accuracy, counts, and statistics
    """
    result = comprehensive_segmentation_metrics(predictions, references)

    summary: dict[str, Any] = {
        "overview": {
            "num_predictions": result.num_predictions,
            "num_references": result.num_references,
            "num_matched": result.num_matched,
            "match_rate": (
                result.num_matched / result.num_references if result.num_references > 0 else 0.0
            ),
        },
        "boundary_metrics": {
            "precision": result.boundary_precision,
            "recall": result.boundary_recall,
            "f1": result.boundary_f1,
            "f1_50ms": result.boundary_f1_50ms,
            "f1_100ms": result.boundary_f1_100ms,
            "f1_200ms": result.boundary_f1_200ms,
        },
        "iou_metrics": {
            "mean_iou": result.mean_iou,
            "min_iou": min(result.per_segment_iou) if result.per_segment_iou else 0.0,
            "max_iou": max(result.per_segment_iou) if result.per_segment_iou else 0.0,
            "std_iou": (
                float(np.std(result.per_segment_iou)) if len(result.per_segment_iou) > 1 else 0.0
            ),
        },
        "label_metrics": {
            "accuracy": result.label_accuracy,
            "precision": result.label_precision,
            "recall": result.label_recall,
            "f1": result.label_f1,
        },
        "segmentation_quality": {
            "over_segmentation_rate": result.over_segmentation_rate,
            "under_segmentation_rate": result.under_segmentation_rate,
            "fragmentation_rate": result.fragmentation_rate,
        },
        "per_class": result.per_class_f1,
    }

    # Add frame-level accuracy if fps provided
    if fps is not None and total_duration is not None:
        total_frames = int(total_duration * fps)
        summary["frame_accuracy"] = frame_accuracy(predictions, references, fps, total_frames)

    return summary


# =============================================================================
# Helper Functions
# =============================================================================


def _match_segments(
    predictions: SegmentList,
    references: SegmentList,
    iou_threshold: float = 0.3,
    match_labels: bool = False,
) -> tuple[list[SegmentMatch], list[int], list[int]]:
    """
    Match predicted segments to reference segments using greedy IoU matching.

    Args:
        predictions: Predicted segments
        references: Reference segments
        iou_threshold: Minimum IoU for a valid match
        match_labels: If True, only match segments with same label

    Returns:
        Tuple of (matches, unmatched_pred_indices, unmatched_ref_indices)
    """
    if len(predictions) == 0 or len(references) == 0:
        return (
            [],
            list(range(len(predictions))),
            list(range(len(references))),
        )

    matches = []
    used_refs = set()
    used_preds = set()

    # Compute all IoU values
    iou_matrix = np.zeros((len(predictions), len(references)))
    for i, pred in enumerate(predictions):
        for j, ref in enumerate(references):
            # Optionally check label match
            if match_labels and pred.label != ref.label:
                iou_matrix[i, j] = 0.0
            else:
                iou_matrix[i, j] = pred.iou(ref)

    # Greedy matching by highest IoU
    while True:
        # Find highest unmatched IoU
        max_iou = 0.0
        best_pred_idx = -1
        best_ref_idx = -1

        for i in range(len(predictions)):
            if i in used_preds:
                continue
            for j in range(len(references)):
                if j in used_refs:
                    continue
                if iou_matrix[i, j] > max_iou:
                    max_iou = iou_matrix[i, j]
                    best_pred_idx = i
                    best_ref_idx = j

        if max_iou < iou_threshold or best_pred_idx < 0:
            break

        # Create match
        pred_seg = predictions[best_pred_idx]
        ref_seg = references[best_ref_idx]
        label_match = pred_seg.label == ref_seg.label

        matches.append(
            SegmentMatch(
                pred_idx=best_pred_idx,
                ref_idx=best_ref_idx,
                pred_segment=pred_seg,
                ref_segment=ref_seg,
                iou=max_iou,
                label_match=label_match,
            )
        )

        used_preds.add(best_pred_idx)
        used_refs.add(best_ref_idx)

    unmatched_preds = [i for i in range(len(predictions)) if i not in used_preds]
    unmatched_refs = [i for i in range(len(references)) if i not in used_refs]

    return matches, unmatched_preds, unmatched_refs


def _compute_label_metrics(
    result: SegmentationResult,
    predictions: SegmentList,
    references: SegmentList,
    matches: list[SegmentMatch],
) -> None:
    """Compute label accuracy, precision, recall, and F1."""
    if not matches:
        return

    # Accuracy: fraction of matched segments with correct labels
    correct = sum(1 for m in matches if m.label_match)
    result.label_accuracy = correct / len(matches)

    # Get all unique labels from references
    Counter(seg.label for seg in references if seg.label is not None)
    Counter(seg.label for seg in predictions if seg.label is not None)

    # Micro-averaged precision/recall/F1
    tp = 0
    fp = 0
    fn = 0

    # Count correct label predictions
    for m in matches:
        if m.label_match and m.pred_segment.label is not None:
            tp += 1
        elif m.pred_segment.label is not None:
            fp += 1
            fn += 1

    # Add unmatched predictions as FP
    fp += len(result.unmatched_predictions)

    # Add unmatched references as FN
    fn += len(result.unmatched_references)

    result.label_precision = tp / (tp + fp) if (tp + fp) > 0 else 0.0
    result.label_recall = tp / (tp + fn) if (tp + fn) > 0 else 0.0
    result.label_f1 = _f1(result.label_precision, result.label_recall)


def _compute_segmentation_rates(
    result: SegmentationResult,
    predictions: SegmentList,
    references: SegmentList,
    matches: list[SegmentMatch],
) -> None:
    """Compute over-segmentation and under-segmentation rates."""
    result.over_segmentation_rate = over_segmentation_rate(predictions, references)
    result.under_segmentation_rate = under_segmentation_rate(predictions, references)

    # Fragmentation rate: average number of prediction fragments per reference
    if len(references) > 0:
        # Count how many predictions each reference overlaps with
        fragment_counts = []
        for ref in references:
            count = sum(1 for pred in predictions if pred.iou(ref) > 0)
            fragment_counts.append(count)
        result.fragmentation_rate = float(np.mean(fragment_counts)) if fragment_counts else 0.0


def _compute_per_class_metrics(
    result: SegmentationResult,
    predictions: SegmentList,
    references: SegmentList,
    matches: list[SegmentMatch],
    iou_threshold: float,
) -> None:
    """Compute per-class precision, recall, F1, and IoU."""
    metrics = per_class_metrics(predictions, references, iou_threshold)

    for label, m in metrics.items():
        result.per_class_precision[label] = m["precision"]
        result.per_class_recall[label] = m["recall"]
        result.per_class_f1[label] = m["f1"]
        result.per_class_iou[label] = m["iou"]
        result.per_class_count[label] = int(m["count"])


def _build_confusion_matrix(
    result: SegmentationResult,
    predictions: SegmentList,
    references: SegmentList,
    matches: list[SegmentMatch],
) -> None:
    """Build confusion matrix for label classification."""
    result.confusion_matrix = confusion_matrix(predictions, references)
