"""§6.7: per-RFC credential delegation — the funder role.

Three responsibilities live here:

  1. The runtime resolver. Given a (slug, picker_key) tuple, return the
     `BaseProvider` instance that should serve calls. When no consenting
     funder is in effect, this is the operator's pre-built provider.
     When a consenting funder is named on the RFC and has registered
     credentials for the picker_key's provider family, a fresh provider
     instance is constructed with the funder's credentials.

  2. The universe resolver. `resolve_funder_universe(slug, operator)`
     returns the funder's per-RFC universe — the subset of operator-
     enabled picker keys served by provider families the funder has
     registered. Returns None when no funder is in effect; an empty
     list when a consenting funder has no registrations intersecting
     the operator's enabled set.

  3. The consent + credential CRUD. Small helpers that the §17
     `/api/users/me/funder` endpoints in api.py call into.

The lighter half (this module) deliberately does NOT handle:

  - Mid-call failure with fallback. Per §6.7, the resolution is
    attribution-clean: a consenting-funder RFC is paid entirely from
    funder credentials. If construction fails or the call fails,
    that's a failure — no silent fallback to operator credentials.
  - Per-instance caching. Constructs fresh on every call. The cost is
    real (HTTP client setup) but bounded; caching across calls is
    deferred until evidence shows the cost bites.
  - Billing attribution surface, rotation ceremony, rate-limit
    attribution. All deferred to the operational-realities §19.2
    candidate.
"""
from __future__ import annotations

from . import db, providers as providers_mod
from .providers import BaseProvider


# ---------------------------------------------------------------------------
# Universe + provider resolution
# ---------------------------------------------------------------------------


def resolve_funder_universe(
    slug: str, operator_providers: dict[str, BaseProvider]
) -> list[str] | None:
    """The funder's registered universe for this RFC, or None.

    None means no consenting funder is in effect — caller falls back to
    the operator universe per §6.7. A list (possibly empty) means a
    consenting funder is in effect; the list is the subset of operator-
    enabled picker keys for which the funder has supplied a provider-
    family API key. An empty list means the named-and-consenting
    funder's registered providers don't intersect the operator's
    enabled set — the RFC drops into the §6.6 opt-out shape.
    """
    funder_user_id = consenting_funder_user_id(slug)
    if funder_user_id is None:
        return None
    registered = registered_provider_families(funder_user_id)
    return [
        picker_key
        for picker_key in operator_providers
        if (family := providers_mod.provider_family_for_picker_key(picker_key))
        and family in registered
    ]


def provider_for_rfc(
    slug: str,
    picker_key: str,
    operator_providers: dict[str, BaseProvider],
) -> BaseProvider | None:
    """The `BaseProvider` that should serve calls for (slug, picker_key).

    When a consenting funder is named on the RFC and has registered
    credentials for picker_key's provider family, returns a fresh
    funder-credentials provider instance. Otherwise returns the
    operator's pre-built provider. Returns None when picker_key isn't
    available in the resolved universe — caller refuses per §6.6/§6.7.
    """
    funder_user_id = consenting_funder_user_id(slug)
    if funder_user_id is None:
        return operator_providers.get(picker_key)
    family = providers_mod.provider_family_for_picker_key(picker_key)
    if family is None:
        return None
    row = db.conn().execute(
        "SELECT api_key FROM user_funder_credentials WHERE user_id = ? AND provider = ?",
        (funder_user_id, family),
    ).fetchone()
    if not row:
        return None
    return providers_mod.construct_for_funder(picker_key, row["api_key"])


# ---------------------------------------------------------------------------
# Consenting-funder lookup (the two-key §6.7 rule)
# ---------------------------------------------------------------------------


def consenting_funder_user_id(slug: str) -> int | None:
    """Resolve the consenting funder for an RFC, or None.

    The two-key rule per §6.7: `cached_rfcs.funder_login` names a user
    AND a `funder_consents` row exists for the (user, slug) pair. The
    third revocation path (account-disabled) is a structural commitment
    — when the disable affordance lands per §6.1, it joins this check
    as a third AND clause; until then, the two-key rule stands alone.
    """
    row = db.conn().execute(
        """
        SELECT u.id AS user_id
          FROM cached_rfcs r
          JOIN users u ON u.gitea_login = r.funder_login
         WHERE r.slug = ? AND r.funder_login IS NOT NULL
        """,
        (slug,),
    ).fetchone()
    if not row:
        return None
    consent = db.conn().execute(
        "SELECT 1 FROM funder_consents WHERE user_id = ? AND rfc_slug = ?",
        (row["user_id"], slug),
    ).fetchone()
    if not consent:
        return None
    return row["user_id"]


# ---------------------------------------------------------------------------
# Credential + consent CRUD (called by the §17 funder endpoints)
# ---------------------------------------------------------------------------


def registered_provider_families(user_id: int) -> set[str]:
    """The provider families this user has registered credentials for."""
    rows = db.conn().execute(
        "SELECT provider FROM user_funder_credentials WHERE user_id = ?",
        (user_id,),
    ).fetchall()
    return {r["provider"] for r in rows}


def list_credentials(user_id: int) -> list[dict[str, str]]:
    """Per-provider registration metadata for the funder surface.

    The API key itself is never returned to the client. The list shape
    is one row per registered family, with the registration timestamp
    for the settings page's display.
    """
    rows = db.conn().execute(
        """
        SELECT provider, created_at, updated_at
          FROM user_funder_credentials
         WHERE user_id = ?
         ORDER BY provider
        """,
        (user_id,),
    ).fetchall()
    return [
        {"provider": r["provider"], "created_at": r["created_at"], "updated_at": r["updated_at"]}
        for r in rows
    ]


def upsert_credential(user_id: int, provider: str, api_key: str) -> None:
    """Register or replace an API key for a provider family.

    Idempotent. Callers gate the provider against
    `providers_mod.FUNDER_PROVIDER_FAMILIES` and against the operator's
    enabled set (per §6.7, the funder cannot expand the operator
    universe) before calling here.
    """
    db.conn().execute(
        """
        INSERT INTO user_funder_credentials (user_id, provider, api_key)
        VALUES (?, ?, ?)
        ON CONFLICT(user_id, provider) DO UPDATE SET
            api_key = excluded.api_key,
            updated_at = datetime('now')
        """,
        (user_id, provider, api_key),
    )


def delete_credential(user_id: int, provider: str) -> bool:
    """Remove a registered key. Returns True if a row was deleted.

    Existing `funder_consents` rows are intentionally left in place —
    the resolved funder universe for affected RFCs simply shrinks. If
    the funder later re-registers the family, those RFCs resume
    drawing on the funder's universe without re-consenting.
    """
    cur = db.conn().execute(
        "DELETE FROM user_funder_credentials WHERE user_id = ? AND provider = ?",
        (user_id, provider),
    )
    return cur.rowcount > 0


def list_consents(user_id: int) -> list[str]:
    """The RFC slugs this user has consented to fund. Sorted for stable display."""
    rows = db.conn().execute(
        "SELECT rfc_slug FROM funder_consents WHERE user_id = ? ORDER BY rfc_slug",
        (user_id,),
    ).fetchall()
    return [r["rfc_slug"] for r in rows]


def add_consent(user_id: int, slug: str) -> None:
    """Idempotent funder-side opt-in to fund this RFC.

    Callers gate with `has_any_credentials` so a consent without any
    registered universe (which would be inert) is refused at the API
    layer rather than written.
    """
    db.conn().execute(
        """
        INSERT INTO funder_consents (user_id, rfc_slug) VALUES (?, ?)
        ON CONFLICT(collection_id, user_id, rfc_slug) DO NOTHING
        """,
        (user_id, slug),
    )


def remove_consent(user_id: int, slug: str) -> bool:
    """Withdraw consent per §6.7's first revocation path. Returns True
    if a row was removed."""
    cur = db.conn().execute(
        "DELETE FROM funder_consents WHERE user_id = ? AND rfc_slug = ?",
        (user_id, slug),
    )
    return cur.rowcount > 0


def has_any_credentials(user_id: int) -> bool:
    row = db.conn().execute(
        "SELECT 1 FROM user_funder_credentials WHERE user_id = ? LIMIT 1",
        (user_id,),
    ).fetchone()
    return row is not None
