"""The admin's featured pin (Stream AW2, D5 and D6): a ledger decision on a product line or a
product variant, in `publish.set_indexed`'s shape.

A pin orders, never admits and never labels: `featured.order` puts a pinned variant (or one
variant per pinned line, its best by score) at the head of every featured list the record is
already a comparison in; a pinned record outside the pool heads the band it belongs to, so a
pinned one-shop variant heads the tail only, and the CLI says so at pin time. Nothing on a
card says it was pinned.

The write: one `decisions` row per target on its `featured` field, the column following
through the ledger's applier (`_flag("featured")`), one batch per call (one undo:
`decisions undo-batch <uid>`), the CLI commits. A killed CLI leaves no batch, no row, no
column. Two pins on one row serialise on the writer's advisory lock. A bulk pin (several
targets) never overrules a person's individual decision on one row (`DECISION_HELD`, reported,
never forced). Undo releases the flag to false: a rule never pins. A merge carries a pin to
the survivor (`_variant_merged_consequences`) and a line fold carries it to the fold row
(`overrides.move`); a pin set by hand outside the ledger is drift, detected by `decisions
verify` and marked here, never prevented. No API writes a pin.

Targets are `line:<slug or id>` (an old slug forwards, an alias line resolves to its fold row)
and `variant:<id>` (a merged variant resolves to its survivor, as its URL does). A hidden line,
or a variant whose line is hidden, is refused a pin (`PAGE_HIDDEN`): the pin would be inert.
`featured` must be a bool: the ledger's writer bool-checks only `hidden`, `indexed` and
`ignored`, so the check is here, before any batch opens (`VALUE_INVALID`).
"""

from __future__ import annotations

from sqlalchemy import select
from sqlalchemy.orm import Session

from app.models import ProductLine, ProductVariant
from app.services import publish
from app.services.decisions import writer
from app.services.decisions.effective import effective

#: The CLI's word for each kind, and the ledger's entity type.
KINDS = {"line": "product_line", "variant": "product_variant"}


def parse_target(spec: str) -> tuple[str, str]:
    """`(kind, ref)` from `line:<slug or id>` or `variant:<id>`; `ValueError` for anything else,
    raised before any database call."""
    kind, _, ref = spec.strip().partition(":")
    if kind not in KINDS or not ref:
        raise ValueError(f"{spec!r} is not line:<slug or id> or variant:<id>")
    if kind == "variant" and not ref.isdigit():
        raise ValueError(f"{spec!r}: a variant is named by its id")
    return kind, ref


def find(db: Session, kind: str, ref: str | int):
    """The live row a target names: a line by slug, old slug or id (forwarding rows and
    redirects followed); a variant by id, a merged one resolved to its survivor."""
    from app.services.catalog_queries import resolve_variant_id

    if kind == "line":
        return publish.find(db, "product_line", ref)
    if kind == "variant":
        try:
            vid = int(ref)
        except (TypeError, ValueError):
            return None
        return db.get(ProductVariant, resolve_variant_id(db, vid))
    raise KeyError(kind)


def line_of(db: Session, variant: ProductVariant) -> ProductLine | None:
    """The variant's product line, an alias followed to its fold row."""
    if variant.product_line_id is None:
        return None
    return publish._canonical(db, db.get(ProductLine, variant.product_line_id))


def is_hidden(db: Session, kind: str, row) -> bool:
    """A hidden line, or a variant whose line is hidden: a pin there would be inert."""
    if kind == "line":
        return publish.is_hidden(row)
    line = line_of(db, row)
    return line is not None and publish.is_hidden(line)


def path_of(db: Session, kind: str, row) -> str | None:
    """Where the pinned thing is seen: the line page, or the variant on its line's page."""
    from app.services import urls

    if kind == "line":
        return publish.page_path(row)
    line = line_of(db, row)
    if line is None or publish.is_hidden(line):
        return None
    return urls.line_path(line.slug, row.id, [])


def set_featured(db: Session, targets: list[tuple[str, str | int]], featured: bool, by, *, batch_kind: str = "cli",
                 reason: str | None = None) -> dict:
    """Pin (`featured=True`) or unpin the targets: one batch, one `decisions` row per row whose
    column changes. Named twice is one decision; already so is counted, not recorded; nothing
    to record opens no batch. Returns `set_indexed`'s dict: the batch uid (None when nothing
    changed), the mode, one entry per target, the refusals and the counts."""
    if not isinstance(featured, bool):
        raise writer.Refused("VALUE_INVALID", "featured is true or false.")
    mode = "bulk" if len(targets) > 1 else "individual"
    done: list[dict] = []
    refused: list[dict] = []
    work: list[tuple[str, str, object]] = []
    seen: set[tuple[str, int]] = set()
    for kind, ref in targets:
        entity_type = KINDS[kind]
        label = f"{kind}:{ref}"
        row = find(db, kind, ref)
        if row is not None and (kind, row.id) in seen:
            done.append({"target": label, "kind": kind, "id": row.id, "path": path_of(db, kind, row), "changed": False})
            continue
        if row is not None:
            seen.add((kind, row.id))
        if row is None:
            refused.append({"target": label, "error_code": "PAGE_NOT_FOUND",
                            "summary": f"No product {kind} answers {ref!r}."})
        elif featured and is_hidden(db, kind, row):
            refused.append({"target": label, "error_code": "PAGE_HIDDEN",
                            "summary": f"{row.name} is hidden; a pin there would lead nothing."})
        elif bool(row.featured) == featured:
            done.append({"target": label, "kind": kind, "id": row.id, "path": path_of(db, kind, row), "changed": False})
        else:
            work.append((entity_type, label, row))
    uid = None
    if work:  # a batch is opened only for something to record
        with writer.batch(db, batch_kind, mode, by, scope={"featured": "pin" if featured else "unpin", "targets": len(work)},
                          commit=False, tail=False) as b:
            for entity_type, label, row in work:
                kind = "line" if entity_type == "product_line" else "variant"
                try:
                    writer.record(b, entity_type, row, "featured", featured,
                                  reason=reason or ("pinned by an admin" if featured else "unpinned"))
                except writer.Refused as exc:
                    refused.append({"target": label, "error_code": exc.code, "summary": exc.summary})
                    continue
                done.append({"target": label, "kind": kind, "id": row.id, "path": path_of(db, kind, row), "changed": True})
        uid = str(b.uid)
    db.flush()
    changed = sum(1 for d in done if d["changed"])
    verb = "pinned" if featured else "unpinned"
    return {"batch_uid": uid if changed else None, "mode": mode, "targets": done, "refusals": refused,
            "counts": {"changed": changed, "unchanged": len(done) - changed, "refused": len(refused)},
            "message": f"{changed} {verb}, {len(done) - changed} already so, {len(refused)} refused"}


def current(db: Session) -> list[dict]:
    """Every pin, newest decision first: the column's pins with the decision behind each
    (`decided_at`, `by`), `inert` when it leads nothing (a hidden line, a variant whose line
    is hidden, a merged variant), `drift` when the column and the ledger disagree (a pin with
    no effective decision, or a decision the column no longer shows)."""
    out: list[dict] = []
    for kind, model in (("line", ProductLine), ("variant", ProductVariant)):
        entity_type = KINDS[kind]
        rows = {r.id: r for r in db.scalars(select(model).where(model.featured.is_(True)))}
        decided = effective(db, entity_type, None, ["featured"])
        for (entity_id, _field), dec in decided.items():
            if dec.value is True and entity_id not in rows:
                row = db.get(model, entity_id)
                if row is not None:
                    rows[entity_id] = row
        for entity_id, row in rows.items():
            dec = decided.get((entity_id, "featured"))
            pinned_by_ledger = dec is not None and dec.value is True
            inert = is_hidden(db, kind, row) or (kind == "variant" and row.merged_into_id is not None) \
                or (kind == "line" and row.alias_of_id is not None)
            out.append({
                "kind": kind, "id": row.id, "name": row.name, "path": path_of(db, kind, row),
                "decided_at": dec.decided_at if dec is not None else None,
                "by": dec.decided_by_username if dec is not None else None,
                "pinned": bool(row.featured), "inert": bool(inert), "drift": bool(row.featured) != pinned_by_ledger,
            })
    out.sort(key=lambda p: (p["decided_at"] is None, -(p["decided_at"].timestamp() if p["decided_at"] else 0), p["kind"], p["id"]))
    return out


def comparison_state(db: Session, kind: str, row) -> str:
    """One line for the CLI: what the pin leads today. A variant: its airports and saving; a
    line: its best compared variant's, the one the pin lifts. `none` means the record is not a
    comparison and shows only in the full list (D6)."""
    from app.services import featured
    from app.services.catalog_queries import featured_records

    if kind == "variant":
        ids = [row.id]
    else:
        ids = list(db.scalars(select(ProductVariant.id).where(ProductVariant.product_line_id == row.id,
                                                              ProductVariant.merged_into_id.is_(None))))
    records = featured_records(db, only_ids=ids or [0], multi_only=False) if ids else []
    compared = [r for r in records if r.compared]
    if not compared:
        return "comparison: none: shows only in the full list"
    best = max(compared, key=lambda r: (r.saving_usd, -r.variant_id))
    held = " (held back: " + ", ".join(featured.gates(best)) + ")" if featured.gates(best) else ""
    return f"comparison: {best.shop_count} airports, ${best.saving_usd:.2f} / {best.saving_pct:.0%}{held}"
