"""Tagesbudget je Backend (Anzahl Jobs, optional Token-Schätzung).

Ablauf: available() für die Routingentscheidung (reserviert nichts),
reserve() unmittelbar vor der Ausführung (atomar unter Dateisperre),
settle() danach: consumed=True verbucht, consumed=False gibt frei.
Reservierungen, die nie abgerechnet wurden (Absturz), zählen nach
reservation_timeout als verbraucht (fail-closed).
Tagesgrenze: UTC.
"""
from __future__ import annotations

import time
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Callable, Mapping, Optional

from .state import LockedJsonFile


@dataclass(frozen=True)
class BudgetLimit:
    daily_jobs: int
    daily_token_estimate: Optional[int] = None


def estimate_tokens(text_bytes: int) -> int:
    """Grobe Schätzung: 4 Bytes je Token, mindestens 1."""
    return max(1, (text_bytes + 3) // 4)


def utc_day(now: float) -> str:
    return datetime.fromtimestamp(now, tz=timezone.utc).date().isoformat()


class BudgetLedger:
    def __init__(self, path: Path, limits: Mapping[str, BudgetLimit],
                 clock: Callable[[], float] = time.time, reservation_timeout: int = 3600):
        self._file = LockedJsonFile(path)
        self._limits = dict(limits)
        self._clock = clock
        self._reservation_timeout = reservation_timeout

    def _prune(self, data: dict, now: float) -> None:
        reservations = data.setdefault("reservations", {})
        days = data.setdefault("days", {})
        for job_id in list(reservations):
            entry = reservations[job_id]
            if entry["ts"] + self._reservation_timeout < now:
                usage = days.setdefault(entry["day"], {}).setdefault(entry["backend"], {"jobs": 0, "tokens": 0})
                usage["jobs"] += 1
                usage["tokens"] += entry["tokens"]
                del reservations[job_id]
        cutoff = utc_day(now - 8 * 86400)
        for day in [d for d in days if d < cutoff]:
            del days[day]

    def _usage(self, data: dict, backend: str, day: str) -> tuple[int, int]:
        usage = data.get("days", {}).get(day, {}).get(backend, {"jobs": 0, "tokens": 0})
        jobs, tokens = usage["jobs"], usage["tokens"]
        for entry in data.get("reservations", {}).values():
            if entry["backend"] == backend and entry["day"] == day:
                jobs += 1
                tokens += entry["tokens"]
        return jobs, tokens

    def _fits(self, data: dict, backend: str, tokens: int, now: float) -> bool:
        limit = self._limits.get(backend)
        if limit is None:
            return False  # kein Budget konfiguriert -> nicht erlaubt
        jobs, used_tokens = self._usage(data, backend, utc_day(now))
        if jobs + 1 > limit.daily_jobs:
            return False
        if limit.daily_token_estimate is not None and used_tokens + tokens > limit.daily_token_estimate:
            return False
        return True

    def available(self, backend: str, tokens: int = 1) -> bool:
        now = self._clock()
        data = self._file.read_snapshot()
        return self._fits(data, backend, tokens, now)

    def reserve(self, backend: str, job_id: str, tokens: int) -> bool:
        now = self._clock()
        with self._file.transaction() as data:
            self._prune(data, now)
            if job_id in data["reservations"]:
                return False
            if not self._fits(data, backend, tokens, now):
                return False
            data["reservations"][job_id] = {
                "backend": backend, "tokens": int(tokens), "day": utc_day(now), "ts": now,
            }
            return True

    def settle(self, job_id: str, *, consumed: bool) -> None:
        now = self._clock()
        with self._file.transaction() as data:
            self._prune(data, now)
            entry = data["reservations"].pop(job_id, None)
            if entry is None or not consumed:
                return
            usage = data["days"].setdefault(entry["day"], {}).setdefault(
                entry["backend"], {"jobs": 0, "tokens": 0})
            usage["jobs"] += 1
            usage["tokens"] += entry["tokens"]

    def usage_today(self, backend: str) -> dict:
        now = self._clock()
        data = self._file.read_snapshot()
        jobs, tokens = self._usage(data, backend, utc_day(now))
        limit = self._limits.get(backend)
        return {
            "jobs": jobs,
            "tokens": tokens,
            "daily_jobs": limit.daily_jobs if limit else 0,
            "daily_token_estimate": limit.daily_token_estimate if limit else None,
        }
