"""The decided layer's shim (Stream K2): the public names every older reader calls, delegating
to the ledger (`services/decisions`) with the pre-ledger names mapped in one table. Kept for
one release so the routers, the CLI, the merge session and the kit suites stand while the
ledger settles; the writer, the effective read and the appliers are the real thing.

Sources of truth: this module, `services/decisions/writer.py`, `services/decisions/effective.py`,
`tests/test_decided.py` (pins today's behaviour through this shim). The mapping: entity types
`product` -> `product_variant`, `line` -> `product_line`, `attribute_alias` -> `attribute_wording`;
fields `product_line_id` -> `product_line`, `attribute` -> `attribute:<kind>`, `quantity` ->
`attribute:quantity`, `alias_of_id` -> `alias_of`, `canonical` -> `meaning`, `pinned_variant_id`
-> `pinned_to`, `review` -> `hidden` (a `checked` review is refused `VALUE_INVALID`: an approved
sheet is the quality fact now, plan W18). `clear` is a release; `move` is the carry.
"""

from __future__ import annotations

from collections.abc import Iterable
from typing import Any

from sqlalchemy import select
from sqlalchemy.orm import Session

from app.models import Brand, Decision, ProductLine
from app.services.attributes import GUARDED_COLUMNS  # noqa: F401  (derived from the registry)
from app.services.decisions.effective import effective as _effective, table_present as _table_present
from app.services.decisions import writer
from app.services.decisions.writer import Refused  # noqa: F401  (the same exception, re-exported)

ENTITY_TYPES = {"product": "product_variant", "line": "product_line", "attribute_alias": "attribute_wording",
                "brand": "brand", "listing": "listing", "product_variant": "product_variant", "product_line": "product_line",
                "attribute_wording": "attribute_wording", "suggestion": "suggestion", "place": "place"}
LEGACY_FIELDS = {"product_line_id": "product_line", "quantity": "attribute:quantity", "alias_of_id": "alias_of",
                 "canonical": "meaning", "pinned_variant_id": "pinned_to", "review": "hidden"}
FIELDS: dict[str, tuple[str, ...]] = {
    "product": ("name", "product_line_id", "attribute", "quantity"),
    "brand": ("name", "alias_of_id", "review"),
    "line": ("name", "alias_of_id", "review"),
    "attribute_alias": ("canonical", "display"),
    "listing": ("pinned_variant_id", "ignored"),
}
REVIEW_STATES = ("checked", "hidden")
REFERENCE_FIELDS = ("product_line", "alias_of", "merged_into", "pinned_to")
_present: dict = {}  # kept for the kits that clear it; the ledger's presence is read per call now


def attribute_alias_key(vertical: str, raw: str) -> str:
    return f"wording:{vertical}|{raw}"


def table_present(db: Session) -> bool:
    return _table_present(db)


def _entity_type(name: str) -> str:
    try:
        return ENTITY_TYPES[name]
    except KeyError as exc:
        raise ValueError(f"unknown entity type {name!r}") from exc


def _field(db: Session, entity_type: str, key: str, field_name: str) -> str:
    if field_name == "attribute" and entity_type == "product_variant":
        from app.models import ProductVariant
        from app.services import keying

        row = db.get(ProductVariant, int(key))
        kind = ((row.attributes or {}).get("attribute_kind") if row is not None else None) or \
            (keying.maps_for(db).attribute_kind(row.name, row.vertical, category=row.category) if row is not None else "") or "concentration"
        return f"attribute:{kind}"
    return LEGACY_FIELDS.get(field_name, field_name)


def _legacy_field(field_name: str) -> str:
    if field_name.startswith("attribute:"):
        return "quantity" if field_name == "attribute:quantity" else "attribute"
    return {v: k for k, v in LEGACY_FIELDS.items()}.get(field_name, field_name)


class _View:
    """A ledger row under a legacy field name: a reference reads its resolved id."""

    def __init__(self, row: Decision) -> None:
        self.row = row
        self.value = row.value_ref_id if row.field in REFERENCE_FIELDS else row.value
        if row.field == "hidden":
            self.value = "hidden" if row.value else None
        self.collected_value = row.rule_value
        self.set_by, self.set_at, self.reason, self.field = row.decided_by, row.decided_at, row.reason, row.field
        self.entity_key = str(row.entity_id)


def read(db: Session, entity_type: str, keys: Iterable[str] | None = None,
         fields: Iterable[str] | None = None) -> dict[str, dict[str, _View]]:
    """Every effective decision of an entity type, by key then legacy field name."""
    from app.services.decisions import natural_keys

    et = _entity_type(entity_type)
    by_id: dict[int, str] = {}
    if keys is not None:
        for k in keys:
            if str(k).lstrip("-").isdigit():
                by_id[int(k)] = str(k)
            elif et == "attribute_wording":
                key = str(k) if str(k).startswith("wording:") else f"wording:{k}"
                row = natural_keys.resolve(db, et, key, None)
                if not isinstance(row, natural_keys.Parked):
                    by_id[row.id] = str(k)
        if not by_id:
            return {}
    wanted = None if fields is None else {LEGACY_FIELDS.get(f, f) for f in fields} | {f for f in fields}
    out: dict[str, dict[str, _View]] = {}
    for (entity_id, field_name), row in _effective(db, et, list(by_id) if keys is not None else None).items():
        legacy = _legacy_field(field_name)
        if wanted is not None and field_name not in wanted and legacy not in wanted:
            continue
        out.setdefault(by_id.get(entity_id, str(entity_id)), {})[legacy] = _View(row)
    return out


def fields_of(db: Session, entity_type: str, key: str) -> dict[str, _View]:
    return read(db, entity_type, [key]).get(str(key), {})


def guarded_columns(overrides: dict[str, Any]) -> frozenset[str]:
    from app.services.attributes import guarded_for

    return guarded_for(overrides)


def _batch(db: Session, set_by, *, kind: str = "route", commit: bool = False, tail: bool = False):
    return writer.batch(db, kind, "individual", set_by, commit=commit, tail=tail)


def decide(db: Session, entity_type: str, key: str, field: str, value: Any, *, set_by: int | None,
           reason: str | None = None, collected_value: Any = None, batch=None) -> Decision:
    """Record one decision in a batch of one (or the caller's), materialised by the applier."""
    et = _entity_type(entity_type)
    if field not in FIELDS.get(entity_type, ()) and not writer.field_ok(et, LEGACY_FIELDS.get(field, field)):
        raise ValueError(f"{entity_type} has no decidable field {field!r}")
    field_name = _field(db, et, str(key), field)
    if field == "review":
        if value not in REVIEW_STATES:
            raise Refused("VALUE_INVALID", "A review is hidden; checked is retired (an approved sheet is the quality fact).")
        if value == "checked":
            raise Refused("VALUE_INVALID", "checked is retired: an approved sheet is the quality fact.")
        value = True
    if field == "attribute" and value == "":
        return _release(db, et, key, field_name, set_by, batch)
    entity = key if not str(key).lstrip("-").isdigit() else int(key)
    if batch is not None:
        return writer.record(batch, et, entity, field_name, value, reason=reason)
    with _batch(db, set_by) as b:
        return writer.record(b, et, entity, field_name, value, reason=reason)


def ledger(db: Session, entity_type: str, key: str, field: str, value: Any, *, set_by: int | None,
           reason: str | None = None, collected_value: Any = None, batch=None) -> Decision | None:
    if not table_present(db):
        return None
    return decide(db, entity_type, key, field, value, set_by=set_by, reason=reason, collected_value=collected_value, batch=batch)


def _release(db, et, key, field_name, set_by, batch=None):
    entity = key if not str(key).lstrip("-").isdigit() else int(key)
    if batch is not None:
        return writer.record(batch, et, entity, field_name, None, effect="release", reason="cleared")
    with _batch(db, set_by) as b:
        return writer.record(b, et, entity, field_name, None, effect="release", reason="cleared")


def clear(db: Session, entity_type: str, key: str, field: str, *, set_by: int | None = None, batch=None) -> bool:
    """Withdraw a decision: a release row; the rules take over on the next rekey."""
    if not table_present(db):
        return False
    et = _entity_type(entity_type)
    field_name = _field(db, et, str(key), field)
    if (int(key), field_name) not in _effective(db, et, [int(key)], [field_name]):
        return False
    _release(db, et, key, field_name, set_by, batch)
    return True


def move(db: Session, entity_type: str, from_key: str, to_key: str, *, batch=None, cause=None) -> dict[str, list[str]]:
    """The carry: the loser's effective decisions go to the survivor where it has none for that
    field (each a row with `detail.carried_from`); the rest are listed as kept."""
    if not table_present(db):
        return {"moved": [], "kept": []}
    et = _entity_type(entity_type)
    ours = _effective(db, et, [int(to_key)])
    theirs = _effective(db, et, [int(from_key)])
    moved, kept = [], []

    def carry(b):
        for (_, field_name), row in theirs.items():
            if field_name in ("merged_into", "alias_of"):
                continue  # a pointer is never carried: the loser's own forwarding stays its own
            if (int(to_key), field_name) in ours:
                kept.append(_legacy_field(field_name))
                continue
            value = row.value_ref_id if row.value_ref_id is not None else row.value
            try:
                writer.record(b, et, int(to_key), field_name, value, origin=row.origin, reason=row.reason, cause=cause,
                              detail={"carried_from": row.natural_key}, decided_by=row.decided_by)
                moved.append(_legacy_field(field_name))
            except Refused as exc:
                kept.append(f"{_legacy_field(field_name)} ({exc.code})")

    if batch is not None:
        carry(batch)
    else:
        with writer.batch(db, "cli", "individual", None, commit=False, tail=False, note="carry") as b:
            carry(b)
    return {"moved": sorted(moved), "kept": sorted(kept)}


# --------------------------------------------------------------------------- the decisions a route applies

def _live_product(db: Session, variant_id: int):
    from app.models import ProductVariant

    product = db.get(ProductVariant, variant_id)
    if product is None:
        raise Refused("PRODUCT_NOT_FOUND", "No such product variant.")
    if product.merged_into_id is not None:
        raise Refused("PRODUCT_MERGED", "This product variant was merged away; decide on its survivor.")
    return product


def decide_product(db: Session, variant_id: int, field: str, value: Any, *, set_by: int | None,
                   reason: str | None = None) -> dict[str, Any]:
    """Record a variant decision and let the key follow: the ledger row, the column, the rekey and
    the fold at the batch's tail (`merges.rekey_product_variants`, `merges._fold`). Commits."""
    from app.services import quantity as quantity_service

    product = _live_product(db, variant_id)
    if field not in FIELDS["product"]:
        raise Refused("FIELD_UNKNOWN", f"A product variant has no decidable field {field!r}.")
    if field == "product_line_id":
        try:
            value = int(value)
        except (TypeError, ValueError) as exc:
            raise Refused("VALUE_INVALID", "A product line is chosen by its id.") from exc
        if db.get(ProductLine, value) is None:
            raise Refused("LINE_NOT_FOUND", "No such product line.")
    elif field == "attribute":
        if value is None:
            value = ""
        if not isinstance(value, str):
            raise Refused("VALUE_INVALID", "An attribute is a string; empty means none.")
        value = value.strip().lower()[:80]
    elif field == "quantity":
        if not isinstance(value, dict):
            raise Refused("VALUE_INVALID", "A quantity is {value, unit} with an optional form.")
        unit = value.get("unit")
        if unit not in quantity_service.UNITS and not (unit is None and value.get("set_contents")):
            raise Refused("VALUE_INVALID", "The unit is ml, g or pcs.")
        try:
            number = float(value.get("value")) if value.get("value") is not None else None
        except (TypeError, ValueError) as exc:
            raise Refused("VALUE_INVALID", "The value is a number.") from exc
        form = value.get("form") or "single"
        if form not in quantity_service.FORMS:
            raise Refused("VALUE_INVALID", "The form is single, pack, set or refill.")
        value = {"value": number, "unit": unit, "form": form, "pack_count": value.get("pack_count"),
                 "pack_unit_value": value.get("pack_unit_value"), "set_contents": value.get("set_contents")}
    with writer.batch(db, "route", "individual", set_by, commit=True, tail=True) as b:
        decide(db, "product", str(product.id), field, value, set_by=set_by, reason=reason, batch=b)
        db.flush()
    db.refresh(product)
    return {"variant_id": product.id, "field": field, "value": value, "match_key": product.match_key,
            "groups_merged": b.folded.get("groups", 0), "rows_merged": b.folded.get("merged_rows", 0), "batch": str(b.uid)}


def _listing(db: Session, listing_id: int):
    from app.models import Listing

    listing = db.get(Listing, listing_id)
    if listing is None:
        raise Refused("LISTING_NOT_FOUND", "No such listing.")
    return listing


def pin_listing(db: Session, listing_id: int, variant_id: int, *, set_by: int | None, reason: str | None = None) -> dict:
    listing = _listing(db, listing_id)
    _live_product(db, variant_id)
    with writer.batch(db, "route", "individual", set_by, commit=True, tail=False) as b:
        writer.record(b, "listing", listing, "pinned_to", variant_id, reason=reason)
    db.refresh(listing)
    return {"listing_id": listing.id, "variant_id": listing.variant_id, "pinned_variant_id": listing.pinned_variant_id, "reason": reason}


def unpin_listing(db: Session, listing_id: int, *, set_by: int | None) -> dict:
    listing = _listing(db, listing_id)
    with writer.batch(db, "route", "individual", set_by, commit=True, tail=False) as b:
        if (listing.id, "pinned_to") in _effective(db, "listing", [listing.id], ["pinned_to"]):
            writer.record(b, "listing", listing, "pinned_to", None, effect="release", reason="unpinned")
        else:
            listing.pinned_variant_id, listing.pinned_by, listing.pinned_at = None, None, None
    return {"listing_id": listing.id, "variant_id": listing.variant_id, "pinned_variant_id": None}


def ignore_listing(db: Session, listing_id: int, *, set_by: int | None, reason: str | None = None) -> dict:
    listing = _listing(db, listing_id)
    with writer.batch(db, "route", "individual", set_by, commit=True, tail=False) as b:
        writer.record(b, "listing", listing, "ignored", True, reason=reason)
    return {"listing_id": listing.id, "ignored": True, "reason": listing.ignore_reason}


def unignore_listing(db: Session, listing_id: int, *, set_by: int | None) -> dict:
    listing = _listing(db, listing_id)
    with writer.batch(db, "route", "individual", set_by, commit=True, tail=False) as b:
        if (listing.id, "ignored") in _effective(db, "listing", [listing.id], ["ignored"]):
            writer.record(b, "listing", listing, "ignored", None, effect="release", reason="un-ignored")
        else:
            listing.ignored_at, listing.ignored_by, listing.ignore_reason = None, None, None
    return {"listing_id": listing.id, "ignored": False}


# --------------------------------------------------------------------------- the review gate

def review(db: Session, entity_type: str, entity_id: int, state: str | None, *, set_by: int | None,
           reason: str | None = None) -> dict[str, Any]:
    """`hidden` takes an eligible page off the site; None withdraws it; `checked` is retired
    (`VALUE_INVALID`: an approved sheet is the quality fact, plan W18). Commits."""
    if entity_type not in ("brand", "line"):
        raise Refused("ENTITY_UNKNOWN", "A review is of a brand or a product line.")
    et = _entity_type(entity_type)
    row = db.get(Brand if entity_type == "brand" else ProductLine, entity_id)
    if row is None:
        raise Refused("ENTITY_NOT_FOUND", f"No such {entity_type}.")
    if row.alias_of_id is not None:
        raise Refused("ENTITY_ALIASED", f"This {entity_type} is an alias; review the one it points at.")
    if state is None:
        with writer.batch(db, "route", "individual", set_by, commit=True, tail=False) as b:
            cleared = clear(db, entity_type, str(entity_id), "review", set_by=set_by, batch=b)
        return {entity_type: entity_id, "review": None, "cleared": cleared}
    if state != "hidden":
        raise Refused("VALUE_INVALID", "A review is hidden; checked is retired (an approved sheet is the quality fact).")
    with writer.batch(db, "route", "individual", set_by, commit=True, tail=False) as b:
        writer.record(b, et, row, "hidden", True, reason=reason)
    return {entity_type: entity_id, "review": state}


def hidden_ids(db: Session, entity_type: str) -> set[int]:
    """The brands or lines a person hid from the site: the materialised column."""
    model = Brand if entity_type == "brand" else ProductLine
    return set(db.scalars(select(model.id).where(model.hidden.is_(True))))
