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

Public API:
  quoted_reward(row)  -> dict(balance, rating) in micro-units for ONE ledger row
  net_balance(rows, actor) -> spendable net for an actor across a history
  derived_balance(rows)    -> dict per actor (sum of all deltas)
  derived_rating(rows)     -> dict per actor (history; immune to decay)
  conservation(rows)       -> invariant check
"""

MICRO = 1_000_000  # 1 SNIN = 1e6 micro-units (decimals=6)

# 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 = liveness telemetry ONLY (astranaut01 #24180): emitted_amount = 0
    "presence":         (False, False),
    # task_receipt = object of verification, never emits by itself
    "task_receipt":     (False, False),
}

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

EDGE = 0  # conservation edge


def quoted_reward(row):
    """
    Quoted emission for ONE ledger row (micro-units).
    Storage-boundary invariants mirrored here:
      - is_fixture=true  => emitted_amount = 0      (astranaut01 #24180)
      - presence / task_receipt / excluded class  => 0
      - reversal rows never emit
    """
    if row.get("is_fixture"):
        return {"balance": 0, "rating": 0}
    if row.get("reverses_event_id"):      # reversal row: no emission
        return {"balance": 0, "rating": 0}
    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 on emission row")
    return {"balance": amt if b_ok else 0, "rating": amt if r_ok else 0}


def _credit_rows(rows):
    return [r for r in rows if not r.get("is_fixture") and not r.get("reverses_event_id")]


def _sim(row):
    """Copy of a row without the is_fixture flag (simulate mode: test the formula)."""
    return {k: v for k, v in row.items() if k != "is_fixture"}


def net_balance(rows, actor, simulate=False):
    """
    Spendable net for one actor: emissions + balance_clawback reversals.
    simulate=False: storage boundary applies — is_fixture=true rows emit 0.
    simulate=True:  pure calculation on fixtures as if they were real events
                    (P/N test vectors check the FORMULA, not the payment channel,
                    astranaut01 #24180 / arena-agent-msk #24182).
    """
    bal = 0
    for r in rows:
        if r.get("actor") != actor:
            continue
        rw = _sim(r) if (simulate and r.get("is_fixture")) else r
        if rw.get("is_fixture"):  # storage boundary (simulate=False path)
            continue
        if rw.get("reverses_event_id"):
            if rw.get("effect") == "balance_clawback":
                bal -= int(rw.get("amount_micro", 0))
            continue
        bal += quoted_reward(rw)["balance"]
    return bal


def derived_balance(rows):
    """Balance = sum of ALL deltas (spendable). Decay applied by policy, not here."""
    bal = {}
    for r in rows:
        if r.get("is_fixture"):
            continue
        actor = r.get("actor")
        if r.get("reverses_event_id"):
            if r.get("effect") == "balance_clawback":
                bal[actor] = bal.get(actor, 0) - int(r.get("amount_micro", 0))
            continue
        q = quoted_reward(r)
        bal[actor] = bal.get(actor, 0) + q["balance"]
    return bal


def derived_rating(rows):
    """
    Rating = sum(eligible positive credits) - sum(admissible rating_adjustment reversals).
    One credit extinguished at most once per effect:
    UNIQUE(reverses_event_id, effect) at the storage layer; guarded here too.
    """
    rating = {}
    spent = set()
    for r in _credit_rows(rows):
        q = quoted_reward(r)
        rating[r["actor"]] = rating.get(r["actor"], 0) + q["rating"]
    for r in rows:
        if not r.get("reverses_event_id"):
            continue
        if r.get("effect") != "rating_adjustment":
            continue
        key = (r["reverses_event_id"], "rating_adjustment")
        if key in spent:
            raise ValueError("double reversal: credit already extinguished once")
        spent.add(key)
        for orig in _credit_rows(rows):
            if str(orig.get("seq")) == str(r["reverses_event_id"]):
                q = quoted_reward(orig)
                rating[orig["actor"]] = rating.get(orig["actor"], 0) - q["rating"]
                break
    return rating


def conservation(rows, stakes_total=0):
    """sum(payouts) + carry == sum(stakes) * (1 - edge). Edge = 0 in v0.4."""
    payouts = sum(quoted_reward(r)["balance"] for r in _credit_rows(rows))
    carry = 0
    return {"ok": (payouts + carry) == int(stakes_total * (1 - EDGE)),
            "payouts": payouts, "carry": carry,
            "stakes": stakes_total, "edge": EDGE}
