"""§22 collection grain — resolution helpers beneath the project tier.

In S1 each project has exactly one collection (the default). These helpers
recover the collection for a project and read the per-corpus fields (`type`,
`initial_state`) that moved down from `projects` in migration 029. Project-grain
authz (auth.py) recovers a row's project by joining `collections` on
`collection_id`.
"""
from __future__ import annotations

import json

from . import db

DEFAULT_COLLECTION_ID = "default"

# §22.4a item (2): the displayed noun for an entry is a type-driven label, a
# framework concept (like role names), not deployment content. The chrome reads
# this from the API rather than hardcoding "RFC", so a `specification` collection
# says "Spec" and a `bdd` collection says "Feature" with no per-deployment config.
ENTRY_NOUN = {
    "document": "RFC",
    "specification": "Spec",
    "bdd": "Feature",
}
_DEFAULT_ENTRY_NOUN = "RFC"


def entry_noun(collection_type: str) -> str:
    """The §22.4a entry noun for a collection type. Unknown types fall back to
    the generic 'RFC' so a future type is never label-less."""
    return ENTRY_NOUN.get(collection_type, _DEFAULT_ENTRY_NOUN)


def _enabled_models_from_config(config_json: str | None) -> list[str] | None:
    """§22.12 per-collection enabled_models from a `config_json` blob, or None
    when unset (the collection inherits its project's universe)."""
    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 _fields_from_config(config_json: str | None) -> dict | None:
    """§22.4a SLICE-2 per-collection metadata field schema from a `config_json`
    blob, or None when the collection declares no `fields:`. The stored value is
    already normalized by `metadata_schema.parse_fields` at ingest, so it's
    served verbatim."""
    if not config_json:
        return None
    try:
        cfg = json.loads(config_json)
    except (json.JSONDecodeError, TypeError):
        return None
    fields = cfg.get("fields") if isinstance(cfg, dict) else None
    return fields if isinstance(fields, dict) and fields else None


def default_collection_id(project_id: str) -> str:
    """The id of a project's default (S1: sole) collection. Falls back to the
    literal 'default' when the project has no collection row yet."""
    row = db.conn().execute(
        "SELECT id FROM collections WHERE project_id = ? ORDER BY created_at, id LIMIT 1",
        (project_id,),
    ).fetchone()
    return row["id"] if row else DEFAULT_COLLECTION_ID


def project_of_collection(collection_id: str) -> str | None:
    """The project a collection belongs to, or None if unknown."""
    row = db.conn().execute(
        "SELECT project_id FROM collections WHERE id = ?", (collection_id,)
    ).fetchone()
    return row["project_id"] if row else None


def collection_initial_state(collection_id: str) -> str:
    """§22.4b landing state for new entries in a collection. 'super-draft'
    default for an unknown/unset row (today's safe flow)."""
    row = db.conn().execute(
        "SELECT initial_state FROM collections WHERE id = ?", (collection_id,)
    ).fetchone()
    if row is None or not row["initial_state"]:
        return "super-draft"
    return row["initial_state"]


def collection_type(collection_id: str) -> str:
    """The collection's immutable §22.4a type. 'document' default for unknown."""
    row = db.conn().execute(
        "SELECT type FROM collections WHERE id = ?", (collection_id,)
    ).fetchone()
    return row["type"] if row and row["type"] else "document"


def subfolder_of(collection_id: str) -> str:
    """The content-repo subfolder a collection lives under (§22.3). Empty string
    for the default collection (entries at the repo root `rfcs/`)."""
    row = db.conn().execute(
        "SELECT subfolder FROM collections WHERE id = ?", (collection_id,)
    ).fetchone()
    return (row["subfolder"] if row else "") or ""


def get_collection(collection_id: str) -> dict | None:
    """The full collection row as a dict, or None if unknown. `enabled_models`
    (§22.12) is unpacked from `config_json` as a list, or None when unset."""
    row = db.conn().execute(
        "SELECT id, project_id, type, subfolder, initial_state, visibility, name, "
        "config_json FROM collections WHERE id = ?",
        (collection_id,),
    ).fetchone()
    if row is None:
        return None
    out = dict(row)
    config_json = out.pop("config_json", None)
    out["enabled_models"] = _enabled_models_from_config(config_json)
    # §22.4a SLICE-2: serve the collection's metadata field schema (None when
    # the collection declares no `fields:` — INV-5, the default `document`
    # collection is unaffected).
    out["fields"] = _fields_from_config(config_json)
    out["entry_noun"] = entry_noun(out["type"])
    return out


def list_collections(project_id: str, include_unlisted: bool = False) -> list[dict]:
    """Collections in a project, the default first then by name (§22.5). `unlisted`
    is omitted from enumeration unless include_unlisted (a direct-id read or the
    corpus mirror, which serves every collection)."""
    rows = db.conn().execute(
        "SELECT id, project_id, type, subfolder, initial_state, visibility, name "
        "FROM collections WHERE project_id = ? ORDER BY (id != 'default'), name, id",
        (project_id,),
    ).fetchall()
    out: list[dict] = []
    for r in rows:
        if not include_unlisted and r["visibility"] == "unlisted":
            continue
        item = dict(r)
        item["entry_noun"] = entry_noun(item["type"])
        out.append(item)
    return out
