"""§6.6 per-RFC model availability — the resolver, extended in §6.7
with the funder-universe layer.

The meta-repo entry's optional `models:` frontmatter and the operator's
provisioned providers (the §18 `ENABLED_MODELS` universe) combine into a
single resolved list per RFC. Every AI surface picks against that list:
the §8.12 chat picker, the §10.2 PR-draft, the §8.13 flag-resolution
invocation, the §9.1 tag suggestions when graduation is in scope.

Rule, in one place:

- The base universe is normally the operator's provisioned providers.
  When a consenting funder is in effect for the RFC per §6.7, the base
  universe is replaced (not augmented) by the funder's registered
  universe — the subset of operator-enabled picker keys the funder has
  supplied credentials for.
- Cache row's `models_json` is NULL → the field is absent on the entry.
  Resolved list = the base universe.
- Cache row's `models_json` is a JSON array → the field is set on the
  entry. Resolved list = intersection of the array with the base
  universe, preserving the entry's stated order.

The empty case folds in naturally: `models: []` yields empty, an entry
whose listed models aren't in the operator's enabled set yields empty,
a consenting funder whose registrations don't intersect the operator's
enabled set yields empty. Callers treat all four the same — refuse
cleanly per §6.6.

The function is slug-aware and provider-aware; it does not depend on
the FastAPI request lifecycle, which keeps it cheap to call inside any
endpoint that knows the slug and has the providers dict in scope.
"""
from __future__ import annotations

import json

from . import db, funder
from .providers import BaseProvider


def _models_from_config(config_json: str | None) -> list[str] | None:
    """The `enabled_models` list inside a project/collection `config_json`,
    or None when the key is absent (meaning "no narrowing at this tier")."""
    if not config_json:
        return None
    try:
        cfg = json.loads(config_json)
    except (json.JSONDecodeError, TypeError):
        return None
    em = cfg.get("enabled_models") if isinstance(cfg, dict) else None
    return [str(m) for m in em] if isinstance(em, list) else None


def _narrow(universe: list[str], allowed: list[str] | None) -> list[str]:
    """Intersect `universe` with `allowed`, preserving universe order. `allowed`
    None means no narrowing at this tier; an empty list narrows to empty (an
    opt-out), exactly like the §6.6 per-entry `models: []`."""
    if allowed is None:
        return universe
    allow = set(allowed)
    return [k for k in universe if k in allow]


def _scope_narrowed_universe(
    collection_id: str | None, operator_keys: list[str]
) -> list[str]:
    """§22.12 — narrow the operator (deployment) universe by the entry's
    project then its collection `enabled_models`. Each tier may only narrow;
    a missing config at a tier is a no-op. The collection cannot widen its
    project because narrowing composes from the operator ceiling downward."""
    if collection_id is None:
        return list(operator_keys)
    conn = db.conn()
    crow = conn.execute(
        "SELECT project_id, config_json FROM collections WHERE id = ?",
        (collection_id,),
    ).fetchone()
    universe = list(operator_keys)
    if crow is None:
        return universe
    prow = conn.execute(
        "SELECT config_json FROM projects WHERE id = ?", (crow["project_id"],)
    ).fetchone()
    universe = _narrow(universe, _models_from_config(prow["config_json"] if prow else None))
    universe = _narrow(universe, _models_from_config(crow["config_json"]))
    return universe


def resolve_models_for_rfc(
    slug: str, providers: dict[str, BaseProvider]
) -> list[str]:
    """Return the per-RFC resolved model keys per §6.6, extended by §6.7 and
    §22.12.

    The first entry is the RFC's default model. An empty list means
    AI is unavailable on this RFC and callers refuse the AI surface.
    """
    row = db.conn().execute(
        "SELECT collection_id, models_json FROM cached_rfcs WHERE slug = ?",
        (slug,),
    ).fetchone()
    collection_id = row["collection_id"] if row is not None else None
    # §22.12: first narrow the operator universe by the entry's project +
    # collection enabled_models (the deployment → project → collection chain).
    scope_universe = _scope_narrowed_universe(collection_id, list(providers.keys()))
    # §6.7: a consenting funder universe (if any) replaces the operator universe
    # as the base set — still bounded by the §22.12 scope narrowing above.
    funder_universe = funder.resolve_funder_universe(slug, providers)
    if funder_universe is not None:
        base_universe = _narrow(list(funder_universe), scope_universe)
    else:
        base_universe = scope_universe
    if row is None or row["models_json"] is None:
        return list(base_universe)
    try:
        listed = [str(m) for m in json.loads(row["models_json"])]
    except (json.JSONDecodeError, TypeError):
        return list(base_universe)
    if not listed:
        return []
    base_set = set(base_universe)
    return [m for m in listed if m in base_set]


def default_model_for_rfc(
    slug: str, providers: dict[str, BaseProvider]
) -> str:
    """The first entry in the resolved list, or empty string if none.

    Callers that need a deterministic single choice — §10.2's draft,
    §9.1's tag suggestions — use this. An empty return signals the
    refusal path per §6.6.
    """
    resolved = resolve_models_for_rfc(slug, providers)
    return resolved[0] if resolved else ""
