#!/usr/bin/env python3
"""Phase 1 early win — bank-wide progressive trap (progwinval on bus)."""

from __future__ import annotations

import os
import sys
import threading
import time
import urllib.error
import urllib.request
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Callable, Optional

# Flat layout: shared decode modules live beside this file (dev/ or prod/).
_RUNTIME_ROOT = Path(__file__).resolve().parent
if str(_RUNTIME_ROOT) not in sys.path:
    sys.path.insert(0, str(_RUNTIME_ROOT))

from decode_x9_result import (
    SKILL_JPOT_GRANT_OPCODES,
    decode_frame,
    tid_hex_from_frame,
)

# Gameplay x9 opcodes that carry progressive block — exclude skill deal 0x59/0x79.
PHASE1_GAMEPLAY_JPOT_OPCODES = frozenset({0x29, 0x39, 0x49, 0x69})

DEFAULT_DYZINEFRAME_FLAG_BASE = "http://127.0.0.1:31234"
DEFAULT_DEDUPE_MS = 5000.0
DEFAULT_MAX_LOOPS = 6


def is_phase1_progressive_opcode(msg_type: int) -> bool:
    if msg_type in SKILL_JPOT_GRANT_OPCODES:
        return True
    return msg_type in PHASE1_GAMEPLAY_JPOT_OPCODES


def phase1_dedupe_key(raw: bytes) -> tuple[str, int, int, int] | None:
    if len(raw) < 8 or raw[0] != 0x05 or raw[-1] != 0x0D:
        return None
    tid = tid_hex_from_frame(raw) or "????"
    msg = raw[1]
    play_seq = raw[7]
    counter = raw[17] if len(raw) > 17 else -1
    return (tid.upper(), msg, play_seq, counter)


def fireworks_loops_from_progwin_cents(cents: int, *, max_loops: int = DEFAULT_MAX_LOOPS) -> int:
    if cents <= 0:
        return 0
    if cents >= 1_000_000:
        return min(max_loops, 6)
    if cents >= 500_000:
        return min(max_loops, 5)
    if cents >= 100_000:
        return min(max_loops, 4)
    if cents >= 50_000:
        return min(max_loops, 3)
    if cents >= 10_000:
        return min(max_loops, 2)
    return 1


def resolve_dyzineframe_flag_base() -> str:
    return os.environ.get("DYZINEFRAME_FLAG_BASE", DEFAULT_DYZINEFRAME_FLAG_BASE).rstrip("/")


def _is_local_flag_base(base: str) -> bool:
    lowered = base.lower()
    return lowered.startswith("http://127.0.0.1") or lowered.startswith("http://localhost")


def trigger_local_fireworks_ipc(
    *,
    winner_tid: str,
    progwin_cents: int,
    game_id_hex: str | None = None,
    dedupe_key: str | None = None,
    local_wire_tid: str | None = None,
) -> dict[str, Any] | None:
    from fireworks_ipc import local_fireworks_ipc_enabled, write_fireworks_request

    if not local_fireworks_ipc_enabled():
        return None
    return write_fireworks_request(
        winner_tid=winner_tid,
        progwin_cents=progwin_cents,
        game_id_hex=game_id_hex,
        dedupe_key=dedupe_key,
        local_wire_tid=local_wire_tid,
        # Bank progressive jackpot — full celebration with JACKPOT WINNER overlay.
        is_progressive=True,
    )


def trigger_dyzineframe_fireworks(
    loops: int,
    *,
    base_url: str | None = None,
    timeout_s: float = 0.5,
) -> bool:
    if loops <= 0:
        return False
    base = (base_url or resolve_dyzineframe_flag_base()).rstrip("/")
    if _is_local_flag_base(base):
        return False
    url = f"{base}/flag/fireworks/{loops}"
    req = urllib.request.Request(url, method="GET")
    with urllib.request.urlopen(req, timeout=timeout_s) as resp:
        return 200 <= resp.status < 300


@dataclass
class Phase1Hit:
    tid_hex: str
    msg_type: int
    game_id_hex: str | None
    play_sequence: int | None
    counter: int | None
    progwin_cents: int
    fireworks_loops: int
    source: str
    dedupe_key: tuple[str, int, int, int]


class Phase1ProgressiveTrap:
    """Detect progwinval != 0 on bus; trigger DyzineFrame fireworks (bank-wide, no TID filter)."""

    def __init__(
        self,
        *,
        dedupe_ms: float = DEFAULT_DEDUPE_MS,
        max_loops: int = DEFAULT_MAX_LOOPS,
        flag_base: str | None = None,
        dry_run: bool = False,
        on_event: Callable[..., None] | None = None,
    ):
        self.dedupe_ms = dedupe_ms
        self.max_loops = max_loops
        self.flag_base = flag_base
        self.dry_run = dry_run
        self._on_event = on_event
        self._lock = threading.Lock()
        self._seen: dict[tuple[str, int, int, int], float] = {}

    def _emit(self, ev: str, **fields: Any) -> None:
        if self._on_event is not None:
            self._on_event(ev, **fields)

    def _prune(self, now: float) -> None:
        ttl = self.dedupe_ms / 1000.0
        cutoff = now - ttl
        self._seen = {k: ts for k, ts in self._seen.items() if ts >= cutoff}

    def inspect(
        self,
        frame: bytes,
        *,
        source: str,
        local_wire_tid: str | None = None,
    ) -> Phase1Hit | None:
        dec = decode_frame(frame)
        if not dec.is_progressive_hit:
            return None
        if not is_phase1_progressive_opcode(dec.msg_type):
            return None
        key = phase1_dedupe_key(frame)
        if key is None:
            return None
        prog = dec.progressive
        assert prog is not None
        cents = prog.progwin_primary
        loops = fireworks_loops_from_progwin_cents(cents, max_loops=self.max_loops)
        if loops <= 0:
            return None
        counter = key[3] if key[3] >= 0 else None
        hit = Phase1Hit(
            tid_hex=dec.tid_hex or key[0],
            msg_type=dec.msg_type,
            game_id_hex=dec.game_id_hex,
            play_sequence=dec.play_sequence,
            counter=counter,
            progwin_cents=cents,
            fireworks_loops=loops,
            source=source,
            dedupe_key=key,
        )
        now = time.monotonic()
        with self._lock:
            self._prune(now)
            if key in self._seen:
                self._emit(
                    "early_win_phase1_dedupe",
                    tid=hit.tid_hex,
                    msg=f"{hit.msg_type:02X}",
                    game=hit.game_id_hex,
                    progwin_cents=cents,
                    source=source,
                )
                return None
            self._seen[key] = now
        self._emit(
            "early_win_phase1_hit",
            tid=hit.tid_hex,
            msg=f"{hit.msg_type:02X}",
            game=hit.game_id_hex,
            seq=hit.play_sequence,
            counter=hit.counter,
            progwin_cents=cents,
            progwin_dollars=round(cents / 100.0, 2),
            fireworks_loops=loops,
            source=source,
        )
        if self.dry_run:
            self._emit(
                "early_win_phase1_fireworks_dry_run",
                loops=loops,
                tid=hit.tid_hex,
            )
            return hit
        dedupe_s = ":".join(str(x) for x in hit.dedupe_key)
        ipc_payload = trigger_local_fireworks_ipc(
            winner_tid=hit.tid_hex,
            progwin_cents=cents,
            game_id_hex=hit.game_id_hex,
            dedupe_key=dedupe_s,
            local_wire_tid=local_wire_tid,
        )
        if ipc_payload is not None:
            self._emit(
                "early_win_phase1_fireworks_ipc",
                tid=hit.tid_hex,
                progwin_cents=cents,
                duration_sec=ipc_payload.get("duration_sec"),
                is_winner=ipc_payload.get("is_winner"),
                seq=ipc_payload.get("seq"),
            )
        base = self.flag_base
        if base and not _is_local_flag_base(base.rstrip("/")):
            try:
                ok = trigger_dyzineframe_fireworks(loops, base_url=base)
            except (OSError, urllib.error.URLError, urllib.error.HTTPError, TimeoutError) as exc:
                self._emit(
                    "early_win_phase1_fireworks_err",
                    loops=loops,
                    tid=hit.tid_hex,
                    error=str(exc),
                )
                return hit
            if ok:
                self._emit(
                    "early_win_phase1_fireworks",
                    loops=loops,
                    tid=hit.tid_hex,
                    progwin_cents=cents,
                )
        elif ipc_payload is None:
            self._emit(
                "early_win_phase1_fireworks_err",
                loops=loops,
                tid=hit.tid_hex,
                error="no local IPC and no remote flag base",
            )
        return hit


def build_phase1_trap_from_env(
    on_event: Callable[..., None] | None = None,
    *,
    default: str = "0",
) -> Phase1ProgressiveTrap | None:
    if os.environ.get("FIXTURE_EARLY_WIN_PHASE1", default).strip().lower() not in (
        "1",
        "true",
        "yes",
    ):
        return None
    dedupe_ms = float(os.environ.get("FIXTURE_EARLY_WIN_DEDUPE_MS", str(DEFAULT_DEDUPE_MS)))
    max_loops = int(os.environ.get("FIXTURE_EARLY_WIN_MAX_LOOPS", str(DEFAULT_MAX_LOOPS)))
    dry_run = os.environ.get("FIXTURE_EARLY_WIN_DRY_RUN", "0") in ("1", "true", "yes")
    flag_base = os.environ.get("DYZINEFRAME_FLAG_BASE") or None
    return Phase1ProgressiveTrap(
        dedupe_ms=dedupe_ms,
        max_loops=max_loops,
        flag_base=flag_base,
        dry_run=dry_run,
        on_event=on_event,
    )


def _selftest() -> list[str]:
    errors: list[str] = []
    if fireworks_loops_from_progwin_cents(500_088) < 2:
        errors.append("500088 cents should map to >=2 loops")
    if fireworks_loops_from_progwin_cents(0) != 0:
        errors.append("zero cents -> 0 loops")
    if not is_phase1_progressive_opcode(0x49):
        errors.append("0x49 should trap")
    if is_phase1_progressive_opcode(0x59):
        errors.append("0x59 deal should not trap")
    if is_phase1_progressive_opcode(0x89):
        errors.append("0x89 slot should not trap")
    frame = bytearray(80)
    frame[0] = 0x05
    frame[1] = 0x49
    frame[3] = 0x38
    frame[4] = 0xD7
    frame[5] = 0x09
    frame[6] = 0x30
    frame[7] = 0x07
    frame[13:17] = (500_088).to_bytes(4, "big")
    frame[17] = 0x42
    frame[-1] = 0x0D
    key = phase1_dedupe_key(bytes(frame))
    if key is None:
        errors.append("dedupe key failed")
    hits = []
    trap = Phase1ProgressiveTrap(dry_run=True, on_event=lambda ev, **kw: hits.append(ev))
    trap.inspect(bytes(frame), source="test")
    trap.inspect(bytes(frame), source="test")
    if hits.count("early_win_phase1_hit") != 1:
        errors.append(f"expected 1 hit, got {hits}")
    dec = decode_frame(bytes(frame))
    if not dec.is_progressive_hit:
        errors.append("decode should see jpot hit")
    return errors


if __name__ == "__main__":
    import sys

    fails = _selftest()
    if fails:
        for msg in fails:
            print(msg, file=sys.stderr)
        sys.exit(1)
    print("early_win_phase1 selftest ok")
