"""Template sets: fill the shared variables once, get a whole punchlist's worth
of items (plan §1 manager hat). The Keith-email case: one email's 13 asks →
one set → one instantiation.

Pinning happens at run time: the set names template KEYS (optionally pinning a
version); instantiation resolves each to a concrete published version and the
items pin those forever. Each item receives ONLY the variables its own template
declares — a set-level variable a template doesn't know is simply not passed.
"""

import json
import logging
from pathlib import Path

from sqlalchemy import select

from app.db import get_session_factory
from app.models import SetRun, TemplateSet, WorkflowTemplate
from app.schemas import validate_spec
from app.services import items as items_svc
from app.services import workflows as wf

log = logging.getLogger(__name__)

SET_SEED_DIR = Path(__file__).resolve().parent.parent / "seeds" / "sets"


class SetError(Exception):
    def __init__(self, message, code="BAD_INPUT"):
        super().__init__(message)
        self.code = code


def latest_published(session, key: str) -> TemplateSet | None:
    return session.execute(
        select(TemplateSet)
        .where(TemplateSet.key == key, TemplateSet.status == "published")
        .order_by(TemplateSet.version.desc())
    ).scalars().first()


def library(session) -> list[dict]:
    rows = session.execute(
        select(TemplateSet).where(TemplateSet.status == "published")
        .order_by(TemplateSet.key, TemplateSet.version.desc())
    ).scalars().all()
    seen, out = set(), []
    for t in rows:
        if t.key in seen:
            continue
        seen.add(t.key)
        out.append({"key": t.key, "version": t.version, "title": t.title,
                    "variables": t.variables, "size": len(t.items)})
    return out


def publish(session, actor: str, raw: dict) -> TemplateSet:
    """Validate + publish a set as key@next-version. Every referenced template
    must resolve to a published version NOW — a set that references a missing
    workflow is refused with the full list, builder-style."""
    key = (raw.get("key") or "").strip()
    title = (raw.get("title") or "").strip()
    items = raw.get("items") or []
    if not key or not title or not items:
        raise SetError("A set needs key, title and a non-empty items list.")
    errors = []
    for i, entry in enumerate(items):
        tkey = (entry.get("template_key") or "").strip()
        if not tkey:
            errors.append(f"items[{i}]: template_key is required")
            continue
        pin = entry.get("pin_version")
        if pin is not None:
            row = session.execute(
                select(WorkflowTemplate).where(
                    WorkflowTemplate.key == tkey, WorkflowTemplate.version == pin,
                    WorkflowTemplate.status == "published")
            ).scalars().first()
        else:
            row = wf.latest_published(session, tkey)
        if row is None:
            errors.append(f"items[{i}]: no published workflow "
                          f"'{tkey}'" + (f" v{pin}" if pin else ""))
    if errors:
        raise SetError("; ".join(errors))
    prev = session.execute(
        select(TemplateSet).where(TemplateSet.key == key)
        .order_by(TemplateSet.version.desc())
    ).scalars().first()
    row = TemplateSet(key=key, version=(prev.version + 1) if prev else 1,
                      title=title, variables=raw.get("variables") or {},
                      items=items, status="published",
                      created_by=actor)
    session.add(row)
    session.flush()
    return row


def instantiate(session, actor: str, punchlist_id: str, set_key: str,
                variables: dict | None = None) -> tuple[SetRun, list]:
    """Fill once → N items, in the set's order, appended after existing items."""
    tset = latest_published(session, set_key)
    if tset is None:
        raise SetError(f"No published set '{set_key}'.", "NO_SUCH_SET")
    variables = dict(variables or {})

    missing = [name for name, v in (tset.variables or {}).items()
               if v.get("required", True) and not variables.get(name)]
    if missing:
        raise SetError(f"Missing required variables: {', '.join(sorted(missing))}.",
                       "MISSING_VARIABLES")

    run = SetRun(punchlist_id=punchlist_id, set_template_id=tset.id,
                 variables=variables, created_by=actor)
    session.add(run)
    session.flush()

    created = []
    for entry in tset.items:
        tkey = entry["template_key"]
        pin = entry.get("pin_version")
        if pin is not None:
            tpl = session.execute(
                select(WorkflowTemplate).where(
                    WorkflowTemplate.key == tkey, WorkflowTemplate.version == pin,
                    WorkflowTemplate.status == "published")
            ).scalars().first()
        else:
            tpl = wf.latest_published(session, tkey)
        if tpl is None:  # was validated at publish; a later retire can still bite
            raise SetError(f"Workflow '{tkey}' is no longer published.", "NO_SUCH_TEMPLATE")
        spec = validate_spec(tpl.spec)
        # each template takes only the variables it declares
        item_vars = {k: v for k, v in variables.items() if k in spec.variables}
        item = items_svc.instantiate(
            session, actor, punchlist_id,
            template_key=tkey, variables=item_vars, set_run_id=run.id,
            section=entry.get("section"))
        created.append(item)
    return run, created


def load_seeds() -> None:
    """Append-only, like workflow seeds: publish v1 for any set key that has no
    published version yet."""
    if not SET_SEED_DIR.is_dir():
        return
    with get_session_factory()() as session:
        for f in sorted(SET_SEED_DIR.glob("*.json")):
            raw = json.loads(f.read_text())
            key = raw.get("key", f.stem)
            if latest_published(session, key):
                continue
            try:
                publish(session, "system-seed", raw)
                log.info("seeded set %s v1", key)
            except SetError as exc:
                raise RuntimeError(f"seed set '{f.name}' is invalid: {exc}")
        session.commit()
