"""Non-Manual Annotator router — face bbox, unannotated queue, label vocab, save."""

from __future__ import annotations

import asyncio
import logging
from pathlib import Path
from typing import Any

from fastapi import APIRouter, HTTPException, Query

from sltk.api.dependencies import safe_error_detail, validate_path
from sltk.api.models import (
    FaceBboxResponse,
    NMAnnotatorSaveRequest,
    NMAnnotatorSaveResponse,
    NMLabelVocabEntry,
    NMLabelVocabResponse,
)

logger = logging.getLogger(__name__)

router = APIRouter(prefix="/api/nm-annotator", tags=["nm-annotator"])


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _get_corpus_db(workspace: str | None = None):
    """Return the CorpusDB for the given or active workspace."""
    from sltk.api.routers.corpus import _get_corpus_db as get_db

    return get_db(workspace)


# ---------------------------------------------------------------------------
# Endpoints
# ---------------------------------------------------------------------------


@router.get("/unannotated")
async def unannotated_segments(
    gloss: str | None = Query(None, description="Filter by gloss substring"),
    nm_tier_ids: str | None = Query(None, description="JSON list of NM tier IDs"),
    limit: int = Query(200, ge=1, le=2000),
    workspace: str | None = Query(None),
) -> list[dict[str, Any]]:
    """Queue of gloss segments lacking NM annotations."""
    import json

    db = _get_corpus_db(workspace)
    parsed_tiers: list[str] | None = None
    if nm_tier_ids:
        try:
            parsed_tiers = json.loads(nm_tier_ids)
            if not isinstance(parsed_tiers, list) or not all(
                isinstance(t, str) for t in parsed_tiers
            ):
                raise HTTPException(400, "nm_tier_ids must be a JSON array of strings")
        except json.JSONDecodeError:
            raise HTTPException(400, "nm_tier_ids must be a JSON array of strings")

    return db.unannotated_segments(
        nm_tier_ids=parsed_tiers,
        gloss_filter=gloss,
        limit=limit,
    )


@router.get("/labels", response_model=NMLabelVocabResponse)
async def nm_label_vocabulary(
    nm_tier_ids: str | None = Query(None, description="JSON list of NM tier IDs"),
    workspace: str | None = Query(None),
) -> NMLabelVocabResponse:
    """NM label vocabulary from corpus, grouped by tier."""
    import json

    db = _get_corpus_db(workspace)
    parsed_tiers: list[str] | None = None
    if nm_tier_ids:
        try:
            parsed_tiers = json.loads(nm_tier_ids)
            if not isinstance(parsed_tiers, list) or not all(
                isinstance(t, str) for t in parsed_tiers
            ):
                raise HTTPException(400, "nm_tier_ids must be a JSON array of strings")
        except json.JSONDecodeError:
            raise HTTPException(400, "nm_tier_ids must be a JSON array of strings")

    raw = db.nm_label_vocabulary(nm_tier_ids=parsed_tiers)
    tiers = {
        tier: [NMLabelVocabEntry(label=e["label"], count=e["count"]) for e in entries]
        for tier, entries in raw.items()
    }
    return NMLabelVocabResponse(tiers=tiers)


@router.get("/face-bbox", response_model=FaceBboxResponse)
async def face_bbox(
    video_path: str = Query(..., description="Absolute path to video file"),
    time_sec: float = Query(..., ge=0, description="Timestamp in seconds"),
) -> FaceBboxResponse:
    """Detect face bounding box at a given timestamp using MediaPipe."""
    path = Path(video_path)
    if not path.exists():
        raise HTTPException(404, f"Video not found: {video_path}")
    if not path.is_file():
        raise HTTPException(400, "Path is not a file")

    def _detect() -> FaceBboxResponse:
        try:
            import cv2
        except ImportError:
            raise HTTPException(501, "opencv-python (cv2) is required for face detection")

        cap = cv2.VideoCapture(str(path))
        if not cap.isOpened():
            raise HTTPException(
                400,
                "Cannot open video — file may be corrupt or codec missing",
            )

        try:
            # Check timestamp against video duration
            fps = cap.get(cv2.CAP_PROP_FPS)
            frame_count = cap.get(cv2.CAP_PROP_FRAME_COUNT)
            if fps > 0 and frame_count > 0:
                duration = frame_count / fps
                if time_sec > duration:
                    raise HTTPException(
                        400,
                        f"Timestamp {time_sec}s exceeds video duration" f" {duration:.1f}s",
                    )

            cap.set(cv2.CAP_PROP_POS_MSEC, time_sec * 1000)
            ret, frame = cap.read()
            if not ret or frame is None:
                raise HTTPException(400, f"Could not read frame at {time_sec}s")
        finally:
            cap.release()

        h, w = frame.shape[:2]

        # Try MediaPipe FaceDetection first
        try:
            import mediapipe as mp

            mp_face = mp.solutions.face_detection
            with mp_face.FaceDetection(model_selection=1, min_detection_confidence=0.5) as detector:
                rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
                results = detector.process(rgb)
                if results.detections:
                    det = results.detections[0]
                    bbox = det.location_data.relative_bounding_box
                    pad = 0.2
                    bx = max(0.0, bbox.xmin - bbox.width * pad)
                    by = max(0.0, bbox.ymin - bbox.height * pad)
                    bw = min(1.0 - bx, bbox.width * (1 + 2 * pad))
                    bh = min(1.0 - by, bbox.height * (1 + 2 * pad))
                    return FaceBboxResponse(
                        x=round(bx, 4),
                        y=round(by, 4),
                        width=round(bw, 4),
                        height=round(bh, 4),
                        confidence=round(det.score[0], 3),
                    )
        except ImportError:
            logger.info("mediapipe not available, using Haar cascade fallback")

        # Fallback: Haar cascade
        cascade_path = cv2.data.haarcascades + "haarcascade_frontalface_default.xml"  # type: ignore[attr-defined]
        cascade = cv2.CascadeClassifier(cascade_path)
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        faces = cascade.detectMultiScale(gray, 1.1, 4, minSize=(30, 30))
        if len(faces) > 0:
            fx, fy, fw, fh = faces[0]
            pad = 0.15
            bx = max(0.0, (fx - fw * pad) / w)
            by = max(0.0, (fy - fh * pad) / h)
            bw = min(1.0 - bx, fw * (1 + 2 * pad) / w)
            bh = min(1.0 - by, fh * (1 + 2 * pad) / h)
            return FaceBboxResponse(
                x=round(bx, 4),
                y=round(by, 4),
                width=round(bw, 4),
                height=round(bh, 4),
                confidence=0.5,
            )

        # No face detected — return centered default
        return FaceBboxResponse(x=0.2, y=0.1, width=0.6, height=0.6, confidence=0.0)

    return await asyncio.to_thread(_detect)


@router.post("/save", response_model=NMAnnotatorSaveResponse)
async def save_nm_annotation(
    request: NMAnnotatorSaveRequest,
    workspace: str | None = Query(None),
) -> NMAnnotatorSaveResponse:
    """Save NM annotations via session API and re-ingest into corpus DB."""
    from sltk.api.routers.segments import _get_session

    session = _get_session(request.session_id)
    if not session:
        raise HTTPException(404, f"Session not found: {request.session_id}")

    segment_count = len(session.segments)

    if request.save_mode == "save_as":
        if not request.save_as_path:
            raise HTTPException(400, "save_as_path is required for save_as mode")
        save_path = validate_path(request.save_as_path, must_exist=False, allow_creation=True)
    else:
        if not session.source_path:
            raise HTTPException(
                400,
                "Session has no source_path — use save_as mode instead",
            )
        save_path = Path(session.source_path)

    # Open ElanDocument, apply session state, and save
    try:
        from sltk.api.routers.segments import _apply_session_to_doc
        from sltk.io.elan_roundtrip import ElanDocument

        if session.source_path:
            doc = await asyncio.to_thread(ElanDocument.open, session.source_path)
        else:
            doc = ElanDocument.new(video_path=session.video_path)

        _apply_session_to_doc(doc, session)
        await asyncio.to_thread(doc.save, str(save_path))
    except HTTPException:
        raise
    except Exception as exc:
        logger.exception("NM save failed for session %s", request.session_id)
        raise HTTPException(500, safe_error_detail("Failed to save NM annotations", exc))

    # Re-ingest the saved file into corpus DB
    re_ingested = False
    try:
        db = _get_corpus_db(workspace)
        re_ingested = db.ingest_eaf(str(save_path))
    except Exception:
        logger.warning(
            "Re-ingest after NM save failed for %s",
            save_path,
            exc_info=True,
        )

    return NMAnnotatorSaveResponse(
        status="saved",
        saved_path=str(save_path),
        segments_saved=segment_count,
        re_ingested=re_ingested,
    )
