"""Flask HTTP server: static UI, REST API, SSE."""

from __future__ import annotations

import json
import logging
import queue
import time
from pathlib import Path
from typing import Any

from flask import Flask, Response, jsonify, request, send_from_directory

from .commands import CommandValidationError, validate_control_command, validate_temp_setup_op
from .config import WebConfig
from .health import api_health_payload
from .redis_io import (
    CHAN_CONTROL_STATE,
    CHAN_TEMPS_READING,
    CHAN_TEMPS_SETUP,
    KEY_CONTROL_LATEST,
    KEY_HEALTH_CONTROL,
    KEY_HEALTH_TEMPS,
    KEY_HEALTH_WEB,
    KEY_TEMPS_LATEST,
    KEY_TEMPS_SETUP,
    WebRedis,
)

logger = logging.getLogger("hot_tub.web")


def create_app(cfg: WebConfig, redis_io: WebRedis) -> Flask:
    app = Flask(__name__, static_folder=None)
    static_dir = cfg.static_dir.resolve()

    def _sse_event(name: str, data: str) -> str:
        return f"event: {name}\ndata: {data}\n\n"

    @app.get("/api/events")
    def api_events() -> Response:
        def generate():
            for key, channel in (
                (KEY_CONTROL_LATEST, CHAN_CONTROL_STATE),
                (KEY_TEMPS_LATEST, CHAN_TEMPS_READING),
                (KEY_TEMPS_SETUP, CHAN_TEMPS_SETUP),
            ):
                snap = redis_io.get_json(key)
                if snap is not None:
                    yield _sse_event(channel, json.dumps(snap))

            q = redis_io.subscribe_sse()
            last_keepalive = time.monotonic()
            try:
                while True:
                    try:
                        channel, data = q.get(timeout=1.0)
                        yield _sse_event(channel, data)
                    except queue.Empty:
                        pass
                    if time.monotonic() - last_keepalive >= cfg.sse_keepalive_s:
                        yield ": keepalive\n\n"
                        last_keepalive = time.monotonic()
            finally:
                redis_io.unsubscribe_sse(q)

        return Response(
            generate(),
            mimetype="text/event-stream",
            headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
        )

    @app.post("/api/command")
    def api_command() -> tuple[Any, int]:
        body = request.get_json(silent=True)
        if not isinstance(body, dict):
            return jsonify({"error": "JSON body required"}), 400
        try:
            op, args = validate_control_command(body)
            cmd_id = redis_io.publish_control_command(op, args)
        except CommandValidationError as exc:
            return jsonify({"error": str(exc)}), 400
        except RuntimeError as exc:
            return jsonify({"error": str(exc)}), 503
        return jsonify({"id": cmd_id}), 202

    def _setup_op(op: str) -> tuple[Any, int]:
        try:
            validate_temp_setup_op(op)
            cmd_id = redis_io.publish_temp_setup_command(op)
        except CommandValidationError as exc:
            return jsonify({"error": str(exc)}), 400
        except RuntimeError as exc:
            return jsonify({"error": str(exc)}), 503
        return jsonify({"id": cmd_id}), 202

    @app.post("/api/sensor-setup/start")
    def sensor_setup_start() -> tuple[Any, int]:
        return _setup_op("setup_start")

    @app.post("/api/sensor-setup/finish")
    def sensor_setup_finish() -> tuple[Any, int]:
        return _setup_op("setup_finish")

    @app.post("/api/sensor-setup/cancel")
    def sensor_setup_cancel() -> tuple[Any, int]:
        return _setup_op("setup_cancel")

    @app.get("/api/status")
    def api_status() -> Any:
        return jsonify(
            {
                "control": redis_io.get_json(KEY_CONTROL_LATEST),
                "temps": redis_io.get_json(KEY_TEMPS_LATEST),
                "health": {
                    "control": redis_io.get_json(KEY_HEALTH_CONTROL),
                    "temps": redis_io.get_json(KEY_HEALTH_TEMPS),
                    "web": redis_io.get_json(KEY_HEALTH_WEB),
                },
            }
        )

    @app.get("/api/health")
    def api_health() -> Any:
        return jsonify(api_health_payload(redis_io, cfg))

    @app.route("/", defaults={"path": ""})
    @app.route("/<path:path>")
    def static_files(path: str) -> Any:
        if path.startswith("api/"):
            return jsonify({"error": "not found"}), 404
        target = static_dir / path
        if path and target.is_file():
            return send_from_directory(static_dir, path)
        index = static_dir / "index.html"
        if index.is_file():
            return send_from_directory(static_dir, "index.html")
        return jsonify({"error": "frontend not built; see src/web/frontend"}), 503

    return app


def run_server(cfg: WebConfig, redis_io: WebRedis) -> None:
    app = create_app(cfg, redis_io)
    app.run(
        host=cfg.bind_host,
        port=cfg.bind_port,
        threaded=True,
        use_reloader=False,
    )
