"""In-memory control state and persistence."""

from __future__ import annotations

import json
import logging
import os
import tempfile
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Literal

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

HeatMode = Literal["sleep", "maintain", "backflush"]
FilterMode = Literal["on", "auto", "off"]
HeatPhase = Literal[
    "idle",
    "startup_1",
    "startup_2",
    "startup_3",
    "heating",
    "backflush",
    "error_startup",
    "error_safety",
]
PumpState = Literal["on", "off"]

HEAT_MODES: frozenset[str] = frozenset({"sleep", "maintain", "backflush"})
FILTER_MODES: frozenset[str] = frozenset({"on", "auto", "off"})


@dataclass
class ControlState:
    heat_mode: HeatMode = "sleep"
    filter_mode: FilterMode = "off"
    target_f: float = 102.0
    heat_phase: HeatPhase = "idle"
    startup_attempt: int = 0
    heat_pump: PumpState = "off"
    filter_pump: PumpState = "off"
    error: dict[str, Any] | None = None
    last_command_id: str | None = None
    tub_avg_f: float | None = None
    hot_in_f: float | None = None
    filter_window_active: bool = False
    filter_next_start: str | None = None

    def to_persist(self) -> dict[str, Any]:
        return {
            "heat_mode": self.heat_mode,
            "filter_mode": self.filter_mode,
            "target_f": self.target_f,
            "error": self.error,
            "heat_phase": self.heat_phase if self.heat_phase == "error_startup" else "idle",
        }

    @classmethod
    def from_persist(cls, data: dict[str, Any], *, default_target: float) -> ControlState:
        st = cls(target_f=default_target)
        if "heat_mode" in data and data["heat_mode"] in HEAT_MODES:
            st.heat_mode = data["heat_mode"]  # type: ignore[assignment]
        if "filter_mode" in data and data["filter_mode"] in FILTER_MODES:
            st.filter_mode = data["filter_mode"]  # type: ignore[assignment]
        if "target_f" in data:
            st.target_f = float(data["target_f"])
        if data.get("error"):
            st.error = dict(data["error"])
        phase = data.get("heat_phase", "idle")
        if phase == "error_startup" and st.error:
            st.heat_phase = "error_startup"
            code = st.error.get("code")
            if code == "startup_failed":
                st.startup_attempt = 3
        return st


@dataclass
class FsmContext:
    """Transient FSM timing (not persisted)."""

    phase_entered_mono: float = 0.0
    wait_until_mono: float = 0.0
    auto_run_started_mono: float | None = None


def load_persisted_state(path: Path, *, default_target: float) -> ControlState:
    if not path.exists():
        return ControlState(target_f=default_target)
    try:
        data = json.loads(path.read_text())
        return ControlState.from_persist(data, default_target=default_target)
    except (json.JSONDecodeError, OSError, TypeError, ValueError) as exc:
        logger.warning("Could not load state from %s: %s", path, exc)
        return ControlState(target_f=default_target)


def save_persisted_state(path: Path, state: ControlState) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    body = json.dumps(state.to_persist(), indent=2) + "\n"
    fd, tmp = tempfile.mkstemp(dir=path.parent, prefix=".control-state-", suffix=".tmp")
    try:
        with os.fdopen(fd, "w", encoding="utf-8") as f:
            f.write(body)
        os.replace(tmp, path)
    except OSError:
        try:
            os.unlink(tmp)
        except OSError:
            pass
        raise


def state_snapshot(state: ControlState, *, ts: str) -> dict[str, Any]:
    return {
        "ts": ts,
        "heat_mode": state.heat_mode,
        "filter_mode": state.filter_mode,
        "target_f": state.target_f,
        "tub_avg_f": state.tub_avg_f,
        "hot_in_f": state.hot_in_f,
        "heat_pump": state.heat_pump,
        "filter_pump": state.filter_pump,
        "heat_phase": state.heat_phase,
        "startup_attempt": state.startup_attempt,
        "error": state.error,
        "last_command_id": state.last_command_id,
        "filter_window_active": state.filter_window_active,
        "filter_next_start": state.filter_next_start,
    }
