"""Programmatic API for NMS detection, extracted from cli.py logic."""

from __future__ import annotations

import logging
from pathlib import Path

from .constants import ALL_DETECTORS, THRESHOLD_DEFAULTS
from .detectors.blinks import detect_blinks
from .detectors.pipeline import detect_all_nms
from .io.exporters import export_csv, export_elan, export_json
from .io.loaders import load_smpl_eye_data, load_teaser_data, resolve_smpl_path
from .models import BlinkEvent, NMSEvent, TrackingQuality
from .processing.adaptive import compute_adaptive_thresholds
from .processing.validity import compute_validity_mask, segment_valid_regions

logger = logging.getLogger(__name__)


def detect_nms(
    h5_path: str,
    *,
    person: str = "0",
    detectors: set[str] | list[str] | None = None,
    no_adaptive: bool = False,
    no_gaze: bool = False,
    smpl_path: str | None = None,
    smpl_root: str | None = None,
    video_path: str | None = None,
    # Blink params
    prominence: float | None = None,
    sigma: float = 1.5,
    min_distance: int = 5,
    min_width: int = 1,
    max_width: int = 18,
    min_score: float = 0.50,
    min_segment: int = 2,
    yaw_limit: float = 0.8,
    # NMS thresholds
    thresholds: dict[str, float] | None = None,
) -> tuple[list[BlinkEvent], list[NMSEvent], TrackingQuality]:
    """Run NMS detection on a TEASER H5 file.

    This is the main programmatic entry point, extracting the core detection
    pipeline from the CLI without argparse overhead.

    Args:
        h5_path: Path to TEASER HDF5 file.
        person: Person group in HDF5 file (default "0").
        detectors: Set of detector names to run, or None for blink-only.
            Use {"all"} or ALL_DETECTORS for everything.
            Valid: blink, nod, shake, tilt, mouth, eyebrow, gaze, squint, lips.
        no_adaptive: Disable adaptive thresholds.
        no_gaze: Skip eye gaze detection.
        smpl_path: Explicit path to SMPL NLF file for gaze.
        smpl_root: Root directory to search for SMPL files.
        video_path: Path to source video (for ELAN export).
        prominence: Blink detection prominence threshold.
        sigma: Gaussian smoothing sigma.
        min_distance: Min frames between blinks.
        min_width: Min blink width in frames.
        max_width: Max blink width in frames.
        min_score: Min detection confidence for frame validity.
        min_segment: Min valid segment length in frames.
        yaw_limit: Max head yaw in radians for valid frames.
        thresholds: Dict of threshold overrides (keys from THRESHOLD_DEFAULTS).

    Returns:
        Tuple of (blinks, nms_events, quality).
    """
    if not Path(h5_path).exists():
        raise FileNotFoundError(f"HDF5 file not found: {h5_path}")

    # Parse detectors
    if detectors is not None:
        det_set = set(detectors)
        if "all" in det_set:
            det_set = ALL_DETECTORS.copy()
            run_blinks = True
        else:
            run_blinks = "blink" in det_set
            det_set.discard("blink")
            det_set &= ALL_DETECTORS
        nms_detectors = det_set if det_set else None
        run_nms = nms_detectors is not None
    else:
        run_blinks = True
        nms_detectors = None
        run_nms = False

    # Load data
    eyelid, successful_frames, boxes, pose, scores, expression, jaw_pose, quality = (
        load_teaser_data(
            h5_path,
            person,
            load_expression=True,
            load_nms=run_nms,
        )
    )

    skip_blinks = False
    if quality.active_eye == -1:
        if not run_nms:
            return [], [], quality
        skip_blinks = True

    # Load SMPL eye gaze data
    leye_pose, reye_pose = None, None
    if run_nms and not no_gaze:
        resolved_smpl = None
        if smpl_path:
            p = Path(smpl_path)
            if p.exists():
                resolved_smpl = p
        if resolved_smpl is None:
            root = Path(smpl_root) if smpl_root else None
            resolved_smpl = resolve_smpl_path(h5_path, root) if root else resolve_smpl_path(h5_path)
        if resolved_smpl is not None:
            leye_pose, reye_pose = load_smpl_eye_data(
                resolved_smpl,
                successful_frames,
                boxes,
            )

    # Validity masks
    valid = compute_validity_mask(
        eyelid,
        scores,
        pose,
        min_score=min_score,
        yaw_limit=yaw_limit,
        require_eyelid=True,
    )

    if run_nms:
        nms_valid = compute_validity_mask(
            eyelid,
            scores,
            pose,
            min_score=max(0.30, min_score - 0.20),
            yaw_limit=yaw_limit,
            require_eyelid=False,
        )
    else:
        nms_valid = valid

    # Segment valid regions
    segments = segment_valid_regions(valid, min_segment=min_segment)
    nms_segments = (
        segment_valid_regions(nms_valid, min_segment=min_segment) if run_nms else segments
    )

    if not segments and not (run_nms and nms_segments):
        return [], [], quality

    # Adaptive thresholds
    user_thresholds = thresholds or {}

    if no_adaptive:
        adaptive = {}
    else:
        adaptive = compute_adaptive_thresholds(
            pose,
            jaw_pose,
            expression,
            nms_valid if run_nms else valid,
            eyelid=eyelid,
        )

    def _pick(user_val, key):
        """Use user override > adaptive > default."""
        if key in user_thresholds:
            return user_thresholds[key]
        if user_val is not None:
            return user_val
        return adaptive.get(key, THRESHOLD_DEFAULTS.get(key, 0.0))

    blink_prom = _pick(prominence, "prominence")

    # Detect blinks
    blinks = []
    if run_blinks and not skip_blinks and segments:
        blinks = detect_blinks(
            eyelid,
            successful_frames,
            valid,
            segments,
            sigma=sigma,
            prominence=blink_prom,
            min_distance=min_distance,
            min_width=min_width,
            max_width=max_width,
            expression=expression,
        )

    # Detect NMS events
    nms_events = []
    if run_nms and nms_detectors:
        nms_events = detect_all_nms(
            pose,
            jaw_pose,
            expression,
            successful_frames,
            nms_valid,
            nms_segments,
            detectors=nms_detectors,
            nod_amplitude=_pick(None, "nod_amplitude"),
            shake_amplitude=_pick(None, "shake_amplitude"),
            tilt_threshold=_pick(None, "tilt_threshold"),
            mouth_threshold=_pick(None, "mouth_threshold"),
            eyebrow_threshold=_pick(None, "eyebrow_threshold"),
            leye_pose=leye_pose,
            reye_pose=reye_pose,
            gaze_threshold=user_thresholds.get("gaze_threshold", 1.5),
            eyelid=eyelid,
            blink_events=blinks,
            squint_threshold=_pick(None, "squint_threshold"),
            lip_threshold=_pick(None, "lip_threshold"),
        )

    return blinks, nms_events, quality


def export_results(
    blinks: list[BlinkEvent],
    nms_events: list[NMSEvent],
    h5_path: str,
    *,
    output_dir: str | None = None,
    formats: list[str] | None = None,
    participant_id: str | None = None,
    media_path: str = "",
) -> dict[str, str]:
    """Export detection results to the requested formats.

    Args:
        blinks: Detected blink events.
        nms_events: Detected NMS events.
        h5_path: Original H5 file path (for naming).
        output_dir: Output directory (default: same as h5_path).
        formats: List of formats to export: "elan", "csv", "json".
        participant_id: Participant ID for ELAN tier names.
        media_path: Media file path for ELAN export.

    Returns:
        Dict of format -> output path.
    """
    if not formats:
        return {}

    stem = Path(h5_path).stem
    base_dir = Path(output_dir) if output_dir else Path(h5_path).parent
    base_dir.mkdir(parents=True, exist_ok=True)

    export_paths = {}

    for fmt in formats:
        fmt = fmt.lower().strip()
        if fmt == "elan":
            path = str(base_dir / f"{stem}_nms.eaf")
            export_elan(
                blinks,
                path,
                media_path=media_path,
                nms_events=nms_events if nms_events else None,
                participant_id=participant_id,
                h5_path=h5_path,
            )
            export_paths["elan"] = path
        elif fmt == "csv":
            path = str(base_dir / f"{stem}_nms.csv")
            export_csv(blinks, path, nms_events=nms_events if nms_events else None)
            export_paths["csv"] = path
        elif fmt == "json":
            path = str(base_dir / f"{stem}_nms.json")
            export_json(blinks, path, nms_events=nms_events if nms_events else None)
            export_paths["json"] = path

    return export_paths
