"""Pareto dominance utilities for multi-objective optimization."""
from __future__ import annotations

from typing import Any


def dominates(a: dict[str, float], b: dict[str, float]) -> bool:
    """
    Check if candidate A Pareto-dominates candidate B.

    A dominates B if:
    - A is >= B in ALL dimensions (among shared dimensions)
    - A is strictly > B in at least one dimension

    Args:
        a: Dimension metrics dict for candidate A
        b: Dimension metrics dict for candidate B

    Returns:
        True if A dominates B, False otherwise
    """
    shared_dims = set(a.keys()) & set(b.keys())
    if not shared_dims:
        return False

    all_geq = True
    any_strictly_greater = False

    for dim in shared_dims:
        val_a = a.get(dim, 0.0)
        val_b = b.get(dim, 0.0)

        if val_a < val_b:
            all_geq = False
            break
        if val_a > val_b:
            any_strictly_greater = True

    return all_geq and any_strictly_greater


def compute_pareto_front(candidates: dict[str, dict[str, Any]]) -> list[str]:
    """
    Compute the Pareto front from a set of candidates.

    A candidate is on the Pareto front if no other candidate dominates it.

    Args:
        candidates: Dict mapping candidate_id to candidate data.
                   Each candidate must have 'dimension_metrics' dict.

    Returns:
        List of candidate IDs that are on the Pareto front.
    """
    if not candidates:
        return []

    # Extract dimension metrics for each candidate
    metrics: dict[str, dict[str, float]] = {}
    for cid, data in candidates.items():
        dim_metrics = data.get("dimension_metrics")
        if isinstance(dim_metrics, dict):
            metrics[cid] = dim_metrics

    if not metrics:
        return list(candidates.keys())

    front: list[str] = []

    for cid in metrics:
        is_dominated = False
        for other_cid in metrics:
            if cid == other_cid:
                continue
            if dominates(metrics[other_cid], metrics[cid]):
                is_dominated = True
                break
        if not is_dominated:
            front.append(cid)

    return front


def select_pareto_candidates(
    candidates: dict[str, dict[str, Any]],
    max_selected: int = 2,
    analysis_commit: str | None = None,
    analysis_branch: str | None = None,
) -> dict[str, Any]:
    """
    Select candidates using Pareto-based multi-objective selection.

    Strategy:
    1. Compute Pareto front (all non-dominated candidates)
    2. Select primary = highest overall_score on the front
    3. Select additional candidates from front for diversity (up to max_selected)

    Args:
        candidates: Dict mapping candidate_id to candidate data.
                   Each candidate must have 'dimension_metrics' and 'overall_score'.
                   Each candidate should also have 'commit_id' for crossover.
        max_selected: Maximum number of candidates to select.
        analysis_commit: The analysis commit ID (baseline for crossover).
        analysis_branch: The analysis branch name (for git_new_branch).

    Returns:
        Dict with:
        - 'primary': ID of the primary selected candidate (highest avg on front)
        - 'pareto_front': List of all candidate IDs on the Pareto front
        - 'selected': List of selected candidate IDs (up to max_selected)
        - 'dominated': List of dominated candidate IDs
        - 'crossover_info': Dict with parent commit IDs and analysis info for crossover
          (only present if at least 2 candidates are selected)
    """
    if not candidates:
        return {
            "primary": None,
            "pareto_front": [],
            "selected": [],
            "dominated": [],
        }

    # Compute Pareto front
    front = compute_pareto_front(candidates)

    if not front:
        # Fallback: all candidates are incomparable, use all
        front = list(candidates.keys())

    # Find dominated candidates
    dominated = [cid for cid in candidates if cid not in front]

    # Sort front by overall_score descending to pick primary
    def get_score(cid: str) -> float:
        return candidates[cid].get("overall_score") or 0.0

    sorted_front = sorted(front, key=get_score, reverse=True)

    primary = sorted_front[0] if sorted_front else None

    # Select up to max_selected from front
    selected = sorted_front[:max_selected]

    result: dict[str, Any] = {
        "primary": primary,
        "pareto_front": front,
        "selected": selected,
        "dominated": dominated,
    }

    # Add crossover info if we have at least 2 selected candidates
    if len(selected) >= 2:
        parent_commits = []
        for cid in selected:
            commit_id = candidates.get(cid, {}).get("commit_id")
            if commit_id:
                parent_commits.append(commit_id)
        if len(parent_commits) >= 2:
            result["crossover_info"] = {
                "parent_commits": parent_commits,
                "analysis_commit": analysis_commit,
                "analysis_branch": analysis_branch,
            }

    return result
