"""GET /sites/{site_id}/status/full – monitoring snapshot + alert pravidla."""

from __future__ import annotations

import json
from datetime import date, datetime, timedelta, timezone
from typing import Annotated, Any, Literal
from zoneinfo import ZoneInfo

import asyncpg
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field

from app.db_json import fetch_json
from app.deps import get_pg_pool
from app.notifications_logic import (
    EvSessionRow,
    NegWindowRow,
    PriceSlot,
    build_smart_notifications,
)

router = APIRouter(prefix="/sites/{site_id}", tags=["sites"])


class SiteNotificationItem(BaseModel):
    id: str
    level: Literal["success", "info", "warning", "error"]
    title: str
    body: str
    eta_minutes: int | None = None
    action: Literal["connect_ev", "replan", "import_prices", "switch_auto"] | None = None


class SiteNotificationsResponse(BaseModel):
    notifications: list[SiteNotificationItem] = Field(default_factory=list)

INV_STALE_SEC = 300
HEARTBEAT_STALE_SEC = 300
EXPECTED_TOMORROW_PRICE_SLOTS = 90


def _iso_utc(dt: datetime | str | None) -> str | None:
    # JSONB bundle z fn_site_full_status nese timestampy jako stringy — parsovat,
    # jinak .tzinfo na str = AttributeError → 500 celého /status/full.
    dt = _parse_ts(dt)
    if dt is None:
        return None
    if dt.tzinfo is None:
        dt = dt.replace(tzinfo=timezone.utc)
    return dt.astimezone(timezone.utc).isoformat()


def _parse_ts(val: Any) -> datetime | None:
    if val is None:
        return None
    if isinstance(val, datetime):
        return val
    if isinstance(val, str):
        return datetime.fromisoformat(val.replace("Z", "+00:00"))
    return None


def _age_seconds(at: datetime | None) -> int | None:
    if at is None:
        return None
    if at.tzinfo is None:
        at = at.replace(tzinfo=timezone.utc)
    return max(0, int((datetime.now(timezone.utc) - at).total_seconds()))


def _next_plan_interval(
    intervals: list[dict[str, Any]], now_utc: datetime
) -> tuple[str | None, int | None]:
    """Nejbližší 15min slot od aktuálního času včetně probíhajícího."""
    slot_ms = 15 * 60 * 1000
    boundary_ms = (int(now_utc.timestamp() * 1000) // slot_ms) * slot_ms
    boundary = datetime.fromtimestamp(boundary_ms / 1000, tz=timezone.utc)
    for row in sorted(intervals, key=lambda r: r["interval_start"]):
        istart = row["interval_start"]
        if isinstance(istart, str):
            istart = datetime.fromisoformat(istart.replace("Z", "+00:00"))
        if istart.tzinfo is None:
            istart = istart.replace(tzinfo=timezone.utc)
        if istart >= boundary - timedelta(milliseconds=1):
            bat = row.get("battery_setpoint_w")
            bi = int(bat) if bat is not None else None
            return _iso_utc(istart), bi
    return None, None


@router.get("/status/full")
async def get_site_status_full(
    site_id: int,
    pool: Annotated[asyncpg.Pool, Depends(get_pg_pool)],
) -> dict[str, Any]:
    async with pool.acquire() as conn:
        bundle = await fetch_json(
            conn,
            "select ems.fn_site_full_status($1::int)",
            site_id,
        )
    if not isinstance(bundle, dict):
        bundle = json.loads(bundle)
    if bundle.get("error") == "not_found":
        raise HTTPException(status_code=404, detail="Site not found")

    site = bundle.get("site") or {}
    mode_row = bundle.get("operating_mode") or {}
    hb_row = bundle.get("heartbeat") or {}
    inv_row = bundle.get("inverter_latest")
    if not isinstance(inv_row, dict):
        inv_row = None
    ev_rows = bundle.get("ev_chargers") or []
    if not isinstance(ev_rows, list):
        ev_rows = []
    hp_row = bundle.get("heat_pump_latest")
    if not isinstance(hp_row, dict):
        hp_row = None
    reserve_row = bundle.get("battery_limits") or {}
    run_row = bundle.get("active_plan")
    if not isinstance(run_row, dict):
        run_row = None
    intervals: list[dict[str, Any]] = []
    raw_iv = bundle.get("planning_intervals") or []
    if isinstance(raw_iv, list):
        intervals = [x for x in raw_iv if isinstance(x, dict)]

    tomorrow_slots = int(bundle.get("tomorrow_price_slot_count") or 0)

    now_utc = datetime.now(timezone.utc)
    hb_last = _parse_ts(hb_row.get("last_seen") if hb_row else None)
    hb_age = _age_seconds(hb_last)
    inv_measured = _parse_ts(inv_row.get("measured_at") if inv_row else None)
    inv_age = _age_seconds(inv_measured)

    next_start, next_bat = _next_plan_interval(intervals, now_utc)

    ev_list: list[dict[str, Any]] = []
    for r in ev_rows:
        if not isinstance(r, dict):
            continue
        ev_list.append(
            {
                "code": r.get("code"),
                "status": r.get("status"),
                "power_w": int(r["power_w"]) if r.get("power_w") is not None else None,
            }
        )

    telemetry: dict[str, Any] = {
        "inverter": {
            "pv_power_w": int(inv_row["pv_power_w"])
            if inv_row and inv_row.get("pv_power_w") is not None
            else None,
            "battery_soc_pct": float(inv_row["battery_soc_percent"])
            if inv_row and inv_row.get("battery_soc_percent") is not None
            else None,
            "grid_power_w": int(inv_row["grid_power_w"])
            if inv_row and inv_row.get("grid_power_w") is not None
            else None,
            "measured_at": _iso_utc(inv_measured),
            "age_seconds": inv_age,
        },
        "ev_chargers": ev_list,
        "heat_pump": {
            "power_w": int(hp_row["power_w"]) if hp_row and hp_row.get("power_w") is not None else None,
            "tank_temp_c": float(hp_row["tuv_tank_temp_c"])
            if hp_row and hp_row.get("tuv_tank_temp_c") is not None
            else None,
            "measured_at": _iso_utc(hp_row.get("measured_at")) if hp_row else None,
        },
    }

    has_plan = run_row is not None
    planning = {
        "has_active_plan": has_plan,
        "plan_created_at": _iso_utc(run_row.get("created_at")) if run_row else None,
        "next_interval_start": next_start,
        "next_battery_setpoint_w": next_bat,
    }

    mode_code = (mode_row.get("mode_code") if mode_row else None) or ""
    reserve_soc = (
        float(reserve_row["reserve_soc"])
        if reserve_row and reserve_row.get("reserve_soc") is not None
        else None
    )
    min_soc = (
        float(reserve_row["min_soc"]) if reserve_row and reserve_row.get("min_soc") is not None else None
    )
    soc = (
        float(inv_row["battery_soc_percent"])
        if inv_row and inv_row.get("battery_soc_percent") is not None
        else None
    )

    alerts: list[dict[str, str]] = []

    def add_alert(level: Literal["warn", "error"], message: str) -> None:
        alerts.append({"level": level, "message": message})

    if inv_age is None or inv_age > INV_STALE_SEC:
        add_alert("error", "Telemetrie střídače nedostupná")

    if not has_plan:
        add_alert("warn", "Není aktivní plán – EMS neoptimalizuje")

    # OTE D+1 typicky až po ~14:30 Europe/Prague – před tím nevarovat
    now_prague = datetime.now(ZoneInfo("Europe/Prague"))
    prices_expected = (now_prague.hour, now_prague.minute) >= (14, 30)
    if tomorrow_slots < EXPECTED_TOMORROW_PRICE_SLOTS and prices_expected:
        add_alert("warn", "Chybí spotové ceny pro zítřek")

    if mode_code.upper() == "MANUAL":
        add_alert("warn", "Systém v manuálním režimu")

    if min_soc is not None and soc is not None and soc < min_soc:
        add_alert("error", "SoC baterie pod minimálním limitem")
    elif reserve_soc is not None and soc is not None and soc < reserve_soc:
        add_alert("warn", "SoC pod ekonomickou rezervou (arbitrážní podlaha)")

    if hb_age is None or hb_age > HEARTBEAT_STALE_SEC:
        add_alert("error", "EMS heartbeat výpadek")

    alerts.sort(key=lambda a: (0 if a["level"] == "error" else 1, a["message"]))

    return {
        "site": {"id": site.get("id"), "code": site.get("code"), "name": site.get("name")},
        "operating_mode": {
            "mode_code": mode_row.get("mode_code") if mode_row else None,
            "mode_name": mode_row.get("mode_name") if mode_row else None,
            "activated_at": _iso_utc(mode_row.get("activated_at")) if mode_row else None,
            "activated_by": mode_row.get("activated_by") if mode_row else None,
        },
        "heartbeat": {
            "last_seen": _iso_utc(hb_last),
            "age_seconds": hb_age,
            "status": hb_row.get("status") if hb_row else None,
        },
        "telemetry": telemetry,
        "planning": planning,
        "alerts": alerts,
    }


_NOTIF_LEVEL_PRIORITY = {"error": 0, "success": 1, "warning": 2, "info": 3}


def _infrastructure_notification_items(
    *,
    has_plan: bool,
    tomorrow_slots: int,
    mode_code: str,
    min_soc: float | None,
    reserve_soc: float | None,
    soc: float | None,
    inv_age: int | None,
    hb_age: int | None,
) -> list[SiteNotificationItem]:
    """Kritické / provozní notifikace (telemetrie, plán, ceny, režim, heartbeat)."""
    items: list[SiteNotificationItem] = []

    def push(
        nid: str,
        level: Literal["success", "info", "warning", "error"],
        title: str,
        body: str,
        *,
        eta_minutes: int | None = None,
        action: Literal["connect_ev", "replan", "import_prices", "switch_auto"] | None = None,
    ) -> None:
        items.append(
            SiteNotificationItem(
                id=nid,
                level=level,
                title=title,
                body=body,
                eta_minutes=eta_minutes,
                action=action,
            )
        )

    if inv_age is None or inv_age > INV_STALE_SEC:
        push("telemetry_inverter", "error", "Telemetrie střídače", "Data ze střídače nejsou aktuální.")

    if not has_plan:
        push(
            "no_active_plan",
            "warning",
            "Chybí aktivní plán",
            "EMS zatím neoptimalizuje provoz – spusťte plánování.",
            action="replan",
        )

    now_prague = datetime.now(ZoneInfo("Europe/Prague"))
    prices_expected = (now_prague.hour, now_prague.minute) >= (14, 30)
    if tomorrow_slots < EXPECTED_TOMORROW_PRICE_SLOTS and prices_expected:
        push(
            "prices_tomorrow",
            "warning",
            "Ceny na zítřek",
            "Nejsou kompletní spotové ceny OTE pro následující den.",
            action="import_prices",
        )

    if mode_code.upper() == "MANUAL":
        push("mode_manual", "info", "Manuální režim", "Automatická optimalizace je vypnutá.")

    if min_soc is not None and soc is not None and soc < min_soc:
        push(
            "soc_min",
            "error",
            "SoC pod minimem",
            "SoC je pod absolutním minimem z konfigurace baterie.",
        )
    elif reserve_soc is not None and soc is not None and soc < reserve_soc:
        push(
            "soc_reserve",
            "warning",
            "SoC pod ekonomickou rezervou",
            "SoC je pod arbitrážní podlahou – plánovač může v tomto pásmu omezovat export.",
        )

    if hb_age is None or hb_age > HEARTBEAT_STALE_SEC:
        push("heartbeat", "error", "EMS heartbeat", "Služba EMS nehlásí pravidelný heartbeat.")

    return items


def _float_or_none(v: Any) -> float | None:
    if v is None:
        return None
    return float(v)


@router.get("/notifications", response_model=SiteNotificationsResponse)
async def get_site_notifications(
    site_id: int,
    pool: Annotated[asyncpg.Pool, Depends(get_pg_pool)],
) -> SiteNotificationsResponse:
    async with pool.acquire() as conn:
        ctx = await fetch_json(
            conn,
            "select ems.fn_site_notifications_context($1::int)",
            site_id,
        )
    if not isinstance(ctx, dict):
        ctx = json.loads(ctx)
    if ctx.get("error") == "not_found":
        raise HTTPException(status_code=404, detail="Site not found")

    has_plan = bool(ctx.get("has_plan"))
    mode_code = (ctx.get("mode_code") or "") or ""
    reserve_soc = _float_or_none(ctx.get("reserve_soc"))
    min_soc = _float_or_none(ctx.get("min_soc"))
    soc = _float_or_none(ctx.get("soc_pct"))
    inv_age = _age_seconds(_parse_ts(ctx.get("inv_measured_at")))
    hb_age = _age_seconds(_parse_ts(ctx.get("hb_last_seen")))
    tomorrow_slots = int(ctx.get("tomorrow_slots") or 0)

    price_rows = ctx.get("price_slots") or []
    if not isinstance(price_rows, list):
        price_rows = []

    avg_buy = _float_or_none(ctx.get("avg_buy"))
    usable_wh = _float_or_none(ctx.get("usable_wh"))

    ev_rows = ctx.get("ev_sessions") or []
    if not isinstance(ev_rows, list):
        ev_rows = []

    neg_rows = ctx.get("neg_windows") or []
    if not isinstance(neg_rows, list):
        neg_rows = []

    infra = _infrastructure_notification_items(
        has_plan=has_plan,
        tomorrow_slots=int(tomorrow_slots or 0),
        mode_code=mode_code,
        min_soc=min_soc,
        reserve_soc=reserve_soc,
        soc=soc,
        inv_age=inv_age,
        hb_age=hb_age,
    )

    prices: list[PriceSlot] = []
    for r in price_rows:
        if not isinstance(r, dict):
            continue
        buy = _float_or_none(r.get("effective_buy_price_czk_kwh"))
        if buy is None:
            continue
        sell_v = _float_or_none(r.get("effective_sell_price_czk_kwh"))
        istart = r.get("interval_start")
        if isinstance(istart, str):
            istart = datetime.fromisoformat(istart.replace("Z", "+00:00"))
        prices.append(
            PriceSlot(
                interval_start=istart,
                buy=buy,
                sell=sell_v if sell_v is not None else buy,
            )
        )

    battery_kwh = (usable_wh / 1000.0) if usable_wh is not None else None

    ev_sessions: list[EvSessionRow] = []
    for er in ev_rows:
        if not isinstance(er, dict):
            continue
        ss = er.get("session_start")
        if isinstance(ss, str):
            ss = datetime.fromisoformat(ss.replace("Z", "+00:00"))
        ev_sessions.append(
            EvSessionRow(
                id=int(er["id"]),
                charger_id=int(er["charger_id"]),
                energy_delivered_wh=float(er.get("energy_delivered_wh") or 0),
                target_soc_pct=_float_or_none(er.get("target_soc_pct")),
                session_start=ss,
                battery_capacity_kwh=_float_or_none(er.get("battery_capacity_kwh")),
                make=er.get("make"),
                model=er.get("model"),
                default_target_soc_pct=_float_or_none(er.get("default_target_soc_pct")),
                charger_code=str(er.get("charger_code") or ""),
                soc_at_connect_pct=_float_or_none(er.get("soc_at_connect_pct")),
            )
        )

    neg_windows: list[NegWindowRow] = []
    for nr in neg_rows:
        if not isinstance(nr, dict):
            continue
        dr = nr.get("predicted_date")
        if isinstance(dr, datetime):
            d_conv = dr.date()
        elif isinstance(dr, date):
            d_conv = dr
        elif isinstance(dr, str):
            d_conv = date.fromisoformat(dr[:10])
        else:
            d_conv = date.today()
        neg_windows.append(
            NegWindowRow(
                predicted_date=d_conv,
                window_start_hour=int(nr.get("window_start_hour") or 0),
                window_end_hour=int(nr.get("window_end_hour") or 0),
                probability_pct=int(nr.get("probability_pct") or 0),
            )
        )

    sell_now = prices[0].sell if prices else None

    smart_raw = build_smart_notifications(
        prices=prices,
        avg_buy=avg_buy,
        soc_pct=soc,
        battery_kwh=battery_kwh,
        ev_sessions=ev_sessions,
        neg_windows=neg_windows,
        mode=mode_code,
        sell_price_now=sell_now,
    )

    smart_items = [
        SiteNotificationItem(
            id=d["id"],
            level=d["level"],
            title=d["title"],
            body=d["body"],
            eta_minutes=d.get("eta_minutes"),
            action=d.get("action"),
        )
        for d in smart_raw
    ]

    merged = infra + smart_items
    merged.sort(key=lambda x: _NOTIF_LEVEL_PRIORITY.get(x.level, 9))
    return SiteNotificationsResponse(notifications=merged[:5])
