"""Eye-related detectors: eyebrow raise/furrow, eye gaze, eye squint."""

import numpy as np
from scipy.ndimage import gaussian_filter1d, median_filter

from ..constants import FPS
from ..models import NMSEvent
from ..processing.utils import build_idx_to_frame, mask_to_regions, merge_regions


def detect_eyebrow_events(
    expression,
    successful_frames,
    valid,
    segments,
    *,
    sigma=3.0,
    threshold_std=1.0,
    min_duration_frames=2,
    idx_to_frame=None,
    pose=None,
):
    """Detect eyebrow raise and furrow from expression coefficients."""
    if idx_to_frame is None:
        idx_to_frame = build_idx_to_frame(successful_frames)
    events = []

    if expression is None or expression.shape[1] < 38:
        return events

    raise_indices = [8, 37]
    furrow_indices = [7, 15, 17]

    raise_raw = expression[:, raise_indices]
    furrow_raw = expression[:, furrow_indices]

    valid_mask = np.zeros(len(expression), dtype=bool)
    for seg_start, seg_end in segments:
        valid_mask[seg_start:seg_end] = True

    def z_score(arr, mask=None):
        subset = arr[mask] if mask is not None else arr
        mu = np.mean(subset, axis=0)
        std = np.std(subset, axis=0)
        std[std < 1e-8] = 1.0
        return (arr - mu) / std

    raise_z = z_score(raise_raw, valid_mask)
    furrow_z = z_score(furrow_raw, valid_mask)

    raise_score = np.mean(raise_z, axis=1)
    furrow_score = np.mean(furrow_z, axis=1)

    raise_smooth = gaussian_filter1d(raise_score, sigma=sigma)
    furrow_smooth = gaussian_filter1d(furrow_score, sigma=sigma)

    if pose is not None:
        yaw_abs = np.abs(pose[:, 1])
        yaw_penalty = np.clip((yaw_abs - 0.3) / 0.7, 0, 1.0)
        per_frame_thresh = threshold_std * (1.0 + yaw_penalty)
    else:
        per_frame_thresh = np.full(len(raise_smooth), threshold_std)

    raise_active = raise_smooth > per_frame_thresh
    furrow_active = furrow_smooth > per_frame_thresh
    conflict = raise_active & furrow_active
    if conflict.any():
        raise_wins = np.abs(raise_smooth) >= np.abs(furrow_smooth)
        raise_active[conflict & ~raise_wins] = False
        furrow_active[conflict & raise_wins] = False

    for tier, score, sign, active in [
        ("EYEBROW-RAISE", raise_smooth, 1, raise_active),
        ("EYEBROW-FURROW", furrow_smooth, -1, furrow_active),
    ]:
        for seg_start, seg_end in segments:
            seg = score[seg_start:seg_end]
            seg_active = active[seg_start:seg_end]
            if len(seg) < min_duration_frames + 1:
                continue

            regions = mask_to_regions(seg_active)

            for start, end in regions:
                duration = end - start
                if duration < min_duration_frames:
                    continue

                seg_slice = seg[start:end]
                if sign > 0:
                    peak_idx = start + int(np.argmax(seg_slice))
                else:
                    peak_idx = start + int(np.argmax(np.abs(seg_slice)))
                peak_val = float(seg[peak_idx])

                global_start = seg_start + start
                global_end = seg_start + end - 1
                global_peak = seg_start + peak_idx

                mag = abs(peak_val)
                amp_score = float(np.clip((mag - threshold_std) / (2 * threshold_std), 0, 1))
                dur_score = float(np.clip(duration / (FPS * 0.4), 0, 1))
                confidence = 0.6 * amp_score + 0.4 * dur_score

                label = "eyebrow raise" if sign > 0 else "eyebrow furrow"

                events.append(
                    NMSEvent(
                        tier=tier,
                        start_frame=idx_to_frame.get(global_start, global_start),
                        end_frame=idx_to_frame.get(global_end, global_end),
                        peak_frame=idx_to_frame.get(global_peak, global_peak),
                        peak_value=peak_val,
                        duration_ms=duration * (1000.0 / FPS),
                        confidence=float(np.clip(confidence, 0, 1)),
                        label=label,
                    )
                )

    return events


def detect_eye_gaze_events(
    leye_pose,
    reye_pose,
    successful_frames,
    valid,
    segments,
    *,
    sigma=1.5,
    threshold_factor=1.5,
    min_duration_frames=2,
    idx_to_frame=None,
):
    """Detect gaze shift events from SMPL eye pose data."""
    if idx_to_frame is None:
        idx_to_frame = build_idx_to_frame(successful_frames)
    events = []

    if leye_pose is None or reye_pose is None:
        return events

    both_valid = ~(np.isnan(leye_pose[:, 0]) | np.isnan(reye_pose[:, 0]))
    gaze = np.full((len(leye_pose), 2), np.nan, dtype=np.float64)
    gaze[both_valid, 0] = (leye_pose[both_valid, 0] + reye_pose[both_valid, 0]) / 2.0
    gaze[both_valid, 1] = (leye_pose[both_valid, 1] + reye_pose[both_valid, 1]) / 2.0

    for seg_start, seg_end in segments:
        seg_pitch = gaze[seg_start:seg_end, 0]
        seg_yaw = gaze[seg_start:seg_end, 1]

        seg_valid = ~np.isnan(seg_pitch)
        if seg_valid.sum() < 10:
            continue

        pitch_baseline = float(np.nanmedian(seg_pitch))
        yaw_baseline = float(np.nanmedian(seg_yaw))
        pitch_std = float(np.nanstd(seg_pitch))
        yaw_std = float(np.nanstd(seg_yaw))

        if pitch_std < 1e-6 or yaw_std < 1e-6:
            continue

        pitch_dev = seg_pitch - pitch_baseline
        yaw_dev = seg_yaw - yaw_baseline
        valid_idx = np.where(~np.isnan(pitch_dev))[0]
        if len(valid_idx) > 2:
            pitch_dev = np.interp(np.arange(len(pitch_dev)), valid_idx, pitch_dev[valid_idx])
            valid_yaw_idx = np.where(~np.isnan(yaw_dev))[0]
            if len(valid_yaw_idx) > 2:
                yaw_dev = np.interp(np.arange(len(yaw_dev)), valid_yaw_idx, yaw_dev[valid_yaw_idx])
            else:
                yaw_dev = np.nan_to_num(yaw_dev, nan=0.0)
        else:
            pitch_dev = np.nan_to_num(pitch_dev, nan=0.0)
            yaw_dev = np.nan_to_num(yaw_dev, nan=0.0)
        pitch_dev = gaussian_filter1d(pitch_dev, sigma=sigma)
        yaw_dev = gaussian_filter1d(yaw_dev, sigma=sigma)

        pitch_thresh = threshold_factor * pitch_std
        yaw_thresh = threshold_factor * yaw_std
        magnitude = np.sqrt((pitch_dev / pitch_std) ** 2 + (yaw_dev / yaw_std) ** 2)
        shift_mask = magnitude > threshold_factor

        regions = mask_to_regions(shift_mask)
        regions = merge_regions(regions, gap=3)

        for start, end in regions:
            duration = end - start
            if duration < min_duration_frames:
                continue

            mean_pitch = float(np.mean(pitch_dev[start:end]))
            mean_yaw = float(np.mean(yaw_dev[start:end]))

            parts = []
            if abs(mean_pitch) > pitch_thresh * 0.5:
                parts.append("up" if mean_pitch > 0 else "down")
            if abs(mean_yaw) > yaw_thresh * 0.5:
                parts.append("left" if mean_yaw > 0 else "right")
            label = "gaze " + ("-".join(parts) if parts else "shift")

            peak_idx = start + int(np.argmax(magnitude[start:end]))
            peak_val = float(magnitude[peak_idx])

            global_start = seg_start + start
            global_end = seg_start + end - 1
            global_peak = seg_start + peak_idx

            mag_score = float(np.clip((peak_val - threshold_factor) / (2 * threshold_factor), 0, 1))
            dur_score = float(np.clip(duration / (FPS * 0.2), 0, 1))
            confidence = 0.6 * mag_score + 0.4 * dur_score

            events.append(
                NMSEvent(
                    tier="EYE-GAZE",
                    start_frame=idx_to_frame.get(global_start, global_start),
                    end_frame=idx_to_frame.get(global_end, global_end),
                    peak_frame=idx_to_frame.get(global_peak, global_peak),
                    peak_value=peak_val,
                    duration_ms=duration * (1000.0 / FPS),
                    confidence=float(np.clip(confidence, 0, 1)),
                    label=label,
                )
            )

    return events


def detect_eye_squint(
    eyelid,
    successful_frames,
    valid,
    segments,
    *,
    sigma=3.0,
    threshold=0.03,
    min_duration_frames=10,
    baseline_window=251,
    blink_events=None,
    idx_to_frame=None,
):
    """Detect sustained eye squint (partial closure) from eyelid parameters."""
    if idx_to_frame is None:
        idx_to_frame = build_idx_to_frame(successful_frames)
    events = []

    blink_mask = np.zeros(len(eyelid), dtype=bool)
    if blink_events:
        frame_to_idx = {}
        for i, vf in enumerate(successful_frames):
            frame_to_idx[int(vf)] = i
        for b in blink_events:
            for f in range(b.start_frame, b.end_frame + 1):
                idx = frame_to_idx.get(f)
                if idx is not None:
                    blink_mask[idx] = True

    for seg_start, seg_end in segments:
        seg = eyelid[seg_start:seg_end].astype(np.float64)
        if len(seg) < min_duration_frames + 2:
            continue

        smoothed = gaussian_filter1d(seg, sigma=sigma)
        baseline = median_filter(smoothed, size=baseline_window, mode="reflect")

        deviation = smoothed - baseline
        squint_mask = deviation > threshold

        seg_blink = blink_mask[seg_start:seg_end]
        squint_mask &= ~seg_blink

        regions = mask_to_regions(squint_mask)
        regions = merge_regions(regions, gap=3)

        for start, end in regions:
            duration = end - start
            if duration < min_duration_frames:
                continue

            seg_dev = deviation[start:end]
            peak_idx = start + int(np.argmax(seg_dev))
            peak_val = float(deviation[peak_idx])

            global_start = seg_start + start
            global_end = seg_start + end - 1
            global_peak = seg_start + peak_idx

            mean_dev = float(np.mean(seg_dev))
            amp_score = float(np.clip((mean_dev - threshold) / (3 * threshold), 0, 1))
            dur_score = float(np.clip(duration / (FPS * 1.0), 0, 1))
            confidence = 0.5 * amp_score + 0.5 * dur_score

            events.append(
                NMSEvent(
                    tier="EYE-SQUINT",
                    start_frame=idx_to_frame.get(global_start, global_start),
                    end_frame=idx_to_frame.get(global_end, global_end),
                    peak_frame=idx_to_frame.get(global_peak, global_peak),
                    peak_value=peak_val,
                    duration_ms=duration * (1000.0 / FPS),
                    confidence=float(np.clip(confidence, 0, 1)),
                    label="eye squint",
                )
            )

    return events
