from contextlib import asynccontextmanager
from threading import Event, Lock, Thread
from typing import Dict, Optional
from time import sleep

from fastapi import FastAPI
from gpiozero import OutputDevice


# BCM GPIO pin numbers
RELAY_PINS = {
    1: 26,  # Green
    2: 19,  # Yellow
    3: 13,  # Red
    4: 6,   # Alert
}

# Most relay HATs are active-low.
# Change this to True if your relay board is active-high.
RELAY_ACTIVE_HIGH = False

# Alert: one second on, one second off.
ALERT_INTERVAL_SECONDS = 1.0

# Demo: duration of each color in the sequence.
DEMO_INTERVAL_SECONDS = 0.25

# Green -> Yellow -> Red -> Yellow -> repeat
DEMO_SEQUENCE = (
    (1, "green"),
    (2, "yellow"),
    (3, "red"),
    (2, "yellow"),
)


relays: Dict[int, OutputDevice] = {}

# Protects access to the three color relays.
color_lock = Lock()

# Protects alert thread state.
alert_lock = Lock()

# Protects demo thread state.
demo_lock = Lock()


active_color: Optional[str] = None

alert_stop_event = Event()
alert_thread: Optional[Thread] = None

demo_stop_event = Event()
demo_thread: Optional[Thread] = None

def reset_relays() -> None:
    """
    Reset the entire relay system.

    - Stops demo mode
    - Stops alert mode
    - Turns every relay off
    """
    stop_demo(turn_colors_off=False)
    stop_alert()

    with color_lock:
        relays[1].off()
        relays[2].off()
        relays[3].off()

        global active_color
        active_color = None

    if 4 in relays:
        relays[4].off()


def turn_colors_off_locked() -> None:
    """
    Turn off green, yellow, and red.

    color_lock must already be held.
    Alert is not affected.
    """
    global active_color

    relays[1].off()
    relays[2].off()
    relays[3].off()

    active_color = None


def set_color_locked(relay_number: int, color_name: str) -> None:
    """
    Activate exactly one color relay.

    color_lock must already be held.
    Alert is not affected.
    """
    global active_color

    turn_colors_off_locked()
    relays[relay_number].on()
    active_color = color_name


# ---------------------------------------------------------------------------
# Alert mode
# ---------------------------------------------------------------------------

def alert_worker() -> None:
    """
    Toggle the alert relay until alert_stop_event is set.
    """
    alert_relay = relays[4]

    try:
        while not alert_stop_event.is_set():
            alert_relay.on()

            if alert_stop_event.wait(ALERT_INTERVAL_SECONDS):
                break

            alert_relay.off()

            if alert_stop_event.wait(ALERT_INTERVAL_SECONDS):
                break
    finally:
        alert_relay.off()


def start_alert() -> None:
    """
    Start alert blinking.

    Color and demo modes are not affected.
    Repeated calls do not create additional threads.
    """
    global alert_thread

    with alert_lock:
        if alert_thread is not None and alert_thread.is_alive():
            return

        alert_stop_event.clear()

        alert_thread = Thread(
            target=alert_worker,
            name="relay-alert-thread",
            daemon=True,
        )
        alert_thread.start()


def stop_alert() -> None:
    """
    Stop alert blinking.

    Color and demo modes are not affected.
    """
    global alert_thread

    with alert_lock:
        alert_stop_event.set()

        thread = alert_thread
        alert_thread = None

    # Do not hold alert_lock while waiting for the worker.
    if thread is not None and thread.is_alive():
        thread.join()

    if 4 in relays:
        relays[4].off()


def is_alert_running() -> bool:
    with alert_lock:
        return alert_thread is not None and alert_thread.is_alive()


# ---------------------------------------------------------------------------
# Demo mode
# ---------------------------------------------------------------------------

def demo_worker() -> None:
    """
    Continuously cycle through:

    green -> yellow -> red -> yellow
    """
    try:
        while not demo_stop_event.is_set():
            for relay_number, color_name in DEMO_SEQUENCE:
                if demo_stop_event.is_set():
                    break

                with color_lock:
                    set_color_locked(relay_number, color_name)

                if demo_stop_event.wait(DEMO_INTERVAL_SECONDS):
                    break
    finally:
        # Leave all color relays off when demo mode ends.
        with color_lock:
            turn_colors_off_locked()


def start_demo() -> None:
    """
    Start demo mode.

    Alert mode is not affected.
    Repeated calls do not create additional demo threads.
    """
    global demo_thread

    with demo_lock:
        if demo_thread is not None and demo_thread.is_alive():
            return

        demo_stop_event.clear()

        demo_thread = Thread(
            target=demo_worker,
            name="relay-demo-thread",
            daemon=True,
        )
        demo_thread.start()


def stop_demo(turn_colors_off: bool = True) -> None:
    """
    Stop demo mode.

    Alert mode is not affected.

    When turn_colors_off is True, all three color relays are turned off.
    """
    global demo_thread

    with demo_lock:
        demo_stop_event.set()

        thread = demo_thread
        demo_thread = None

    # Do not hold demo_lock while waiting for the worker.
    if thread is not None and thread.is_alive():
        thread.join()

    if turn_colors_off and relays:
        with color_lock:
            turn_colors_off_locked()


def is_demo_running() -> bool:
    with demo_lock:
        return demo_thread is not None and demo_thread.is_alive()


# ---------------------------------------------------------------------------
# Manual color mode
# ---------------------------------------------------------------------------

def activate_manual_color(relay_number: int, color_name: str) -> None:
    """
    Stop demo mode and activate a manually selected color.

    Alert mode is not affected.
    """
    stop_demo(turn_colors_off=True)

    with color_lock:
        set_color_locked(relay_number, color_name)


def turn_everything_off() -> None:
    """
    Stop demo and alert modes and turn off every relay.
    """
    stop_demo(turn_colors_off=True)
    stop_alert()

    if relays:
        with color_lock:
            turn_colors_off_locked()


# ---------------------------------------------------------------------------
# FastAPI lifecycle
# ---------------------------------------------------------------------------

@asynccontextmanager
async def lifespan(app: FastAPI):
    try:
        # Initialize GPIOs
        for relay_number, gpio_pin in RELAY_PINS.items():
            relays[relay_number] = OutputDevice(
                pin=gpio_pin,
                active_high=RELAY_ACTIVE_HIGH,
                initial_value=False,
            )

        # Give the GPIO driver a moment to initialize
        sleep(0.5)

        # Force every relay into the OFF state
        reset_relays()

        yield

    finally:
        reset_relays()

        for relay in relays.values():
            relay.close()

        relays.clear()

app = FastAPI(
    title="Raspberry Pi Signal Light API",
    description=(
        "Controls mutually exclusive green, yellow, and red relays. "
        "Alert blinking operates independently. "
        "Demo mode cycles through green, yellow, red, and yellow."
    ),
    version="3.0.0",
    lifespan=lifespan,
)


# ---------------------------------------------------------------------------
# API endpoints
# ---------------------------------------------------------------------------

@app.get("/")
def root():
    return {
        "service": "Raspberry Pi Signal Light API",
        "active_color": active_color,
        "alert_active": is_alert_running(),
        "demo_active": is_demo_running(),
        "endpoints": {
            "green": "POST /green",
            "yellow": "POST /yellow",
            "red": "POST /red",
            "start_alert": "POST /alert",
            "stop_alert": "POST /alert/off",
            "start_demo": "POST /demo",
            "stop_demo": "POST /demo/off",
            "all_off": "POST /off",
            "status": "GET /status",
        },
    }


@app.post("/green")
def green():
    activate_manual_color(
        relay_number=1,
        color_name="green",
    )

    return {
        "active_color": "green",
        "demo_active": False,
        "alert_active": is_alert_running(),
    }


@app.post("/yellow")
def yellow():
    activate_manual_color(
        relay_number=2,
        color_name="yellow",
    )

    return {
        "active_color": "yellow",
        "demo_active": False,
        "alert_active": is_alert_running(),
    }


@app.post("/red")
def red():
    activate_manual_color(
        relay_number=3,
        color_name="red",
    )

    return {
        "active_color": "red",
        "demo_active": False,
        "alert_active": is_alert_running(),
    }


@app.post("/alert")
def alert():
    start_alert()

    return {
        "active_color": active_color,
        "alert_active": True,
        "demo_active": is_demo_running(),
        "mode": "blinking",
        "on_seconds": ALERT_INTERVAL_SECONDS,
        "off_seconds": ALERT_INTERVAL_SECONDS,
    }


@app.post("/alert/off")
def alert_off():
    stop_alert()

    return {
        "active_color": active_color,
        "alert_active": False,
        "demo_active": is_demo_running(),
    }


@app.post("/demo")
def demo():
    start_demo()

    return {
        "demo_active": True,
        "alert_active": is_alert_running(),
        "sequence": [
            color_name
            for _, color_name in DEMO_SEQUENCE
        ],
        "interval_seconds": DEMO_INTERVAL_SECONDS,
    }


@app.post("/demo/off")
def demo_off():
    stop_demo(turn_colors_off=True)

    return {
        "active_color": None,
        "demo_active": False,
        "alert_active": is_alert_running(),
    }


@app.post("/off")
def off():
    turn_everything_off()

    return {
        "active_color": None,
        "alert_active": False,
        "demo_active": False,
    }


@app.get("/status")
def status():
    with color_lock:
        color_status = {
            "green": {
                "gpio": RELAY_PINS[1],
                "on": relays[1].is_active,
            },
            "yellow": {
                "gpio": RELAY_PINS[2],
                "on": relays[2].is_active,
            },
            "red": {
                "gpio": RELAY_PINS[3],
                "on": relays[3].is_active,
            },
        }

        current_color = active_color

    alert_running = is_alert_running()
    demo_running = is_demo_running()

    return {
        "active_color": current_color,
        "alert_active": alert_running,
        "demo_active": demo_running,
        "relays": {
            **color_status,
            "alert": {
                "gpio": RELAY_PINS[4],
                "on": relays[4].is_active,
                "blinking": alert_running,
            },
        },
    }
