"""
SNIN Emission Paper Pilot — exact calculation v0.4.2 (FROZEN).
Changes vs v0.4.1 (blockers #24948):
  - derived_rating subtracts EXACTLY the reversal amount_micro
    (partial rating_adjustment 10/100 now subtracts 10, not 100)
  - conservation takes assignments: pool = sum(amount_cap);
    origin of funds proven via capability, carry computed.
Pure: no I/O, no state.
"""

MICRO = 1_000_000

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),
    "task_receipt":     (False, False),
}

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):
    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 _reversals(rows):
    return [r for r in rows if r.get("type") == "reversal" and not r.get("is_fixture")]


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. A rating_adjustment reversal subtracts EXACTLY its
    amount_micro from the target credit (partial reversal 10/100 -> -10),
    not the whole credit. UNIQUE(reverses_event_id, effect) is enforced by
    ledger_store; guarded here too.
    """
    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 _reversals(rows):
        if 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)
                cap = q["rating"]
                amt = min(int(r.get("amount_micro", 0)), cap)
                rating[orig["actor"]] = rating.get(orig["actor"], 0) - amt
                break
    return rating


def conservation(rows, assignments, edge=0):
    """
    Conservation with proof of origin (blocker 3 #24948):
      pool = sum of issued assignment amount_cap
      gross = balance emissions (each already capped by its assignment in store)
      clawbacks reduce paid-out balance
      carry = pool - net_payouts  (computed)
      invariant: net_payouts + carry == pool * (1 - edge)
    A history cannot trivially pass: pool comes from ISSUED CAPABILITY, and
    every emission row must reference a valid assignment (store-enforced);
    if emissions exceed issued caps the invariant breaks.
    """
    pool = int(sum(int(a.get("amount_cap_micro", 0)) for a in (assignments or [])) * (1 - edge))
    gross = sum(quoted_emission(r)["balance"] for r in _emissions(rows))
    claw = sum(int(r.get("amount_micro", 0)) for r in _reversals(rows)
               if r.get("effect") == "balance_clawback")
    net_payouts = gross - claw
    carry = pool - net_payouts
    ok = (carry >= 0) and (net_payouts + carry == pool) and (net_payouts <= gross)
    return ok, {
        "pool_from_caps": pool, "gross": gross, "clawbacks": claw,
        "net_payouts": net_payouts, "carry": carry,
        "invariant": f"{net_payouts} + {carry} == {pool}",
    }
