"""One place for: provider selection by stage, schema-failure retry, failover, cost (M3/M5)."""
from __future__ import annotations
import time
from collections import deque
from typing import Type

from pydantic import BaseModel

from . import config
from .jobs import FatalError, RetryableError
from .providers import get_provider
from .providers.base import ImagePart, ProviderError, Result, SchemaError, TextPart, cost_micro

# recent failure timestamps per provider name (process-local circuit breaker)
_failures: dict[str, deque] = {}
FAILOVER_WINDOW_S = 300
FAILOVER_THRESHOLD = 3


def _note_failure(name: str) -> None:
    dq = _failures.setdefault(name, deque())
    now = time.monotonic()
    dq.append(now)
    while dq and now - dq[0] > FAILOVER_WINDOW_S:
        dq.popleft()


def _tripped(name: str) -> bool:
    dq = _failures.get(name)
    if not dq:
        return False
    now = time.monotonic()
    return sum(1 for t in dq if now - t <= FAILOVER_WINDOW_S) >= FAILOVER_THRESHOLD


def reset_breakers() -> None:
    _failures.clear()


def call(conn, *, stage: str, tenant_id: str | None, system: str,
         parts: list[TextPart | ImagePart], schema: Type[BaseModel], max_tokens: int = 8000) -> Result:
    """stage: 'extract' | 'score' | 'structure'. Raises RetryableError / FatalError for the job loop."""
    primary = {"provider": config.get(conn, f"ai.{stage}.provider", tenant_id),
               "model": config.get(conn, f"ai.{stage}.model", tenant_id)}
    fallback = config.get(conn, f"ai.{stage}.fallback", tenant_id)
    target = fallback if (fallback and _tripped(primary["provider"])) else primary
    provider = get_provider(target["provider"])

    attempt_parts = list(parts)
    for schema_try in range(2):          # one retry on schema failure, with the error fed back
        try:
            res = provider.structured(model=target["model"], system=system, parts=attempt_parts,
                                      schema=schema, max_tokens=max_tokens)
            break
        except SchemaError as e:
            if schema_try == 1:
                raise RetryableError(f"schema validation failed twice: {e}") from e
            attempt_parts = list(parts) + [TextPart(
                "Your previous output failed validation with this error:\n" + str(e)[:2000] +
                "\nReturn corrected output that satisfies the schema exactly.")]
        except ProviderError as e:
            _note_failure(target["provider"])
            if e.retryable:
                raise RetryableError(str(e)) from e
            raise FatalError(str(e)) from e
    pricing = config.get(conn, "ai.pricing", None) or {}
    res.cost_micro = cost_micro(pricing, res.model, res.tokens_in, res.tokens_out, res.cached_tokens)
    if target is not primary:
        res.extra["failover_from"] = primary
    return res
