"""Heat mode FSM: sleep, maintain (startup + heating), backflush."""

from __future__ import annotations

import logging
import time
from typing import Any

from .config import ControlConfig
from .safety import apply_safety_trip, check_heat_allowed
from .state import ControlState, FsmContext, HeatPhase, PumpState

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

STARTUP_PHASES: tuple[HeatPhase, ...] = ("startup_1", "startup_2", "startup_3")


def _attempt_to_phase(attempt: int) -> HeatPhase:
    if attempt <= 1:
        return "startup_1"
    if attempt == 2:
        return "startup_2"
    return "startup_3"


def _mono() -> float:
    return time.monotonic()


def _enter_phase(state: ControlState, ctx: FsmContext, phase: HeatPhase) -> None:
    if state.heat_phase != phase:
        logger.info("heat_phase %s -> %s", state.heat_phase, phase)
    state.heat_phase = phase
    ctx.phase_entered_mono = _mono()
    ctx.wait_until_mono = 0.0


def _maintain_needs_heat(state: ControlState, cfg: ControlConfig) -> bool:
    if state.tub_avg_f is None:
        return False
    low = state.target_f - cfg.maintain.band_f
    if state.tub_avg_f <= low:
        return True
    if state.tub_avg_f >= state.target_f:
        return False
    # Between band edges: keep heating if already in heating or startup; else idle
    if state.heat_phase in ("heating", *STARTUP_PHASES):
        return True
    return False


def _startup_check_passed(state: ControlState, cfg: ControlConfig) -> bool:
    if state.tub_avg_f is None or state.hot_in_f is None:
        return False
    return state.hot_in_f >= state.tub_avg_f + cfg.maintain.hot_in_rise_f


def _begin_startup(state: ControlState, ctx: FsmContext, cfg: ControlConfig) -> None:
    if state.startup_attempt <= 0:
        state.startup_attempt = 1
    state.heat_pump = "on"
    _enter_phase(state, ctx, _attempt_to_phase(state.startup_attempt))
    ctx.wait_until_mono = ctx.phase_entered_mono + cfg.maintain.startup_delay_s


def _fail_startup_attempt(
    state: ControlState,
    ctx: FsmContext,
    cfg: ControlConfig,
) -> None:
    state.heat_pump = "off"
    attempt = state.startup_attempt
    if attempt >= cfg.maintain.startup_attempts:
        _enter_phase(state, ctx, "error_startup")
        state.error = {
            "code": "startup_failed",
            "detail": "Three startup attempts failed — check propane or run backflush",
        }
        logger.warning("startup failed after %s attempts", attempt)
        return
    state.startup_attempt = attempt + 1
    _enter_phase(state, ctx, "idle")
    ctx.wait_until_mono = _mono() + cfg.maintain.retry_off_s
    logger.info("startup attempt %s failed; retry after %.0fs", attempt, cfg.maintain.retry_off_s)


def tick_heat(
    state: ControlState,
    ctx: FsmContext,
    cfg: ControlConfig,
    reading: dict[str, Any] | None,
) -> None:
    """Advance heat FSM one tick; may set heat_pump and heat_phase."""
    now_mono = _mono()

    if state.heat_mode == "sleep":
        if state.heat_pump != "off":
            state.heat_pump = "off"
        if state.heat_phase not in ("idle", "error_startup"):
            _enter_phase(state, ctx, "idle")
        if state.heat_phase != "error_startup":
            state.startup_attempt = 0
            if state.error and state.error.get("code") == "startup_failed":
                state.error = None
        return

    if state.heat_mode == "backflush":
        safety = check_heat_allowed(
            state,
            cfg,
            reading,
            require_hot_in=False,
        )
        if not safety.allow_heat and state.heat_phase == "backflush":
            if safety.trip:
                apply_safety_trip(state, safety.reason or "safety")
            state.heat_pump = "off"
            return

        if state.heat_phase != "backflush":
            _enter_phase(state, ctx, "backflush")
            state.heat_pump = "on"
            ctx.wait_until_mono = ctx.phase_entered_mono + cfg.backflush.duration_s
            return

        if now_mono >= ctx.wait_until_mono:
            state.heat_pump = "off"
            _enter_phase(state, ctx, "idle")
            revert = cfg.backflush.revert_to_heat_mode
            if revert in ("sleep", "maintain", "backflush"):
                logger.info("backflush complete; heat_mode -> %s", revert)
                state.heat_mode = revert  # type: ignore[assignment]
            else:
                state.heat_mode = "sleep"
        return

    # maintain
    if state.heat_phase == "error_startup":
        state.heat_pump = "off"
        return

    if state.heat_phase == "error_safety":
        safety = check_heat_allowed(state, cfg, reading, require_hot_in=False)
        if safety.allow_heat:
            state.error = None
            _enter_phase(state, ctx, "idle")
        else:
            state.heat_pump = "off"
            return

    safety = check_heat_allowed(
        state,
        cfg,
        reading,
        require_hot_in=state.heat_phase in STARTUP_PHASES and now_mono >= ctx.wait_until_mono,
    )
    if not safety.allow_heat:
        if state.heat_pump == "on" and safety.trip:
            apply_safety_trip(state, safety.reason or "safety")
        elif state.heat_pump == "on":
            state.heat_pump = "off"
            _enter_phase(state, ctx, "idle")
        return

    needs = _maintain_needs_heat(state, cfg)

    if not needs:
        if state.heat_pump != "off":
            state.heat_pump = "off"
        if state.heat_phase not in ("idle",):
            _enter_phase(state, ctx, "idle")
        state.startup_attempt = 0
        return

    # Need heat
    if state.heat_pump == "off" and state.heat_phase == "idle":
        if ctx.wait_until_mono > now_mono:
            return
        _begin_startup(state, ctx, cfg)
        return

    if state.heat_phase in STARTUP_PHASES:
        if now_mono < ctx.wait_until_mono:
            return
        if _startup_check_passed(state, cfg):
            logger.info(
                "startup success (attempt %s): hot_in=%.1f tub_avg=%.1f",
                state.startup_attempt,
                state.hot_in_f or 0,
                state.tub_avg_f or 0,
            )
            _enter_phase(state, ctx, "heating")
            state.heat_pump = "on"
            state.startup_attempt = 0
        else:
            _fail_startup_attempt(state, ctx, cfg)
        return

    if state.heat_phase == "heating":
        state.heat_pump = "on"
        if state.tub_avg_f is not None and state.tub_avg_f >= state.target_f:
            state.heat_pump = "off"
            _enter_phase(state, ctx, "idle")
        return

    # Pump on but phase idle — start startup
    if state.heat_pump == "off":
        _begin_startup(state, ctx, cfg)
