"""Video overlay renderer: precomputes frame states and creates annotated video."""

import shutil
import subprocess
from pathlib import Path

import cv2
import numpy as np

from ..constants import FPS
from ..models import BlinkEvent, FrameState, NMSEvent
from .drawing import SIGNAL_CHANNELS, TIER_SHORT, blend_overlay, pill, rounded_rect


def precompute_frame_states(total_video_frames, blinks, eyelid, successful_frames, valid, scores):
    """Build a FrameState for every video frame for efficient overlay rendering."""
    frame_to_idx = {}
    for i, vf in enumerate(successful_frames):
        frame_to_idx[int(vf)] = i

    blink_frames = {}
    for b in blinks:
        for f in range(b.start_frame, b.end_frame + 1):
            if f not in blink_frames or b.confidence > blink_frames[f][1]:
                blink_frames[f] = (True, b.confidence)

    states = []
    cumulative_blinks = 0
    cumulative_valid = 0

    for vf in range(total_video_frames):
        data_idx = frame_to_idx.get(vf)

        is_valid = False
        eyelid_val = float("nan")
        det_score = float("nan")

        if data_idx is not None:
            is_valid = bool(valid[data_idx])
            eyelid_val = float(eyelid[data_idx])
            if scores is not None and data_idx < len(scores):
                det_score = float(scores[data_idx])

        if is_valid:
            cumulative_valid += 1

        is_blink = vf in blink_frames
        blink_conf = blink_frames[vf][1] if is_blink else 0.0

        while cumulative_blinks < len(blinks) and blinks[cumulative_blinks].peak_frame <= vf:
            cumulative_blinks += 1

        elapsed = vf + 1
        valid_time = cumulative_valid / FPS
        blink_rate = cumulative_blinks / (valid_time / 60.0) if valid_time > 10 else 0.0
        coverage = cumulative_valid / elapsed if elapsed > 0 else 0.0

        states.append(
            FrameState(
                is_valid=is_valid,
                is_blink=is_blink,
                blink_confidence=blink_conf,
                eyelid_value=eyelid_val,
                det_score=det_score,
                blink_count=cumulative_blinks,
                blink_rate=blink_rate,
                coverage_pct=coverage * 100,
            )
        )

    return states


def create_annotated_video(
    video_path: Path,
    output_path: str,
    blinks: list[BlinkEvent],
    eyelid: np.ndarray,
    successful_frames: np.ndarray,
    valid: np.ndarray,
    boxes: np.ndarray | None = None,
    scores: np.ndarray | None = None,
    pose: np.ndarray | None = None,
    jaw_pose: np.ndarray | None = None,
    expression: np.ndarray | None = None,
    nms_events: list[NMSEvent] | None = None,
    nms_valid: np.ndarray | None = None,
    verbose: bool = False,
    max_frames: int = 0,
):
    """Create output video with a modern signal dashboard overlay."""
    cap = cv2.VideoCapture(str(video_path))
    if not cap.isOpened():
        raise RuntimeError(f"Cannot open video: {video_path}")

    w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
    h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
    fps = cap.get(cv2.CAP_PROP_FPS) or FPS
    total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
    if max_frames > 0:
        total_frames = min(total_frames, max_frames)

    has_nms = nms_events is not None and len(nms_events) > 0
    nms_events = nms_events or []

    states = precompute_frame_states(total_frames, blinks, eyelid, successful_frames, valid, scores)

    frame_to_idx = {}
    for i, vf in enumerate(successful_frames):
        frame_to_idx[int(vf)] = i

    nms_active: dict[int, dict[str, str]] = {}
    if has_nms:
        for evt in nms_events:
            for f in range(evt.start_frame, evt.end_frame + 1):
                nms_active.setdefault(f, {})[evt.tier] = evt.label
    for b in blinks:
        for f in range(b.start_frame, b.end_frame + 1):
            nms_active.setdefault(f, {})["BLINK"] = "blink"

    nms_counts: dict[str, int] = {}
    for b in blinks:
        nms_counts["BLINK"] = nms_counts.get("BLINK", 0) + 1
    if has_nms:
        for evt in nms_events:
            nms_counts[evt.tier] = nms_counts.get(evt.tier, 0) + 1

    active_tiers = {"BLINK"}
    if has_nms:
        for evt in nms_events:
            active_tiers.add(evt.tier)
    channels = [ch for ch in SIGNAL_CHANNELS if ch[0] in active_tiers]

    FONT = cv2.FONT_HERSHEY_SIMPLEX
    PAD = 12
    ROW_H = 26
    CORNER_R = 8
    BG = (26, 26, 32)
    BG_ROW = (34, 34, 42)
    BG_HEADER = (38, 38, 48)
    BORDER = (58, 58, 68)
    SEP = (48, 48, 58)
    TXT_PRIMARY = (235, 235, 240)
    TXT_SECONDARY = (160, 160, 168)
    TXT_DIM = (65, 65, 72)
    ACCENT = (200, 160, 60)
    PROGRESS_COLOR = (200, 160, 60)

    panel_w = 194
    header_h = 32
    body_start = header_h
    row_section_h = len(channels) * ROW_H
    stats_h = 70
    panel_h = header_h + row_section_h + stats_h + PAD
    panel_x = w - panel_w - 10
    panel_y = 10

    out_path = output_path
    if not out_path.endswith((".mp4", ".avi")):
        out_path = output_path + ".mp4"
    if out_path.endswith(".mp4"):
        for codec in ["avc1", "H264", "X264", "mp4v"]:
            fourcc = cv2.VideoWriter_fourcc(*codec)  # type: ignore[attr-defined]
            out = cv2.VideoWriter(out_path, fourcc, fps, (w, h))
            if out.isOpened():
                break
            out.release()
    else:
        fourcc = cv2.VideoWriter_fourcc(*"XVID")  # type: ignore[attr-defined]
        out = cv2.VideoWriter(out_path, fourcc, fps, (w, h))
    if not out.isOpened():
        out_path = Path(output_path).with_suffix(".avi").as_posix()
        fourcc = cv2.VideoWriter_fourcc(*"XVID")  # type: ignore[attr-defined]
        out = cv2.VideoWriter(out_path, fourcc, fps, (w, h))
    if not out.isOpened():
        cap.release()
        raise RuntimeError(f"Cannot create output video: {out_path}")

    panel = np.zeros((panel_h, panel_w, 3), dtype=np.uint8)
    badge_h_max, badge_w_max = 30, 160
    badge_buf = np.zeros((badge_h_max, badge_w_max, 3), dtype=np.uint8)
    face_label_buf = np.zeros((22, 120, 3), dtype=np.uint8)

    static_panel = np.full((panel_h, panel_w, 3), BG, dtype=np.uint8)

    rounded_rect(static_panel, (0, 0), (panel_w - 1, header_h - 1), BG_HEADER, CORNER_R)
    cv2.rectangle(
        static_panel, (0, header_h - CORNER_R), (panel_w - 1, header_h - 1), BG_HEADER, -1
    )
    cv2.putText(static_panel, "NMS Detector", (PAD, 22), FONT, 0.42, ACCENT, 1, cv2.LINE_AA)

    for ch_i in range(len(channels)):
        ry = body_start + ch_i * ROW_H
        if ch_i % 2 == 1:
            cv2.rectangle(static_panel, (0, ry), (panel_w - 1, ry + ROW_H - 1), BG_ROW, -1)

    sep_y = body_start + row_section_h + 4
    cv2.line(static_panel, (PAD, sep_y), (panel_w - PAD, sep_y), SEP, 1, cv2.LINE_AA)

    s1_y = sep_y + 16
    s2_y = s1_y + 16
    s3_y = s2_y + 16
    cv2.putText(static_panel, "Blinks", (PAD, s1_y), FONT, 0.33, TXT_SECONDARY, 1, cv2.LINE_AA)
    cv2.putText(static_panel, "Coverage", (PAD, s2_y), FONT, 0.33, TXT_SECONDARY, 1, cv2.LINE_AA)
    cv2.putText(static_panel, "Signals", (PAD, s3_y), FONT, 0.33, TXT_SECONDARY, 1, cv2.LINE_AA)

    rounded_rect(static_panel, (0, 0), (panel_w - 1, panel_h - 1), BORDER, CORNER_R, 1)

    tier_color_map = {tid: col for tid, _, col in SIGNAL_CHANNELS}

    frame_idx = 0
    while True:
        ret, frame = cap.read()
        if not ret or frame_idx >= len(states):
            break

        st = states[frame_idx]
        data_idx = frame_to_idx.get(frame_idx)
        active_now = nms_active.get(frame_idx, {})
        time_s = frame_idx / fps

        if data_idx is not None and boxes is not None and st.is_valid:
            bx1, by1, bx2, by2 = boxes[data_idx].astype(int)
            if active_now:
                first_tier = next(iter(active_now))
                box_col = tier_color_map.get(first_tier, (100, 160, 80))
                cv2.rectangle(frame, (bx1, by1), (bx2, by2), box_col, 2, cv2.LINE_AA)
            else:
                cv2.rectangle(frame, (bx1, by1), (bx2, by2), (90, 140, 70), 1, cv2.LINE_AA)

            if active_now:
                label_y = by1 - 8
                for tier_id, evt_label in active_now.items():
                    if tier_id == "BLINK":
                        disp = "BLINK"
                    else:
                        disp = evt_label.upper() if len(evt_label) <= 14 else evt_label[:14].upper()
                    col = tier_color_map.get(tier_id, TXT_PRIMARY)
                    (tw_l, th_l), _ = cv2.getTextSize(disp, FONT, 0.38, 1)
                    lbl_x = max(bx1, 4)
                    lbl_y = max(label_y, th_l + 8)
                    lbl_w = tw_l + 16
                    lbl_h = th_l + 10
                    face_label_buf[:lbl_h, :lbl_w] = (20, 20, 28)
                    cv2.circle(face_label_buf, (7, lbl_h // 2), 4, col, -1, cv2.LINE_AA)
                    cv2.putText(
                        face_label_buf,
                        disp,
                        (15, lbl_h - 5),
                        FONT,
                        0.38,
                        (220, 220, 225),
                        1,
                        cv2.LINE_AA,
                    )
                    blend_overlay(
                        frame, face_label_buf[:lbl_h, :lbl_w], lbl_x, lbl_y - lbl_h, alpha=0.82
                    )
                    label_y -= lbl_h + 3

        if st.is_blink:
            a = min(255, int(140 + 115 * st.blink_confidence))
            cv2.rectangle(frame, (1, 1), (w - 2, h - 2), (50, 50, a), 3, cv2.LINE_AA)
            cv2.rectangle(frame, (4, 4), (w - 5, h - 5), (30, 30, a // 2), 1, cv2.LINE_AA)

        np.copyto(panel, static_panel)

        t_min = int(time_s // 60)
        t_sec = time_s % 60
        t_str = f"{t_min}:{t_sec:04.1f}"
        (tw, _), _ = cv2.getTextSize(t_str, FONT, 0.34, 1)
        cv2.putText(
            panel, t_str, (panel_w - tw - PAD, 22), FONT, 0.34, TXT_SECONDARY, 1, cv2.LINE_AA
        )

        for ch_i, (tier_id, label, color_on) in enumerate(channels):
            ry = body_start + ch_i * ROW_H
            is_active = tier_id in active_now

            pill_w_px = 10
            pill_h_px = 10
            pill_x = PAD
            pill_y = ry + (ROW_H - pill_h_px) // 2
            if is_active:
                pill(panel, pill_x, pill_y, pill_w_px, pill_h_px, color_on)
                pill(panel, pill_x - 1, pill_y - 1, pill_w_px + 2, pill_h_px + 2, color_on, 1)
            else:
                pill(panel, pill_x, pill_y, pill_w_px, pill_h_px, (45, 45, 52))

            lx = pill_x + pill_w_px + 8
            ly = ry + ROW_H // 2 + 5
            if is_active:
                evt_lbl = active_now.get(tier_id, label)
                disp_lbl = evt_lbl if len(evt_lbl) <= 16 else evt_lbl[:16]
                cv2.putText(panel, disp_lbl, (lx, ly), FONT, 0.36, TXT_PRIMARY, 1, cv2.LINE_AA)
            else:
                cv2.putText(panel, label, (lx, ly), FONT, 0.32, TXT_DIM, 1, cv2.LINE_AA)

            bar_x = panel_w - 40 - PAD
            bar_w_total = 40
            bar_cy = ry + ROW_H // 2
            bar_h_px = 5
            pill(panel, bar_x, bar_cy - bar_h_px // 2, bar_w_total, bar_h_px, (38, 38, 44))
            if is_active:
                pill(panel, bar_x, bar_cy - bar_h_px // 2, bar_w_total, bar_h_px, color_on)

        count_str = str(st.blink_count)
        cv2.putText(panel, count_str, (70, s1_y), FONT, 0.38, TXT_PRIMARY, 1, cv2.LINE_AA)
        if st.blink_rate > 0:
            rate_str = f"{st.blink_rate:.1f}/m"
            (rw, _), _ = cv2.getTextSize(rate_str, FONT, 0.30, 1)
            cv2.putText(
                panel,
                rate_str,
                (panel_w - rw - PAD, s1_y),
                FONT,
                0.30,
                TXT_SECONDARY,
                1,
                cv2.LINE_AA,
            )

        cov_str = f"{st.coverage_pct:.0f}%"
        cov_col = (80, 210, 120) if st.coverage_pct > 60 else (60, 170, 240)
        (covw, _), _ = cv2.getTextSize(cov_str, FONT, 0.38, 1)
        cv2.putText(
            panel, cov_str, (panel_w - covw - PAD, s2_y), FONT, 0.38, cov_col, 1, cv2.LINE_AA
        )

        sig_parts = []
        for tid, short in TIER_SHORT.items():
            cnt = nms_counts.get(tid, 0)
            if cnt > 0:
                sig_parts.append(f"{short}:{cnt}")
        sig_str = " ".join(sig_parts) if sig_parts else "-"
        cv2.putText(panel, sig_str, (70, s3_y), FONT, 0.30, TXT_PRIMARY, 1, cv2.LINE_AA)

        blend_overlay(frame, panel, panel_x, panel_y, alpha=0.90)

        nms_frame_valid = True
        if nms_valid is not None and data_idx is not None:
            nms_frame_valid = bool(nms_valid[data_idx])
        if not st.is_valid and not nms_frame_valid and data_idx is not None:
            badge_text = "NO TRACKING"
            sc = 0.45
            (btw, bth), _ = cv2.getTextSize(badge_text, FONT, sc, 1)
            b_w = min(btw + 16, badge_w_max)
            b_h = min(bth + 12, badge_h_max)
            badge_buf[:b_h, :b_w] = (24, 24, 32)
            rounded_rect(badge_buf, (0, 0), (b_w - 1, b_h - 1), (55, 55, 65), 5, 1)
            cv2.putText(
                badge_buf, badge_text, (8, bth + 4), FONT, sc, TXT_SECONDARY, 1, cv2.LINE_AA
            )
            bx = (w - panel_w - 20) // 2 - btw // 2
            by = 28
            blend_overlay(frame, badge_buf[:b_h, :b_w], bx, by, alpha=0.85)

        progress = frame_idx / max(total_frames - 1, 1)
        bar_end_x = int(progress * w)
        cv2.rectangle(frame, (0, h - 3), (bar_end_x, h), PROGRESS_COLOR, -1)
        cv2.rectangle(frame, (bar_end_x, h - 3), (w, h), (30, 30, 35), -1)

        f_text = f"F {frame_idx}"
        cv2.putText(frame, f_text, (8, h - 8), FONT, 0.32, (0, 0, 0), 2, cv2.LINE_AA)
        cv2.putText(frame, f_text, (8, h - 8), FONT, 0.32, TXT_DIM, 1, cv2.LINE_AA)

        out.write(frame)
        frame_idx += 1

        if max_frames > 0 and frame_idx >= max_frames:
            break

        if verbose and frame_idx % 5000 == 0:
            pct = 100 * frame_idx / total_frames if total_frames else 0
            print(f"  Processed {frame_idx}/{total_frames} frames ({pct:.0f}%)")

    cap.release()
    out.release()

    ffmpeg_bin = shutil.which("ffmpeg")
    try:
        import imageio_ffmpeg

        ffmpeg_bin = imageio_ffmpeg.get_ffmpeg_exe()
    except ImportError:
        pass
    if out_path.endswith(".mp4") and ffmpeg_bin:
        tmp_path = out_path + ".tmp.mp4"
        try:
            result = subprocess.run(
                [
                    ffmpeg_bin,
                    "-y",
                    "-i",
                    out_path,
                    "-c:v",
                    "libx264",
                    "-preset",
                    "fast",
                    "-crf",
                    "23",
                    "-pix_fmt",
                    "yuv420p",
                    "-movflags",
                    "+faststart",
                    tmp_path,
                ],
                capture_output=True,
                timeout=600,
            )
            if (
                result.returncode == 0
                and Path(tmp_path).exists()
                and Path(tmp_path).stat().st_size > 0
            ):
                Path(tmp_path).replace(out_path)
            else:
                Path(tmp_path).unlink(missing_ok=True)
        except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
            Path(tmp_path).unlink(missing_ok=True)
