"""Service layer for fetching and persisting Tibber 15-minute electricity prices.

``refresh_prices`` is the main entry point.  It is designed to be called from a
scheduled background job (see ``app/main.py``) and from tests.

Design decisions
----------------
- **Guard clause**: if no active contract with ``kind="tibber"`` exists, or if
  the Tibber API token is empty in the runtime settings, the function is a
  complete no-op (returns 0) and does not raise.  This means the scheduler can
  call ``refresh_prices`` unconditionally; the service itself decides whether to
  do anything based on current configuration.
- **Upsert idempotency**: rows are matched by ``starts_at`` (the unique
  constraint on ``tibber_price``).  If a row already exists, its price fields
  and ``fetched_at`` are updated in-place; if it does not exist, a new row is
  inserted.  Running ``refresh_prices`` twice in a row must not double-insert.
- **No destructive operations**: this service never deletes ``tibber_price``
  rows.  Only INSERT or UPDATE.
- **Exception propagation**: exceptions from the Tibber client are *not*
  swallowed here.  The scheduled job wrapper in ``main.py`` is responsible for
  catching and logging errors so that a single fetch failure does not crash the
  scheduler.
- **SQLite timezone note**: ``DateTime(timezone=True)`` columns come back as
  timezone-naive UTC datetimes on read.  We store UTC-aware datetimes on write
  (``starts_at`` from ``PricePoint`` is already UTC-aware; ``fetched_at`` is
  ``datetime.now(UTC)``).  Reads elsewhere that compare against these values
  must normalise accordingly.
"""

from __future__ import annotations

import logging
from datetime import UTC, datetime

from sqlalchemy import select
from sqlalchemy.orm import Session

from app.integrations.tibber.client import fetch_price_range
from app.models.energy import EnergyContract, TibberPrice

logger = logging.getLogger(__name__)


def _active_tibber_contract_exists(session: Session) -> bool:
    """Return True if there is an active contract with kind='tibber'."""
    row = session.execute(
        select(EnergyContract).where(
            EnergyContract.active.is_(True),
            EnergyContract.kind == "tibber",
        ).limit(1)
    ).scalar_one_or_none()
    return row is not None


def refresh_prices(session: Session, settings: object) -> int:
    """Fetch today-and-tomorrow Tibber prices and upsert them into ``tibber_price``.

    Parameters
    ----------
    session:
        An active SQLAlchemy session.  The caller is responsible for closing it;
        this function commits each upserted row individually to keep transactions
        short.
    settings:
        A runtime settings object that exposes ``tibber_api_token`` and
        ``tibber_home_id`` attributes (typically a ``Settings`` or merged
        runtime-settings instance from ``build_runtime_settings``).

    Returns
    -------
    int
        Number of rows upserted (inserted or updated).  Returns 0 for a no-op.

    Notes
    -----
    - No-op (returns 0) when:
        * No active contract exists with ``kind="tibber"``, OR
        * ``settings.tibber_api_token`` is empty or whitespace.
    - Exceptions from the Tibber client are *not* caught here; they propagate to
      the caller (the scheduled job wrapper swallows them and logs).
    """
    token: str = getattr(settings, "tibber_api_token", "") or ""
    if not token.strip():
        logger.debug("refresh_prices: tibber_api_token is empty — no-op")
        return 0

    if not _active_tibber_contract_exists(session):
        logger.debug("refresh_prices: no active tibber contract — no-op")
        return 0

    home_id: str = getattr(settings, "tibber_home_id", "") or ""
    home_id_or_none: str | None = home_id.strip() or None

    logger.info("refresh_prices: fetching Tibber price range (home_id=%r)", home_id_or_none)

    # May raise TibberError or TibberAuthError — let them propagate.
    price_points = fetch_price_range(token, home_id_or_none)

    if not price_points:
        logger.info("refresh_prices: API returned zero price points — nothing to upsert")
        return 0

    fetched_at = datetime.now(UTC)
    upserted = 0

    for point in price_points:
        existing = session.execute(
            select(TibberPrice).where(TibberPrice.starts_at == point.starts_at)
        ).scalar_one_or_none()

        if existing is not None:
            # Update in-place — price fields may change (e.g. Tibber corrects a
            # forecast), but starts_at and resolution stay the same.
            existing.total = point.total
            existing.energy = point.energy
            existing.tax = point.tax
            existing.level = point.level
            existing.currency = point.currency
            existing.fetched_at = fetched_at
        else:
            session.add(
                TibberPrice(
                    starts_at=point.starts_at,
                    resolution=point.resolution,
                    total=point.total,
                    energy=point.energy,
                    tax=point.tax,
                    level=point.level,
                    currency=point.currency,
                    fetched_at=fetched_at,
                )
            )

        upserted += 1

    session.commit()

    logger.info("refresh_prices: upserted %d price points", upserted)
    return upserted
