"""Service layer for EnergyContract CRUD, versioning, and activation.

All functions accept an explicit SQLAlchemy Session; callers are responsible
for committing or rolling back the transaction.

Design decisions
----------------
- ``create_contract``:   validates values against the pricing profile *before*
  writing any rows; raises ``ProfileValidationError`` on non-compliance.
- ``add_version``:       append-only; closes the previous open version by
  setting its ``effective_to`` to the new version's ``effective_from``; raises
  ``ContractVersionError`` if the new date is strictly earlier than the previous
  version's ``effective_from``.
- ``activate_contract``: mutual-exclusion; sets all other contracts' ``active``
  to False, then sets the given contract's ``active`` to True.
- ``active_contract_version_at``: returns the single version of the currently
  active contract that covers *ts* (``effective_from ≤ ts < effective_to``,
  or open-ended when ``effective_to`` is None).

SQLite timezone note
--------------------
SQLite stores ``DateTime(timezone=True)`` columns as naive UTC strings; on
read-back they come out as **timezone-naive** datetimes.  Wherever this code
compares timestamps from the DB against timezone-aware values (e.g. from
Pydantic or ``datetime.now(UTC)``), it calls ``_as_utc()`` to make both sides
comparable without tripping on "offset-naive vs offset-aware" TypeErrors.
"""

from __future__ import annotations

import logging
from datetime import UTC, datetime
from typing import Any

from sqlalchemy import select
from sqlalchemy.orm import Session

from app.integrations.pricing.profiles import validate_values
from app.models.energy import EnergyContract, EnergyContractVersion

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------


def _as_utc(dt: datetime) -> datetime:
    """Return *dt* as a timezone-aware UTC datetime.

    SQLite's DateTime(timezone=True) column type stores datetimes as naive UTC
    strings and gives them back as naive datetimes on read.  This helper
    re-attaches the UTC timezone info when it is missing, making cross-origin
    comparisons safe.
    """
    if dt.tzinfo is None:
        return dt.replace(tzinfo=UTC)
    return dt


# ---------------------------------------------------------------------------
# Custom exception
# ---------------------------------------------------------------------------


class ContractVersionError(ValueError):
    """Raised when a new contract version has an invalid effective date.

    Specifically: the new version's ``effective_from`` must be greater than or
    equal to the previous open version's ``effective_from``.  Allowing equal
    timestamps would cause ambiguous overlap; the service therefore also rejects
    strictly-equal values (same second) to avoid silent data loss.
    """


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def get_contract_or_none(session: Session, contract_id: int) -> EnergyContract | None:
    """Return the contract with the given id, or None if not found."""
    return session.execute(
        select(EnergyContract).where(EnergyContract.id == contract_id)
    ).scalar_one_or_none()


def list_contracts(session: Session) -> list[EnergyContract]:
    """Return all contracts ordered by id (ascending)."""
    return list(
        session.execute(select(EnergyContract).order_by(EnergyContract.id)).scalars().all()
    )


def _open_version(session: Session, contract: EnergyContract) -> EnergyContractVersion | None:
    """Return the current open version (effective_to IS NULL) for *contract*, or None."""
    return session.execute(
        select(EnergyContractVersion)
        .where(
            EnergyContractVersion.contract_id == contract.id,
            EnergyContractVersion.effective_to.is_(None),
        )
        .order_by(EnergyContractVersion.effective_from.desc())
        .limit(1)
    ).scalar_one_or_none()


# ---------------------------------------------------------------------------
# Core service functions
# ---------------------------------------------------------------------------


def create_contract(
    session: Session,
    *,
    name: str,
    kind: str,
    currency: str = "EUR",
    values: dict[str, Any],
    effective_from: datetime,
) -> EnergyContract:
    """Create a new energy contract with its first pricing version.

    Parameters
    ----------
    session:
        Active SQLAlchemy session.  Caller must commit after this returns.
    name:
        Human-readable label for the contract.
    kind:
        Pricing strategy identifier (``"manual"`` or ``"tibber"``).
    currency:
        ISO 4217 currency code (default ``"EUR"``).
    values:
        Pricing values dict conforming to the named profile's structure.
        Validated via ``validate_values(kind, values)`` before any writes.
    effective_from:
        UTC datetime at which the first pricing version takes effect.

    Returns
    -------
    EnergyContract
        The newly created contract (not yet committed).

    Raises
    ------
    ProfileNotFoundError
        If no YAML profile exists for *kind*.
    ProfileValidationError
        If *values* does not conform to the profile structure.
    """
    # Validate (and fill defaults) before any DB write.
    filled_values = validate_values(kind, values)

    now = datetime.now(UTC)
    contract = EnergyContract(
        name=name,
        kind=kind,
        currency=currency,
        active=False,  # New contracts are inactive; caller must explicitly activate.
        created_at=now,
        updated_at=now,
    )
    session.add(contract)
    session.flush()  # Assign contract.id so we can reference it in the version FK.

    version = EnergyContractVersion(
        contract_id=contract.id,
        effective_from=effective_from,
        effective_to=None,
        values=filled_values,
        created_at=now,
    )
    session.add(version)

    logger.info("Created energy contract %r (kind=%s, id=%d)", name, kind, contract.id)
    return contract


def add_version(
    session: Session,
    contract: EnergyContract,
    *,
    effective_from: datetime,
    values: dict[str, Any],
) -> EnergyContractVersion:
    """Add a new pricing version to an existing contract (append-only).

    The previous open version's ``effective_to`` is automatically set to the
    new version's ``effective_from`` (version closure), ensuring there is never
    a gap or overlap between consecutive versions.

    The new ``effective_from`` **must be strictly greater than** the previous
    open version's ``effective_from``.  Equal timestamps are rejected because
    they would produce two versions starting at the same instant, making it
    impossible to determine which is current.  If this constraint is not met,
    ``ContractVersionError`` is raised and **no rows are written**.

    Parameters
    ----------
    session:
        Active SQLAlchemy session.  Caller must commit after this returns.
    contract:
        The parent ``EnergyContract`` to append a version to.
    effective_from:
        UTC datetime at which this version's pricing takes effect.
    values:
        New pricing values dict conforming to the contract's profile.

    Returns
    -------
    EnergyContractVersion
        The newly created version (not yet committed).

    Raises
    ------
    ContractVersionError
        If *effective_from* is not strictly after the previous open version's
        ``effective_from``.
    ProfileNotFoundError
        If no YAML profile exists for the contract's kind.
    ProfileValidationError
        If *values* does not conform to the profile structure.
    """
    # Validate (and fill defaults) before any DB write.
    filled_values = validate_values(contract.kind, values)

    prev = _open_version(session, contract)
    if prev is not None:
        # Enforce strictly-after constraint to prevent ambiguous overlap.
        # Use _as_utc() on both sides: the incoming value may be tz-aware
        # while the DB-read value is tz-naive (SQLite limitation).
        if _as_utc(effective_from) <= _as_utc(prev.effective_from):
            raise ContractVersionError(
                f"New version effective_from ({effective_from.isoformat()}) must be strictly "
                f"after the previous open version's effective_from "
                f"({prev.effective_from.isoformat()})."
            )
        # Close the previous open version.
        prev.effective_to = effective_from

    now = datetime.now(UTC)
    new_version = EnergyContractVersion(
        contract_id=contract.id,
        effective_from=effective_from,
        effective_to=None,
        values=filled_values,
        created_at=now,
    )
    session.add(new_version)

    logger.info(
        "Added version to contract id=%d (kind=%s, effective_from=%s)",
        contract.id,
        contract.kind,
        effective_from.isoformat(),
    )
    return new_version


def activate_contract(session: Session, contract: EnergyContract) -> None:
    """Activate a contract with mutual exclusion.

    Sets every other contract's ``active`` flag to False, then sets the given
    contract's ``active`` to True.  This guarantees at most one active contract
    at any time.

    Caller must commit after this returns.
    """
    # Deactivate all contracts (including the target; we re-activate below).
    for other in session.execute(select(EnergyContract)).scalars().all():
        other.active = False
    contract.active = True
    contract.updated_at = datetime.now(UTC)
    logger.info("Activated contract %r (id=%d)", contract.name, contract.id)


def deactivate_contract(session: Session, contract: EnergyContract) -> None:
    """Deactivate a contract without touching other contracts.

    Caller must commit after this returns.
    """
    contract.active = False
    contract.updated_at = datetime.now(UTC)
    logger.info("Deactivated contract %r (id=%d)", contract.name, contract.id)


def active_contract_versions(session: Session) -> list[EnergyContractVersion]:
    """Return all versions of the currently active contract, ordered by effective_from ascending.

    Returns an empty list when there is no active contract.  The list spans the
    full history of the active contract (all closed + the current open version)
    and is used to iterate over pricing-rate segments for cross-version fixed-
    cost / credit accumulation (Principle C).
    """
    active = session.execute(
        select(EnergyContract).where(EnergyContract.active.is_(True)).limit(1)
    ).scalar_one_or_none()

    if active is None:
        return []

    return list(
        session.execute(
            select(EnergyContractVersion)
            .where(EnergyContractVersion.contract_id == active.id)
            .order_by(EnergyContractVersion.effective_from)
        )
        .scalars()
        .all()
    )


def active_contract_version_at(
    session: Session, ts: datetime
) -> EnergyContractVersion | None:
    """Return the active contract's version that covers *ts*.

    A version covers *ts* when:
        ``effective_from ≤ ts`` AND (``effective_to IS NULL`` OR ``ts < effective_to``)

    Returns None when:
    - There is no active contract.
    - The active contract has no version covering *ts*.

    Parameters
    ----------
    session:
        Active SQLAlchemy session (read-only usage).
    ts:
        UTC datetime to look up.

    Returns
    -------
    EnergyContractVersion | None
    """
    active = session.execute(
        select(EnergyContract).where(EnergyContract.active.is_(True)).limit(1)
    ).scalar_one_or_none()

    if active is None:
        return None

    # Build a query for all versions of the active contract covering ts.
    stmt = (
        select(EnergyContractVersion)
        .where(
            EnergyContractVersion.contract_id == active.id,
            EnergyContractVersion.effective_from <= ts,
        )
        .order_by(EnergyContractVersion.effective_from.desc())
        .limit(1)
    )
    version = session.execute(stmt).scalar_one_or_none()

    if version is None:
        return None

    # Exclude versions whose effective window has already closed before ts.
    if version.effective_to is not None and _as_utc(ts) >= _as_utc(version.effective_to):
        return None

    return version
