"""Local entitlement metering store for g-gremlin."""

from __future__ import annotations

import logging
import sqlite3
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any

from g_gremlin.paths import gremlin_path

log = logging.getLogger(__name__)

USAGE_DB_PATH = gremlin_path("usage.db")
CLOCK_STATE_KEY = "last_seen_utc"


@dataclass
class MeterReserveResult:
    allowed: bool
    meter_key: str
    period_key: str
    used: int
    cap: int | None
    reset_at: str | None


@dataclass
class MeterSummary:
    meter_key: str
    period: str
    period_key: str
    used: int
    cap: int | None
    reset_at: str | None


def _utc_now() -> datetime:
    return datetime.now(timezone.utc)


def _ensure_parent(path: Path) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)


def _init_schema(conn: sqlite3.Connection) -> None:
    conn.execute(
        """
        CREATE TABLE IF NOT EXISTS usage_meters (
            license_id TEXT NOT NULL,
            meter_key TEXT NOT NULL,
            period_key TEXT NOT NULL,
            used INTEGER NOT NULL DEFAULT 0,
            PRIMARY KEY (license_id, meter_key, period_key)
        )
        """
    )
    conn.execute(
        """
        CREATE TABLE IF NOT EXISTS clock_state (
            key TEXT PRIMARY KEY,
            value TEXT NOT NULL
        )
        """
    )


def _open_db(db_path: Path | None = None) -> sqlite3.Connection:
    path = db_path or USAGE_DB_PATH
    _ensure_parent(path)
    conn = sqlite3.connect(path, timeout=30.0)
    conn.execute("PRAGMA journal_mode=WAL")
    conn.execute("PRAGMA synchronous=NORMAL")
    conn.row_factory = sqlite3.Row
    _init_schema(conn)
    return conn


def _parse_iso_utc(value: str | None) -> datetime | None:
    if not value:
        return None
    try:
        parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
        if parsed.tzinfo is None:
            return parsed.replace(tzinfo=timezone.utc)
        return parsed.astimezone(timezone.utc)
    except Exception:
        return None


def _to_iso_utc(value: datetime) -> str:
    return value.astimezone(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")


def get_period_key(period: str, now_utc: datetime | None = None) -> str:
    now = now_utc or _utc_now()
    if period == "daily":
        return now.strftime("%Y-%m-%d")
    if period == "monthly":
        return now.strftime("%Y-%m")
    return "static"


def get_reset_at(period: str, now_utc: datetime | None = None) -> str | None:
    now = now_utc or _utc_now()
    if period == "daily":
        reset = now.replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1)
        return _to_iso_utc(reset)
    if period == "monthly":
        year = now.year + (1 if now.month == 12 else 0)
        month = 1 if now.month == 12 else now.month + 1
        reset = now.replace(year=year, month=month, day=1, hour=0, minute=0, second=0, microsecond=0)
        return _to_iso_utc(reset)
    return None


def _recover_if_corrupt(exc: sqlite3.DatabaseError, db_path: Path | None) -> None:
    path = db_path or USAGE_DB_PATH
    if not path.exists():
        return
    backup_path = path.with_suffix(path.suffix + ".corrupt")
    try:
        if backup_path.exists():
            backup_path.unlink(missing_ok=True)
        path.replace(backup_path)
        log.warning("Replaced corrupt usage DB at %s with backup %s (%s)", path, backup_path, exc)
    except Exception:
        log.warning("Failed to rotate corrupt usage DB at %s (%s)", path, exc)


def detect_clock_rollback(db_path: Path | None = None) -> datetime:
    system_now = _utc_now()
    try:
        with _open_db(db_path) as conn:
            row = conn.execute(
                "SELECT value FROM clock_state WHERE key = ?",
                (CLOCK_STATE_KEY,),
            ).fetchone()
            last_seen = _parse_iso_utc(row["value"]) if row else None
            effective_now = max(system_now, last_seen) if last_seen else system_now
            if last_seen and system_now < last_seen:
                log.warning(
                    "Detected system clock rollback: system=%s, last_seen=%s. Using last_seen.",
                    _to_iso_utc(system_now),
                    _to_iso_utc(last_seen),
                )
            conn.execute(
                "INSERT INTO clock_state(key, value) VALUES(?, ?) "
                "ON CONFLICT(key) DO UPDATE SET value = excluded.value",
                (CLOCK_STATE_KEY, _to_iso_utc(effective_now)),
            )
            conn.commit()
            return effective_now
    except sqlite3.DatabaseError as exc:
        _recover_if_corrupt(exc, db_path)
        with _open_db(db_path) as conn:
            conn.execute(
                "INSERT INTO clock_state(key, value) VALUES(?, ?) "
                "ON CONFLICT(key) DO UPDATE SET value = excluded.value",
                (CLOCK_STATE_KEY, _to_iso_utc(system_now)),
            )
            conn.commit()
        return system_now


def get_meter_usage(
    meter_key: str,
    period: str,
    license_id: str,
    db_path: Path | None = None,
    effective_now: datetime | None = None,
) -> int:
    checked_now = effective_now or detect_clock_rollback(db_path)
    period_key = get_period_key(period, checked_now)
    try:
        with _open_db(db_path) as conn:
            row = conn.execute(
                "SELECT used FROM usage_meters WHERE license_id = ? AND meter_key = ? AND period_key = ?",
                (license_id, meter_key, period_key),
            ).fetchone()
            return int(row["used"]) if row else 0
    except sqlite3.DatabaseError as exc:
        _recover_if_corrupt(exc, db_path)
        return 0


def check_headroom(
    meter_key: str,
    cap: int | None,
    period: str,
    license_id: str,
    db_path: Path | None = None,
) -> MeterReserveResult:
    effective_now = detect_clock_rollback(db_path)
    period_key = get_period_key(period, effective_now)
    used = get_meter_usage(
        meter_key,
        period,
        license_id,
        db_path,
        effective_now=effective_now,
    )
    if cap is None:
        return MeterReserveResult(
            allowed=True,
            meter_key=meter_key,
            period_key=period_key,
            used=used,
            cap=None,
            reset_at=get_reset_at(period, effective_now),
        )
    return MeterReserveResult(
        allowed=used < cap,
        meter_key=meter_key,
        period_key=period_key,
        used=used,
        cap=cap,
        reset_at=get_reset_at(period, effective_now),
    )


def reserve_meter(
    meter_key: str,
    cap: int | None,
    period: str,
    license_id: str,
    amount: int = 1,
    db_path: Path | None = None,
) -> MeterReserveResult:
    if amount < 1:
        raise ValueError("amount must be >= 1")

    effective_now = detect_clock_rollback(db_path)
    period_key = get_period_key(period, effective_now)
    reset_at = get_reset_at(period, effective_now)

    if cap is None:
        used = get_meter_usage(
            meter_key,
            period,
            license_id,
            db_path,
            effective_now=effective_now,
        )
        return MeterReserveResult(
            allowed=True,
            meter_key=meter_key,
            period_key=period_key,
            used=used,
            cap=None,
            reset_at=reset_at,
        )

    if cap <= 0:
        return MeterReserveResult(
            allowed=False,
            meter_key=meter_key,
            period_key=period_key,
            used=0,
            cap=cap,
            reset_at=reset_at,
        )

    try:
        with _open_db(db_path) as conn:
            conn.execute("BEGIN IMMEDIATE")
            row = conn.execute(
                "SELECT used FROM usage_meters WHERE license_id = ? AND meter_key = ? AND period_key = ?",
                (license_id, meter_key, period_key),
            ).fetchone()
            current_used = int(row["used"]) if row else 0
            next_used = current_used + amount
            if next_used > cap:
                conn.rollback()
                return MeterReserveResult(
                    allowed=False,
                    meter_key=meter_key,
                    period_key=period_key,
                    used=current_used,
                    cap=cap,
                    reset_at=reset_at,
                )

            conn.execute(
                """
                INSERT INTO usage_meters(license_id, meter_key, period_key, used)
                VALUES(?, ?, ?, ?)
                ON CONFLICT(license_id, meter_key, period_key)
                DO UPDATE SET used = used + excluded.used
                """,
                (license_id, meter_key, period_key, amount),
            )
            conn.commit()
            return MeterReserveResult(
                allowed=True,
                meter_key=meter_key,
                period_key=period_key,
                used=next_used,
                cap=cap,
                reset_at=reset_at,
            )
    except sqlite3.DatabaseError as exc:
        _recover_if_corrupt(exc, db_path)
        return MeterReserveResult(
            allowed=False,
            meter_key=meter_key,
            period_key=period_key,
            used=0,
            cap=cap,
            reset_at=reset_at,
        )


def record_volume(
    meter_key: str,
    amount: int,
    period: str,
    license_id: str,
    cap: int | None = None,
    db_path: Path | None = None,
) -> MeterReserveResult:
    if amount < 0:
        raise ValueError("amount must be >= 0")
    if amount == 0:
        return check_headroom(meter_key, cap, period, license_id, db_path)

    effective_now = detect_clock_rollback(db_path)
    period_key = get_period_key(period, effective_now)
    reset_at = get_reset_at(period, effective_now)

    try:
        with _open_db(db_path) as conn:
            conn.execute("BEGIN IMMEDIATE")
            row = conn.execute(
                "SELECT used FROM usage_meters WHERE license_id = ? AND meter_key = ? AND period_key = ?",
                (license_id, meter_key, period_key),
            ).fetchone()
            current_used = int(row["used"]) if row else 0
            next_used = current_used + amount
            conn.execute(
                """
                INSERT INTO usage_meters(license_id, meter_key, period_key, used)
                VALUES(?, ?, ?, ?)
                ON CONFLICT(license_id, meter_key, period_key)
                DO UPDATE SET used = used + excluded.used
                """,
                (license_id, meter_key, period_key, amount),
            )
            conn.commit()
            allowed = True if cap is None else next_used <= cap
            return MeterReserveResult(
                allowed=allowed,
                meter_key=meter_key,
                period_key=period_key,
                used=next_used,
                cap=cap,
                reset_at=reset_at,
            )
    except sqlite3.DatabaseError as exc:
        _recover_if_corrupt(exc, db_path)
        return MeterReserveResult(
            allowed=False,
            meter_key=meter_key,
            period_key=period_key,
            used=0,
            cap=cap,
            reset_at=reset_at,
        )


def get_all_meter_summaries(
    limit_caps: dict[str, int | None],
    limit_defs: dict[str, Any],
    license_id: str,
    db_path: Path | None = None,
) -> list[MeterSummary]:
    effective_now = detect_clock_rollback(db_path)
    summaries: list[MeterSummary] = []
    for meter_key, cap in limit_caps.items():
        limit_def = limit_defs.get(meter_key)
        period = getattr(limit_def, "period", None)
        if period is None and isinstance(limit_def, dict):
            period = str(limit_def.get("period") or "static")
        period = str(period or "static")
        period_key = get_period_key(period, effective_now)
        used = get_meter_usage(
            meter_key,
            period,
            license_id,
            db_path,
            effective_now=effective_now,
        )
        summaries.append(
            MeterSummary(
                meter_key=meter_key,
                period=period,
                period_key=period_key,
                used=used,
                cap=cap,
                reset_at=get_reset_at(period, effective_now),
            )
        )
    return summaries


def reset_meters_for_license(license_id: str, db_path: Path | None = None) -> None:
    try:
        with _open_db(db_path) as conn:
            conn.execute("DELETE FROM usage_meters WHERE license_id = ?", (license_id,))
            conn.commit()
    except sqlite3.DatabaseError as exc:
        _recover_if_corrupt(exc, db_path)
