"""Head movement detectors: nods, shakes, and tilts."""

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

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


def detect_head_oscillations(
    signal,
    successful_frames,
    valid,
    segments,
    *,
    tier="HEAD-NOD",
    sigma=1.0,
    min_amplitude=0.03,
    window_frames=8,
    min_crossings=2,
    merge_gap=4,
    min_duration_frames=2,
    idx_to_frame=None,
):
    """Detect oscillatory head movements (nods or shakes) via zero-crossing analysis."""
    if idx_to_frame is None:
        idx_to_frame = build_idx_to_frame(successful_frames)
    events = []

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

        smoothed = gaussian_filter1d(seg, sigma=sigma)
        velocity = np.diff(smoothed)

        signs = np.sign(velocity)
        crossings = np.where(np.diff(signs) != 0)[0]

        if len(crossings) < min_crossings:
            continue

        osc_mask = np.zeros(len(seg), dtype=bool)
        left_ptr = 0
        for i in range(len(seg) - window_frames):
            while left_ptr < len(crossings) and crossings[left_ptr] < i:
                left_ptr += 1
            right_ptr = left_ptr
            while right_ptr < len(crossings) and crossings[right_ptr] < i + window_frames:
                right_ptr += 1
            window_crossings = right_ptr - left_ptr
            if window_crossings >= min_crossings:
                window_sig = smoothed[i : i + window_frames]
                amplitude = window_sig.max() - window_sig.min()
                if amplitude >= min_amplitude:
                    osc_mask[i : i + window_frames] = True

        regions = mask_to_regions(osc_mask)
        regions = merge_regions(regions, merge_gap)

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

            seg_slice = smoothed[start:end]
            peak_idx = start + int(np.argmax(np.abs(seg_slice - np.mean(seg_slice))))
            peak_val = float(seg_slice.max() - seg_slice.min())

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

            region_crossings = np.sum((crossings >= start) & (crossings < end))
            crossing_density = region_crossings / (duration / window_frames)
            amp_score = float(np.clip((peak_val - min_amplitude) / (3 * min_amplitude), 0, 1))
            density_score = float(np.clip((crossing_density - min_crossings) / min_crossings, 0, 1))
            confidence = 0.5 * amp_score + 0.5 * density_score

            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=tier.lower().replace("-", " "),
                )
            )

    return events


def detect_single_head_movements(
    signal,
    successful_frames,
    valid,
    segments,
    *,
    tier="HEAD-NOD",
    sigma=1.0,
    min_amplitude=0.01,
    existing_events=None,
    idx_to_frame=None,
):
    """Detect isolated single head movements via velocity peaks."""
    if idx_to_frame is None:
        idx_to_frame = build_idx_to_frame(successful_frames)

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

    events = []

    for seg_start, seg_end in segments:
        seg = signal[seg_start:seg_end].astype(np.float64)
        if len(seg) < 4:
            continue

        smoothed = gaussian_filter1d(seg, sigma=sigma)
        velocity = np.diff(smoothed)

        abs_vel = np.abs(velocity)
        peaks, props = find_peaks(abs_vel, prominence=min_amplitude * 0.4, distance=3)

        if len(peaks) == 0:
            continue

        for pk in peaks:
            global_pk = seg_start + pk
            if covered[global_pk]:
                continue

            left = pk
            while left > 0 and np.sign(velocity[left - 1]) == np.sign(velocity[pk]):
                left -= 1
            right = pk + 1
            while right < len(velocity) and np.sign(velocity[right]) == np.sign(velocity[pk]):
                right += 1

            amplitude = abs(smoothed[right] - smoothed[left]) if right < len(smoothed) else 0
            if amplitude < min_amplitude:
                continue

            start = max(0, left - 1)
            end = min(len(seg), right + 1)
            duration = end - start
            if duration < 2:
                continue

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

            region_covered = covered[global_start : global_end + 1].mean()
            if region_covered > 0.5:
                continue

            peak_val = float(amplitude)
            amp_score = float(np.clip((amplitude - min_amplitude) / (3 * min_amplitude), 0, 1))
            confidence = 0.7 * amp_score + 0.3 * 0.5

            label_prefix = tier.lower().replace("-", " ")
            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_pk, global_pk),
                    peak_value=peak_val,
                    duration_ms=duration * (1000.0 / FPS),
                    confidence=float(np.clip(confidence, 0, 1)),
                    label=f"single {label_prefix}",
                )
            )

    return events


def detect_head_tilts(
    signal,
    successful_frames,
    valid,
    segments,
    *,
    sigma=3.0,
    threshold=0.06,
    baseline_window=251,
    min_duration_frames=2,
    idx_to_frame=None,
):
    """Detect sustained head tilts from roll signal."""
    if idx_to_frame is None:
        idx_to_frame = build_idx_to_frame(successful_frames)
    events = []

    for seg_start, seg_end in segments:
        seg = signal[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
        tilt_mask = np.abs(deviation) > threshold

        regions = mask_to_regions(tilt_mask)

        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(np.abs(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))
            direction = "tilt-left" if mean_dev > 0 else "tilt-right"

            amp_score = float(np.clip((abs(peak_val) - threshold) / (3 * threshold), 0, 1))
            dur_score = float(np.clip(duration / (FPS * 0.5), 0, 1))
            confidence = 0.6 * amp_score + 0.4 * dur_score

            events.append(
                NMSEvent(
                    tier="HEAD-TILT",
                    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=direction,
                )
            )

    return events
