"""Pydantic schemas for the Energy data API (M6-T09).

Covers six endpoint groups under /api/energy:
  - GET  /prices             — price curve (tibber 15min points or manual tariff)
  - GET  /costs              — energy_cost_period rows (time-range, paginated)
  - GET  /costs/summary      — aggregated metered + standing charges − credits
  - GET  /dsmr/latest        — most recent dsmr_reading blob
  - POST /costs/recompute    — explicit idempotent recompute (returns count)
  - POST /tibber/test        — three-state Tibber connection test
"""

from __future__ import annotations

from datetime import datetime
from typing import Any, Literal

from pydantic import BaseModel, Field


# ---------------------------------------------------------------------------
# GET /api/energy/prices
# ---------------------------------------------------------------------------


class PricePointSchema(BaseModel):
    """A single 15-minute price point (tibber) or placeholder entry."""

    starts_at: datetime
    buy: float = Field(description="All-in buy price in EUR/kWh (including taxes).")
    sell: float = Field(description="Net sell price in EUR/kWh.")
    level: str | None = Field(
        default=None,
        description="Tibber price level (CHEAP / NORMAL / EXPENSIVE); null for manual.",
    )


class ManualTariffSchema(BaseModel):
    """Fixed-tariff breakdown for manual contracts.

    Prices are the *effective* buy prices as used by the billing engine
    (energy_buy_x + energy_tax + ode) and the raw sell prices.
    """

    buy_dal: float = Field(description="Effective buy price, low-tariff / dal (EUR/kWh).")
    buy_normal: float = Field(description="Effective buy price, normal / high-tariff (EUR/kWh).")
    sell_dal: float = Field(description="Sell price, low-tariff / dal (EUR/kWh).")
    sell_normal: float = Field(description="Sell price, normal / high-tariff (EUR/kWh).")


class PricesResponse(BaseModel):
    """Response for GET /api/energy/prices.

    ``kind`` mirrors the active contract kind:
    - ``"tibber"``  → ``points`` has actual 15-min price entries; ``tariff`` is null.
    - ``"manual"``  → ``points`` is empty; ``tariff`` carries the fixed-rate table.
    - ``None``      → no active contract; both ``points`` and ``tariff`` are empty/null.

    ``currency`` comes from the active contract (or "EUR" fallback).
    ``points`` is always ascending by ``starts_at``.
    """

    kind: str | None = Field(
        description="Active contract kind ('tibber' or 'manual'), or null if no active contract."
    )
    currency: str = Field(description="ISO 4217 currency code.")
    points: list[PricePointSchema] = Field(
        description=(
            "15-minute price points for tibber contracts (ascending by starts_at). "
            "Empty for manual contracts or when no active contract exists."
        )
    )
    tariff: ManualTariffSchema | None = Field(
        default=None,
        description=(
            "Fixed tariff table for manual contracts. "
            "Null for tibber contracts and when no active contract exists."
        ),
    )


# ---------------------------------------------------------------------------
# GET /api/energy/costs
# ---------------------------------------------------------------------------


class CostPeriodSchema(BaseModel):
    """One 15-minute billing record from the energy_cost_period table."""

    period_start: datetime
    d1_kwh: float = Field(description="Delivered low-tariff kWh for this period.")
    d2_kwh: float = Field(description="Delivered normal-tariff kWh for this period.")
    r1_kwh: float = Field(description="Returned low-tariff kWh for this period.")
    r2_kwh: float = Field(description="Returned normal-tariff kWh for this period.")
    import_cost: float = Field(description="Cost of electricity drawn from grid (EUR).")
    export_revenue: float = Field(description="Revenue from electricity fed to grid (EUR).")
    net_cost: float = Field(description="import_cost − export_revenue (EUR).")
    currency: str = Field(description="ISO 4217 currency code.")
    degraded: bool = Field(
        description="True when the period was computed with incomplete data."
    )
    contract_version_id: int | None = Field(
        default=None,
        description="FK to the contract version used for this billing period (null when degraded).",
    )

    model_config = {"from_attributes": True}


class CostsResponse(BaseModel):
    """Response for GET /api/energy/costs."""

    items: list[CostPeriodSchema]
    total: int = Field(description="Number of items returned.")


# ---------------------------------------------------------------------------
# GET /api/energy/costs/summary
# ---------------------------------------------------------------------------


class SummaryResponse(BaseModel):
    """Response for GET /api/energy/costs/summary.

    All monetary values are in ``currency``.

    ``total_payable = metered_net + fixed_costs − credits``
    """

    currency: str
    metered_import: float = Field(description="Σ import_cost for non-degraded periods.")
    metered_export: float = Field(description="Σ export_revenue for non-degraded periods.")
    metered_net: float = Field(description="Σ net_cost for non-degraded periods.")
    fixed_costs: float = Field(
        description="Standing charges (network_fee + management_fee) apportioned over the interval."
    )
    credits: float = Field(
        description="Energy-tax credit (heffingskorting) apportioned over the interval."
    )
    total_payable: float = Field(
        description="metered_net + fixed_costs − credits (actual amount owed)."
    )
    period_count: int = Field(description="Number of non-degraded billing periods in range.")
    degraded_count: int = Field(description="Number of degraded billing periods in range.")
    days: float = Field(description="Interval length in days.")


# ---------------------------------------------------------------------------
# GET /api/energy/dsmr/latest
# ---------------------------------------------------------------------------


class DsmrLatestResponse(BaseModel):
    """Response for GET /api/energy/dsmr/latest.

    ``found`` is False when no dsmr_reading rows exist yet.  The front-end
    should check ``found`` before reading ``recorded_at`` or ``payload``.
    """

    found: bool
    recorded_at: datetime | None = None
    payload: dict[str, Any] | None = None


# ---------------------------------------------------------------------------
# POST /api/energy/costs/recompute
# ---------------------------------------------------------------------------


class RecomputeResponse(BaseModel):
    """Response for POST /api/energy/costs/recompute."""

    recomputed: int = Field(
        description=(
            "Number of 15-minute periods for which a billing record was written "
            "(inserted or updated).  Periods skipped due to missing contract or "
            "missing Tibber price are not counted."
        )
    )


# ---------------------------------------------------------------------------
# POST /api/energy/tibber/test
# ---------------------------------------------------------------------------


class TibberTestPriceSchema(BaseModel):
    """Current Tibber price point returned on a successful test.

    Carries enough fields for the front-end to confirm the API is working and
    display the live price.  The API token is **never** included.
    """

    starts_at: datetime
    total: float
    energy: float
    tax: float
    currency: str
    level: str | None = None


class TibberTestResponse(BaseModel):
    """Three-state response for POST /api/energy/tibber/test.

    Possible ``result`` values:

    - ``"success"``      — Tibber API responded with a valid price.
    - ``"config-error"`` — Token is missing or not configured.
    - ``"failed"``       — API call failed (auth rejected, network error, timeout, etc.).

    ``price`` is populated only on ``"success"``; it is null otherwise.
    ``message`` always contains a human-readable explanation.
    """

    result: Literal["success", "config-error", "failed"]
    message: str
    price: TibberTestPriceSchema | None = None
