"""Temperature daemon main loop."""

from __future__ import annotations

import logging
import os
import signal
import time
from datetime import datetime, timezone
from typing import Any

from .commands import TempCommandError, apply_temp_command, parse_command_message
from .config import TempConfig
from .identity import (
    TUB_COUNT,
    classify_readings,
    evaluate_identity,
    load_probe_map,
    role_readings,
    save_probe_map,
)
from .reader import read_all_f
from .redis_out import CHAN_COMMAND, RedisPublisher
from .runtime import TempRuntime

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

_stop = False


def _handle_signal(signum: int, _frame: object) -> None:
    global _stop
    logger.info("Received signal %s, shutting down", signum)
    _stop = True


def _utc_now() -> str:
    return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")


def _health_status(setup_state: str, ok_count: int, visible_count: int) -> str:
    if setup_state == "need_reinstall":
        return "error"
    if setup_state in ("need_hot_in", "need_water"):
        return "degraded"
    if visible_count == 0:
        return "error"
    if ok_count < TUB_COUNT:
        return "degraded"
    return "ok"


def build_sample(
    cfg: TempConfig,
    runtime: TempRuntime | None = None,
) -> tuple[dict[str, Any], Any, dict[str, Any], dict[str, Any]]:
    from pathlib import Path

    path = cfg.probe_map_path
    rt = runtime or TempRuntime()
    setup_mode = rt.effective_setup_mode(cfg)
    probes_f = read_all_f(cfg.devices_path)
    visible = list(probes_f.keys())
    pmap = load_probe_map(path)
    identity = evaluate_identity(visible, pmap, setup_mode=setup_mode)
    if identity.map_changed:
        save_probe_map(path, identity.probe_map)
        logger.info("Probe map updated: %s", identity.detail)

    labeled, tub_avg, hot_in_f, ok_count, fail_count, spread = classify_readings(
        probes_f, identity.probe_map
    )

    roles = role_readings(probes_f, identity.probe_map)
    rt.last_setup_state = identity.setup_state

    reading: dict[str, Any] = {
        "ts": _utc_now(),
        "setup_state": identity.setup_state,
        "probes": labeled,
        "roles": roles,
        "tub_avg_f": tub_avg,
        "hot_in_f": hot_in_f,
        "outdoor_f": None,
        "ok_count": ok_count,
        "fail_count": fail_count,
        "spread_f": spread,
    }

    setup_payload = {
        "ts": reading["ts"],
        "state": identity.setup_state,
        "detail": identity.detail,
        "visible_ids": identity.visible_ids,
        "setup_active": rt.setup_active,
    }

    health_payload = {
        "service": "temps",
        "ts": reading["ts"],
        "status": _health_status(identity.setup_state, ok_count, len(visible)),
        "detail": identity.detail,
        "pid": os.getpid(),
    }

    return reading, identity, setup_payload, health_payload


def run_once(
    cfg: TempConfig,
    *,
    dry_run: bool = False,
    redis_pub: RedisPublisher | None = None,
    runtime: TempRuntime | None = None,
) -> dict[str, Any]:
    rt = runtime or TempRuntime()
    reading, identity, setup_payload, health_payload = build_sample(cfg, runtime=rt)
    logger.info(
        "sample setup=%s tub_avg=%s hot_in=%s ok=%s/%s visible=%s",
        identity.setup_state,
        reading["tub_avg_f"],
        reading["hot_in_f"],
        reading["ok_count"],
        TUB_COUNT,
        len(identity.visible_ids),
    )
    if dry_run:
        return reading
    pub = redis_pub or RedisPublisher(cfg.redis_url)
    try:
        pub.publish_reading(reading)
        pub.publish_setup(setup_payload, force=rt.setup_active)
        pub.publish_health(health_payload)
    finally:
        if redis_pub is None:
            pub.close()
    return reading


def run_loop(cfg: TempConfig, *, dry_run: bool = False) -> None:
    global _stop
    _stop = False
    signal.signal(signal.SIGTERM, _handle_signal)
    signal.signal(signal.SIGINT, _handle_signal)

    logger.info(
        "Temperature daemon starting (dry_run=%s interval=%ss setup_mode=%s)",
        dry_run,
        cfg.interval_s,
        cfg.setup_mode,
    )

    redis_pub: RedisPublisher | None = None
    if not dry_run:
        redis_pub = RedisPublisher(cfg.redis_url)

    runtime = TempRuntime()
    try:
        while not _stop:
            loop_start = time.monotonic()
            if redis_pub is not None:
                for raw in redis_pub.poll_commands(timeout_s=min(0.5, cfg.interval_s)):
                    try:
                        envelope = parse_command_message(raw)
                        apply_temp_command(cfg, runtime, envelope)
                    except TempCommandError as exc:
                        logger.warning("temps.command rejected: %s", exc)
                    except Exception:
                        logger.exception("temps.command handling failed")
            try:
                run_once(cfg, dry_run=dry_run, redis_pub=redis_pub, runtime=runtime)
            except Exception:
                logger.exception("Error in poll cycle")
            elapsed = time.monotonic() - loop_start
            sleep_s = max(0.0, cfg.interval_s - elapsed)
            if sleep_s > 0 and not _stop:
                time.sleep(sleep_s)
    finally:
        if redis_pub is not None:
            redis_pub.close()
        logger.info("Temperature daemon stopped")
