#!/usr/bin/env python3
"""Cycle each health status LED through red, orange, green, then off.

Hardware smoke test for the three RGB LEDs (red + green channels only).
Pin map matches overview.md. Do not run while a health daemon owns these GPIOs.

On the Pi:
    python3 health/led_test.py
    python3 health/led_test.py --dry-run   # off-Pi / no GPIO

Requires: python3-gpiozero (sudo apt install python3-gpiozero)
"""

from __future__ import annotations

import argparse
import signal
import sys
import time
from dataclasses import dataclass
from typing import Literal

Color = Literal["red", "orange", "green", "off"]

STEP_S = 1.0

# (name, red GPIO, green GPIO) — see src/health/overview.md
LEDS: tuple[tuple[str, int, int], ...] = (
    ("Temp", 27, 18),
    ("Control", 25, 24),
    ("Web", 6, 12),
)

COLORS: tuple[Color, ...] = ("red", "orange", "green", "off")

_stop = False


def _handle_signal(signum: int, _frame: object) -> None:
    global _stop
    print(f"\nReceived signal {signum}, stopping…", flush=True)
    _stop = True


@dataclass
class RgLed:
    name: str
    red_pin: int
    green_pin: int
    dry_run: bool
    _red: object = None
    _green: object = None

    def open(self) -> None:
        if self.dry_run:
            return
        from gpiozero import OutputDevice

        self._red = OutputDevice(self.red_pin, active_high=True, initial_value=False)
        self._green = OutputDevice(self.green_pin, active_high=True, initial_value=False)

    def set_color(self, color: Color) -> None:
        red_on = color in ("red", "orange")
        green_on = color in ("orange", "green")
        if self.dry_run:
            print(f"  {self.name}: {color}", flush=True)
            return
        assert self._red is not None and self._green is not None
        self._red.on() if red_on else self._red.off()
        self._green.on() if green_on else self._green.off()
        print(f"  {self.name}: {color}", flush=True)

    def off(self) -> None:
        self.set_color("off")

    def close(self) -> None:
        if self.dry_run:
            return
        if self._red is not None:
            self._red.off()
            self._red.close()
            self._red = None
        if self._green is not None:
            self._green.off()
            self._green.close()
            self._green = None


def run_cycle(leds: list[RgLed]) -> None:
    for led in leds:
        if _stop:
            return
        print(f"{led.name} LED", flush=True)
        for color in COLORS:
            if _stop:
                return
            led.set_color(color)
            if color != "off":
                time.sleep(STEP_S)


def main() -> int:
    global _stop
    parser = argparse.ArgumentParser(description="Cycle health status LEDs through red/orange/green/off.")
    parser.add_argument(
        "--dry-run",
        action="store_true",
        help="Print sequence only; do not use GPIO.",
    )
    args = parser.parse_args()

    signal.signal(signal.SIGINT, _handle_signal)
    signal.signal(signal.SIGTERM, _handle_signal)

    leds = [RgLed(name, r, g, args.dry_run) for name, r, g in LEDS]
    try:
        for led in leds:
            led.open()
        print("Cycling Temp → Control → Web (Ctrl+C to stop)", flush=True)
        while not _stop:
            run_cycle(leds)
    finally:
        for led in leds:
            led.off()
            led.close()
        print("All LED channels off.", flush=True)
    return 0


if __name__ == "__main__":
    sys.exit(main())
