# SPDX-License-Identifier: Apache-2.0
"""Goldfish as one profile of the Resolution Protocol (SPEC §8, §10.3).

Loads v01/profile/goldfish.profile.json and its locale grammars, and turns the
profile's retention schedule into the key-chain parameters of keys.py.
"""
from __future__ import annotations

import json
from pathlib import Path

from resolution_protocol import Intent, Profile, Session, load_locale, parse, parse_duration, resolve_locales, unreadable_after

V01 = Path(__file__).resolve().parents[2]
PROTOCOL = V01.parent / "protocol"
PROFILE_PATH = V01 / "profile" / "goldfish.profile.json"
DAY = 86400.0


def profile() -> Profile:
    return Profile.load(PROFILE_PATH)


def locales(p: Profile | None = None) -> list[dict]:
    p = p or profile()
    available = {f.stem: load_locale(f) for f in (PROTOCOL / "locales").glob("*.json")}
    return resolve_locales(p.locales, available)


class Goldfish:
    """One recorder: a session, the grammars it listens with, and the profile."""

    def __init__(self, p: Profile | None = None) -> None:
        self.profile = p or profile()
        self.locales = locales(self.profile)
        self.session = Session(self.profile)

    def hear(self, voice: str, text: str, now: float, confidence: float = 0.95) -> list[str]:
        """A voice spoke: register it, parse what it said, apply every intent. Returns the outcomes."""
        self.session.heard(voice, now)
        return [self.session.apply(i, voice, confidence, now) for i in parse(text, self.locales, self.profile.invocation, self.profile.extensions)]

    # --- the eye is a button (ADR-0013) ------------------------------------------------
    @property
    def room_off(self) -> bool:
        """The room is off the record: the ring shows one low red glow (display.ring_frame(room_off=True))."""
        return self.session.ceiling == 0

    # Eye gestures (ADR-0015, from the usage simulations): every press is safe.
    #   press (down):          the room goes off the record at once, never back on
    #   double press:          back on (a provisional resume, SPEC §6.3), only if the room was already off before it
    #   hold for controls.hold: also shows the battery on the ring
    # The phone transfer needs no button (ADR-0015): when the meeting ends the device writes the note into the NFC tag,
    # the tail icon lights, and a phone touching the tail opens it as a link. `lifted` is kept for an IMU (not in v01):
    # without one, an accidental press only ever takes the room off, the safe direction.
    def _b(self, *keys, default=None):
        d = self.profile.extra.get("bindings", {})
        for k in keys:
            d = d.get(k, {}) if isinstance(d, dict) else {}
        return d if d != {} else default

    def eye_down(self, now: float, lifted: bool = False) -> str:
        """The eye went down. Returns the outcome: lowered | still-off | provisional | ignored-lifted."""
        if lifted:
            return "ignored-lifted"
        double = parse_duration(self._b("controls", "double", default="PT0.6S"))
        was_off = self.room_off
        repeat = self._last_down is not None and now - self._last_down[0] <= double and self._last_down[1]
        self._last_down = (now, was_off)
        if was_off and repeat:
            self._last_down = None                       # a third tap starts a new gesture
            return self.session.apply(Intent("resume", "room", addressed=True, source="ui"), None, 1.0, now)
        if was_off:
            return "still-off"
        return self.session.apply(Intent("lower", "room", 0, addressed=True, source="ui"), None, 1.0, now)

    def eye_up(self, now: float, held: float) -> str | None:
        """The eye came up after `held` seconds. A long hold shows the battery; anything shorter does nothing more."""
        if held >= parse_duration(self._b("controls", "hold", default="PT2S")):
            return "battery"
        return None

    def end_meeting(self, now: float, note_url: str) -> str:
        """The meeting is over (the session gap passed, or the device is switched off cleanly): the note goes into
        the tag, which any phone touching the tail can read, and the tail icon lights while a note is waiting."""
        self.note_url, self.note_at = note_url, now
        return "note-ready"

    def note_ready(self, now: float) -> bool:
        """The tail icon is lit while the newest note waits on the tag (handoff.icon_s, default 12 h)."""
        return self.note_url is not None and now - self.note_at < self._b("handoff", "icon_s", default=43200)

    def press_eye(self, now: float, held: float = 0.0, lifted: bool = False) -> str:
        """One whole press (down at `now`, up `held` s later), for simulators and tests."""
        down = self.eye_down(now, lifted)
        up = None if down == "ignored-lifted" else self.eye_up(now + held, held)
        return up or down

    _last_down: tuple | None = None
    note_url: str | None = None
    note_at: float = float("-inf")

    def provisional_left(self, now: float) -> float | None:
        """Fraction of the objection window still open after a raise for others (SPEC §6.3.4: the recorder MUST show
        the provisional state), or None when nothing is provisional. display.ring_frame(provisional=...) shows it."""
        pv = self.session.provisional
        if not pv:
            return None
        w = self.profile.objection_window
        return max(0.0, min(1.0, 1 - (now - pv["opened"]) / w))


def key_curve(p: Profile) -> dict:
    """Retention schedule → keys.Keyring.midnight(r3_days, r2_until_day, r1_days): days each level stays readable (SPEC §5.1)."""
    days = lambda level: round(unreadable_after(level, p.retention) / DAY) or None
    return {"r3_days": days(3), "r2_until_day": days(2), "r1_days": days(1)}
