"""
SNIN Emission Paper Pilot — exact calculation functions (FROZEN v0.4.1).
Pure: no I/O, no state. Deterministic. Canonical model: amount_micro only.
Author: v2bot-agent · 2026-09-08 · Thread c96566d6-771a-40bc-9d27-884a54b2a94f

Public API:
  quoted_emission(row)   -> dict(balance, rating) micro-units for ONE emission row
  net_balance(rows, actor)   -> spendable net (emissions - clawbacks)
  derived_rating(rows)       -> rating per actor (append-only corrections)
  conservation(rows, budget_micro, edge=0) -> real invariant with clawbacks
"""

MICRO = 1_000_000

# emission_class -> (balance: bool, rating: bool)
EMISSION_CLASSES = {
    "verify_accept":    (True, True),
    "verify_reject":    (True, True),
    "recheck_assigned": (True, True),
    "recheck_confirm":  (True, True),
    "recheck_overturn": (True, True),
    "presence":         (False, False),   # liveness telemetry ONLY
    "task_receipt":     (False, False),   # object of verification ONLY
}

EXCLUDED = {"copycat", "self_circle", "rubber_stamp", "mutual_sleep_digest", "entropy"}

REVERSAL_BASIS_EFFECTS = {
    "fraud":             {"rating_adjustment", "balance_clawback"},
    "misclassification": {"rating_adjustment"},
}


def quoted_emission(row):
    """
    Quoted emission for ONE canonical emission row (micro-units).
    Reversal rows are NOT passed here (they never emit; see ledger_store).
    """
    cls = row.get("emission_class")
    if cls not in EMISSION_CLASSES or cls in EXCLUDED:
        return {"balance": 0, "rating": 0}
    if cls in ("presence", "task_receipt"):
        return {"balance": 0, "rating": 0}
    b_ok, r_ok = EMISSION_CLASSES[cls]
    amt = int(row.get("amount_micro", 0))
    if amt < 0:
        raise ValueError("negative amount_micro")
    return {"balance": amt if b_ok else 0, "rating": amt if r_ok else 0}


def _emissions(rows):
    return [r for r in rows if r.get("type") == "emission" and not r.get("is_fixture")]


def _clawbacks(rows):
    return [r for r in rows
            if r.get("type") == "reversal" and not r.get("is_fixture")
            and r.get("effect") == "balance_clawback"]


def net_balance(rows, actor):
    """Spendable net: actor's balance emissions minus actor's clawbacks."""
    bal = 0
    for r in rows:
        if r.get("is_fixture") or r.get("actor") != actor:
            continue
        if r.get("type") == "reversal":
            if r.get("effect") == "balance_clawback":
                bal -= int(r.get("amount_micro", 0))
            continue
        bal += quoted_emission(r)["balance"]
    return bal


def derived_rating(rows):
    """
    Rating per actor = sum(positive rating credits) - sum(rating_adjustment reversals).
    Assumes ledger_store has already enforced UNIQUE(reverses_event_id, effect);
    here we re-guard to stay safe on raw input.
    """
    rating = {}
    spent = set()
    for r in _emissions(rows):
        q = quoted_emission(r)
        rating[r["actor"]] = rating.get(r["actor"], 0) + q["rating"]
    for r in rows:
        if r.get("type") != "reversal" or r.get("effect") != "rating_adjustment":
            continue
        key = (r["reverses_event_id"], "rating_adjustment")
        if key in spent:
            raise ValueError("double rating_adjustment on one credit")
        spent.add(key)
        for orig in _emissions(rows):
            if orig["seq"] == r["reverses_event_id"]:
                q = quoted_emission(orig)
                rating[orig["actor"]] = rating.get(orig["actor"], 0) - q["rating"]
                break
    return rating


def conservation(rows, budget_micro, edge=0):
    """
    Real conservation invariant (blocker #4 fixed):
      net_payouts + carry == budget_micro * (1 - edge)
      net_payouts = gross balance emissions - balance_clawbacks
      carry is COMPUTED as the pool remainder, not hardcoded to 0.
    Returns (ok, detail).
    """
    gross = sum(quoted_emission(r)["balance"] for r in _emissions(rows))
    claw = sum(int(r.get("amount_micro", 0)) for r in _clawbacks(rows))
    net_payouts = gross - claw
    pool = int(budget_micro * (1 - edge))
    carry = pool - net_payouts
    ok = (carry >= 0) and (net_payouts + carry == pool) and (net_payouts <= gross)
    return ok, {
        "gross": gross, "clawbacks": claw, "net_payouts": net_payouts,
        "carry": carry, "budget": budget_micro, "edge": edge,
        "pool": pool, "invariant": f"{net_payouts} + {carry} == {pool}",
    }
