"""Floating meeting-recording capsule — right edge of screen, vertically centred.

Sister of src/overlay.py (the dictation capsule at the bottom of the
screen). Same visual language — warm coal pill on a dark background,
golden glow that reacts to live audio level — just rotated vertical and
parked on the right edge so it doesn't fight the Zoom / Meet window
during a call.

Visual design:
  - Pill shape, vertical (CAP_W << CAP_H). Top/bottom semicircles.
  - Upper portion: a "track" (thin faint warm-gold rounded pill) that
    holds a "level bar" inside it. The level bar grows from the bottom
    of the track upward as voice gets louder, with brightness scaling
    too. No motion when nobody is talking — silence reads as silence.
  - Lower portion: stop button (faint ring + cream-coloured square).
  - No timer text, no static red dot, no fake "breathing" animation.

Live audio:
  - `levels_provider: Callable[[int], list[float]]` returns the most
    recent RMS readings (0..1) from BOTH mic and system streams (whichever
    is louder is what we show — the call has at least one side talking
    at any moment). Same contract as `Overlay` in src/overlay.py.
  - EMA smoothing in the view tames the jitter so the level bar feels
    organic, not jumpy.

NOT click-through: the stop button hit-tests its bottom strip. Clicks
above the strip do nothing (the user can drag the call window past the
capsule without accidentally stopping the recording).
"""

from __future__ import annotations

import math
import threading
from typing import Callable, Optional

import objc  # type: ignore
from PyObjCTools import AppHelper  # type: ignore
from AppKit import (  # type: ignore
    NSBackingStoreBuffered,
    NSBezierPath,
    NSColor,
    NSPanel,
    NSScreen,
    NSStatusWindowLevel,
    NSView,
    NSWindowCollectionBehaviorCanJoinAllSpaces,
    NSWindowCollectionBehaviorStationary,
    NSWindowStyleMaskBorderless,
    NSWindowStyleMaskNonactivatingPanel,
)
from Foundation import NSMakeRect, NSTimer  # type: ignore


# Brand tokens — mirrored from src/overlay.py for visual consistency.
ACCENT_R, ACCENT_G, ACCENT_B = 0xEB / 255.0, 0xB6 / 255.0, 0x38 / 255.0
WARM_R, WARM_G, WARM_B = 0xFF / 255.0, 0x9B / 255.0, 0x50 / 255.0
BG_TOP_R, BG_TOP_G, BG_TOP_B = 0x12 / 255.0, 0x0E / 255.0, 0x0A / 255.0
CREAM_R, CREAM_G, CREAM_B = 0xEF / 255.0, 0xEA / 255.0, 0xE4 / 255.0

CAP_W = 38.0
CAP_H = 96.0
PAD = 6.0
STOP_HIT_H = 36.0          # bottom strip = stop button hit area
RIGHT_MARGIN = 18.0
STRIPE_W = 6.0

# EMA smoothing alpha for the voice level — lower = slower / calmer.
# Mirrors src/overlay.py:EMA_ALPHA so the meeting capsule feels related
# to the dictation capsule.
EMA_ALPHA = 0.18
# Empirical: speech RMS sits around 0.05–0.25 with peaks ~0.4. Map that
# range to a 0..1 visual level so the bar reads "full" on normal voice
# rather than only on shouting. Background hum is well below 0.01.
LEVEL_GAIN = 5.0
LEVEL_FLOOR = 0.012


class MeetingCapsuleView(NSView):
    """The drawn pill. Holds the EMA-smoothed level and the stop callback;
    everything else is recomputed in drawRect_ from those two."""

    def initWithFrame_onStop_(self, frame, on_stop: Callable[[], None]):  # type: ignore  # noqa: N802
        self = objc.super(MeetingCapsuleView, self).initWithFrame_(frame)
        if self is None:
            return None
        self._on_stop = on_stop
        self._level = 0.0          # smoothed [0..1]
        self.setWantsLayer_(True)
        # Soft warm-gold halo behind the capsule — same trick the dictation
        # overlay uses to make the pill feel like it's glowing rather than
        # painted on. Opacity stays constant; the inner level bar carries
        # the audio reactivity.
        layer = self.layer()
        if layer is not None:
            layer.setMasksToBounds_(False)
            warm = NSColor.colorWithCalibratedRed_green_blue_alpha_(
                ACCENT_R, ACCENT_G, ACCENT_B, 1.0,
            )
            layer.setShadowColor_(warm.CGColor())
            layer.setShadowRadius_(20.0)
            layer.setShadowOpacity_(0.4)
            layer.setShadowOffset_((0.0, 0.0))
        return self

    def setLevel_(self, lvl):  # noqa: N802
        self._level = max(0.0, min(1.0, float(lvl)))
        self.setNeedsDisplay_(True)

    def isOpaque(self):  # noqa: N802
        return False

    def drawRect_(self, dirty):  # noqa: N802, ARG002
        bounds = self.bounds()
        w = bounds.size.width
        h = bounds.size.height
        cx = w / 2

        # ── Capsule body — dark warm coal pill, no border ───────────────
        radius = w / 2
        body = NSBezierPath.bezierPathWithRoundedRect_xRadius_yRadius_(
            NSMakeRect(0, 0, w, h), radius, radius,
        )
        NSColor.colorWithCalibratedRed_green_blue_alpha_(
            BG_TOP_R, BG_TOP_G, BG_TOP_B, 0.94,
        ).setFill()
        body.fill()

        # ── Voice-reactive level track ──────────────────────────────────
        track_bottom = STOP_HIT_H + 6
        track_top = h - PAD - 2
        track_h = track_top - track_bottom
        track_rect = NSMakeRect(
            cx - STRIPE_W / 2, track_bottom, STRIPE_W, track_h,
        )
        stripe_radius = STRIPE_W / 2

        # Faint base — always visible so the user knows where the bar lives
        # even during silence.
        NSColor.colorWithCalibratedRed_green_blue_alpha_(
            ACCENT_R, ACCENT_G, ACCENT_B, 0.16,
        ).setFill()
        NSBezierPath.bezierPathWithRoundedRect_xRadius_yRadius_(
            track_rect, stripe_radius, stripe_radius,
        ).fill()

        # Level bar — grows from bottom of track upward proportional to
        # smoothed voice level. Brightness also scales so quiet speech
        # looks calm, loud speech looks present.
        lvl = self._level
        if lvl > 0.02:
            bar_h = track_h * lvl
            bar_rect = NSMakeRect(
                cx - STRIPE_W / 2, track_bottom, STRIPE_W, bar_h,
            )
            # Blend ACCENT → WARM as level rises (cooler gold at low,
            # warmer orange at peak). Alpha 0.6 → 0.98.
            t = lvl
            r = ACCENT_R + (WARM_R - ACCENT_R) * t
            g = ACCENT_G + (WARM_G - ACCENT_G) * t
            b = ACCENT_B + (WARM_B - ACCENT_B) * t
            alpha = 0.6 + 0.38 * t
            NSColor.colorWithCalibratedRed_green_blue_alpha_(
                r, g, b, alpha,
            ).setFill()
            NSBezierPath.bezierPathWithRoundedRect_xRadius_yRadius_(
                bar_rect, stripe_radius, stripe_radius,
            ).fill()

        # ── Stop button — bottom strip ──────────────────────────────────
        # Faint cream ring with a centred filled square. The whole
        # STOP_HIT_H bottom slab is the click target so it's hard to miss.
        stop_cy = STOP_HIT_H / 2 + 2
        ring_r = 13.0
        NSColor.colorWithCalibratedRed_green_blue_alpha_(
            CREAM_R, CREAM_G, CREAM_B, 0.22,
        ).setStroke()
        ring = NSBezierPath.bezierPathWithOvalInRect_(
            NSMakeRect(cx - ring_r, stop_cy - ring_r, ring_r * 2, ring_r * 2)
        )
        ring.setLineWidth_(1.0)
        ring.stroke()

        sq_size = 10.0
        NSColor.colorWithCalibratedRed_green_blue_alpha_(
            CREAM_R, CREAM_G, CREAM_B, 0.92,
        ).setFill()
        NSBezierPath.bezierPathWithRoundedRect_xRadius_yRadius_(
            NSMakeRect(cx - sq_size / 2, stop_cy - sq_size / 2, sq_size, sq_size),
            1.5, 1.5,
        ).fill()

    def mouseDown_(self, event):  # noqa: N802
        # Only react if the click landed inside the stop-button hit area
        # (bottom strip). Above the strip, clicks pass through so the user
        # can interact with whatever's underneath (e.g. dragging a call
        # window past the capsule) without accidentally stopping.
        local = self.convertPoint_fromView_(event.locationInWindow(), None)
        if local.y < STOP_HIT_H:
            try:
                self._on_stop()
            except Exception as e:
                print(f"[meeting_overlay] stop callback error: {e}")


class MeetingOverlay:
    """Public surface. One instance per app; show()/hide() are idempotent."""

    def __init__(self) -> None:
        self._panel: Optional[NSPanel] = None
        self._view: Optional[MeetingCapsuleView] = None
        self._timer: Optional[NSTimer] = None
        self._on_stop: Optional[Callable[[], None]] = None
        self._levels_provider: Optional[Callable[[int], list[float]]] = None
        self._smooth: float = 0.0

    def _ensure_panel(self) -> None:
        if self._panel is not None:
            return
        screen = NSScreen.mainScreen()
        if screen is None:
            return
        # Right edge, vertically centred on the visible portion of the screen
        # (visibleFrame excludes the menu bar + Dock).
        visible = screen.visibleFrame()
        x = visible.origin.x + visible.size.width - CAP_W - RIGHT_MARGIN
        y = visible.origin.y + (visible.size.height - CAP_H) / 2
        rect = NSMakeRect(x, y, CAP_W, CAP_H)

        style = NSWindowStyleMaskBorderless | NSWindowStyleMaskNonactivatingPanel
        panel = NSPanel.alloc().initWithContentRect_styleMask_backing_defer_(
            rect, style, NSBackingStoreBuffered, False,
        )
        panel.setOpaque_(False)
        panel.setBackgroundColor_(NSColor.clearColor())
        panel.setHasShadow_(False)
        panel.setLevel_(NSStatusWindowLevel)
        panel.setIgnoresMouseEvents_(False)
        panel.setCollectionBehavior_(
            NSWindowCollectionBehaviorCanJoinAllSpaces
            | NSWindowCollectionBehaviorStationary,
        )
        panel.setHidesOnDeactivate_(False)
        panel.setMovableByWindowBackground_(False)
        panel.setReleasedWhenClosed_(False)

        view = MeetingCapsuleView.alloc().initWithFrame_onStop_(
            NSMakeRect(0, 0, CAP_W, CAP_H), self._fire_stop,
        )
        panel.contentView().addSubview_(view)
        self._panel = panel
        self._view = view

    def show(self,
             on_stop: Callable[[], None],
             levels_provider: Optional[Callable[[int], list[float]]] = None) -> None:
        """Display the capsule. `levels_provider(n)` returns the most recent
        n RMS readings (0..1) from the meeting's combined audio streams.
        When provided, the level bar reacts to voice; when None, the bar
        stays at zero (still useful as a "recording active" affordance
        with the stop button)."""
        AppHelper.callAfter(self._show_main, on_stop, levels_provider)

    def hide(self) -> None:
        AppHelper.callAfter(self._hide_main)

    def _show_main(self, on_stop, levels_provider) -> None:
        self._on_stop = on_stop
        self._levels_provider = levels_provider
        self._smooth = 0.0
        self._ensure_panel()
        if self._panel is None:
            return
        self._panel.orderFrontRegardless()
        if self._timer is None:
            # 30 fps refresh — smooth enough that the level bar feels
            # continuous, cheap enough to ignore CPU-wise.
            self._timer = NSTimer.scheduledTimerWithTimeInterval_repeats_block_(
                1.0 / 30.0, True, lambda _t: self._tick(),
            )

    def _hide_main(self) -> None:
        if self._timer is not None:
            self._timer.invalidate()
            self._timer = None
        if self._panel is not None:
            self._panel.orderOut_(None)

    def _tick(self) -> None:
        if self._view is None:
            return
        # Pull recent RMS readings, take the loudest of the window so
        # short voice spikes don't get lost in averaging. Then map raw
        # RMS to a 0..1 visual range with a soft floor + a square-root
        # curve so quiet talk still moves the bar.
        raw = 0.0
        if self._levels_provider is not None:
            try:
                recent = self._levels_provider(4)
            except Exception:
                recent = []
            if recent:
                raw = max(recent)
        # Floor out background hum so silence reads as silence.
        if raw < LEVEL_FLOOR:
            raw = 0.0
        # Compress with sqrt so the bar moves on quiet voice; gain on top.
        visual = min(1.0, math.sqrt(raw) * LEVEL_GAIN * 0.45)
        # EMA smooth so the bar doesn't jitter frame-to-frame.
        self._smooth = (1.0 - EMA_ALPHA) * self._smooth + EMA_ALPHA * visual
        try:
            self._view.setLevel_(self._smooth)
        except Exception:
            pass

    def _fire_stop(self) -> None:
        # MeetingRecorder.stop() does ~200 ms of subprocess.wait + WAV
        # finalisation. Run it on a background thread so the click doesn't
        # freeze the run loop. The capsule itself hides immediately so the
        # user gets instant feedback.
        cb = self._on_stop
        if cb is None:
            return
        self.hide()
        threading.Thread(
            target=self._invoke_stop_safe, args=(cb,), daemon=True,
        ).start()

    @staticmethod
    def _invoke_stop_safe(cb: Callable[[], None]) -> None:
        try:
            cb()
        except Exception as e:
            print(f"[meeting_overlay] on_stop error: {e}")
