"""
Contents server — serves alarm audio and status images over WebSocket.

Streams WAV PCM chunks and pushes 1-bit monochrome status images to the
connected ESP32 dashboard client on port 8766.

Protocol:
  Status image:
    1. Text frame:  {"type":"status_image","width":120,"height":120}
    2. Binary frame: 1-bit monochrome bitmap

  Alarm audio:
    1. Text frame:  {"type":"alarm_start","sample_rate":N,"channels":N,"bits":N}
    2. Binary frames: raw PCM chunks (4096 bytes each, paced at ~90% real-time)
    3. Text frame:  {"type":"alarm_stop"}
"""

import argparse
import asyncio
import logging
from datetime import datetime
from pathlib import Path

import websockets

from alarm_scheduler import DEFAULT_CONFIG_PATH, load_config, should_fire
from audio_handler import find_wav, read_wav, stream_alarm
from image_handler import IMG_DIR, load_status_image, send_status_image

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("contents_server")

PORT = 8766
PI_DIR = Path(__file__).parent

# Set by main(), read by handler()
_config_path: Path = DEFAULT_CONFIG_PATH

TICK_INTERVAL = 5  # seconds between schedule checks


def _resolve_path(relative: str) -> Path:
    """Resolve a config path relative to pi/ directory."""
    p = Path(relative)
    if not p.is_absolute():
        p = PI_DIR / p
    return p


def _prepare_alarm(entry: dict) -> dict:
    """Pre-resolve paths and load resources for a single alarm entry."""
    audio_path = find_wav(_resolve_path(entry.get("alarm_audio", "assets/alarm/alarm_test.wav")))
    alarm_img_path = _resolve_path(entry.get("alarm_image", "assets/img/on_alarm.png"))
    pcm, sr, ch, bits = read_wav(audio_path)
    img = load_status_image(alarm_img_path)
    return {
        "config": entry,
        "pcm": pcm, "sr": sr, "ch": ch, "bits": bits,
        "img": img,
        "last_fired": None,
    }


async def handler(ws):
    """Handle a single WebSocket connection."""
    remote = ws.remote_address
    log.info("Client connected: %s:%d", remote[0], remote[1])

    configs = load_config(_config_path)
    img_idle = load_status_image(IMG_DIR / "idle.png")

    try:
        await send_status_image(ws, img_idle)

        if not configs:
            log.info("No alarms configured — idling forever")
            await asyncio.Future()
            return

        alarms = [_prepare_alarm(entry) for entry in configs]

        while True:
            for alarm in alarms:
                if should_fire(alarm["config"]):
                    current_minute = datetime.now().strftime("%Y%m%d%H%M")

                    if current_minute != alarm["last_fired"]:
                        alarm["last_fired"] = current_minute
                        log.info("Alarm firing: %s at %s",
                                 alarm["config"]["alarm_time"], current_minute)
                        await send_status_image(ws, alarm["img"])
                        await stream_alarm(ws, alarm["pcm"], alarm["sr"],
                                           alarm["ch"], alarm["bits"])
                        await send_status_image(ws, img_idle)

            await asyncio.sleep(TICK_INTERVAL)

    except websockets.exceptions.ConnectionClosed:
        log.info("Client disconnected: %s:%d", remote[0], remote[1])


async def main():
    log.info("Contents server starting on port %d", PORT)
    async with websockets.serve(handler, "0.0.0.0", PORT):
        await asyncio.Future()


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Alarm contents server")
    parser.add_argument("--config", type=Path, default=DEFAULT_CONFIG_PATH,
                        help="Path to alarm config JSON (default: %(default)s)")
    args = parser.parse_args()
    _config_path = args.config
    asyncio.run(main())
