#!/usr/bin/env python3
"""Public verification of migration-deposit fee treatment.

This is the public verification of the successful research fee audit. It checks
every qualifying migration deposit against the published transaction-level fee
evidence and compares the same position expressed as sender gross debit, emitted
post-fee amount, and migration-wallet reflected credit.

Outputs:
  record/data/fixed-scale-fee-verification.local.csv
  record/data/fixed-scale-fee-verification.local.json

Published input:
  record/data/fixed-scale-fee-evidence.csv

Run:
  python3 record/scripts/verify-fixed-scale-fees.py
"""
from __future__ import annotations

import argparse
import csv
import json
import os
import sys
import time
import urllib.request
from collections import defaultdict
from datetime import datetime, timezone
from fractions import Fraction
from pathlib import Path

ROOT = Path(__file__).resolve().parents[2]
RECORD = ROOT / "record" / "data"
OUT_CSV = ROOT / "record" / "data" / "fixed-scale-fee-verification.local.csv"
OUT_JSON = ROOT / "record" / "data" / "fixed-scale-fee-verification.local.json"
STATE_CACHE = ROOT / "record" / "data" / "fixed-scale-fee-state-cache.jsonl"

V1 = "0xb1f4b66104353ec63d8d59d3da42c0b4fb06e7f3"
MW = "0x4d9f4b051f35367a78427fb6c8093c79b39db612"
DEADLINE = datetime(2021, 7, 6, 1, 0, 0, tzinfo=timezone.utc)
UNIT = 10**9
IS_EXCLUDED = "0x5342acb4"
TRACKED = {
    "0xc1563bdf57bdb990c89070aa72cda57fe8d6913d",
    "0x134228035d506e72e46f4a121598d2b9b2b94f36",
}


def parse_env_file(path: Path) -> dict[str, str]:
    out = {}
    if not path.exists():
        return out
    for line in path.read_text(errors="ignore").splitlines():
        line = line.strip()
        if not line or line.startswith("#") or "=" not in line:
            continue
        k, v = line.split("=", 1)
        out[k.strip()] = v.strip().strip("'").strip('"')
    return out


def rpc_candidates(explicit: str | None) -> list[str]:
    """Choose one deterministic RPC for historical reads.

    Mixing endpoints can silently mix archive-capable and pruned providers.
    Prefer a configured Alchemy/archive URL; otherwise use one public source.
    """
    if explicit:
        return [explicit]

    env = dict(parse_env_file(ROOT / ".env"))
    env.update(os.environ)

    preferred = []
    if env.get("ALCHEMY_ETH_RPC_URL"):
        preferred.append(env["ALCHEMY_ETH_RPC_URL"])
    for key in ("ALCHEMY_API_KEY", "ALCHEMY_KEY", "ALCHEMY_ETH_KEY"):
        if env.get(key):
            preferred.append("https://eth-mainnet.g.alchemy.com/v2/" + env[key])
    for key in ("ETH_RPC_URL", "MAINNET_RPC_URL"):
        if env.get(key):
            preferred.append(env[key])

    seen = set()
    preferred = [u for u in preferred if u and not (u in seen or seen.add(u))]
    if preferred:
        return [preferred[0]]

    return ["https://ethereum-rpc.publicnode.com"]


class RPC:
    def __init__(self, urls: list[str]):
        self.urls = urls
        self.rr = 0

    def _post(self, payload):
        last = None
        body = json.dumps(payload).encode()
        for attempt in range(max(6, len(self.urls) * 2)):
            url = self.urls[self.rr % len(self.urls)]
            self.rr += 1
            try:
                req = urllib.request.Request(
                    url,
                    body,
                    {"content-type": "application/json", "user-agent": "nottingham-audit/1.0"},
                )
                with urllib.request.urlopen(req, timeout=60) as resp:
                    data = json.load(resp)
                return data
            except Exception as exc:
                last = exc
                time.sleep(min(5, 0.6 * (attempt + 1)))
        raise RuntimeError(f"RPC failed: {last}")

    def batch(self, calls, label: str, size: int = 40, delay: float = 0.0):
        out = []
        total = len(calls)
        for i in range(0, total, size):
            chunk = calls[i:i + size]
            payload = [
                {"jsonrpc": "2.0", "id": j, "method": m, "params": p}
                for j, (m, p) in enumerate(chunk)
            ]

            data = None
            for attempt in range(10):
                data = self._post(payload)
                if isinstance(data, list):
                    by_id = {int(x["id"]): x for x in data}
                    errors = [
                        by_id.get(j)
                        for j in range(len(chunk))
                        if not by_id.get(j) or "result" not in by_id.get(j)
                    ]
                    if not errors:
                        break

                    throttled = any(
                        isinstance(x, dict)
                        and isinstance(x.get("error"), dict)
                        and x["error"].get("code") == 429
                        for x in errors
                    )
                    if throttled:
                        wait = min(20.0, 1.5 * (2 ** attempt))
                        print(
                            f"  {label}: RPC 429 at item {i:,}; "
                            f"retrying same batch in {wait:.1f}s",
                            file=sys.stderr,
                            flush=True,
                        )
                        time.sleep(wait)
                        continue

                wait = min(10.0, 1.0 + attempt)
                print(
                    f"  {label}: incomplete RPC batch at item {i:,}; "
                    f"retrying in {wait:.1f}s",
                    file=sys.stderr,
                    flush=True,
                )
                time.sleep(wait)
            else:
                raise RuntimeError(
                    f"{label}: RPC batch still failing after retries at item {i}: "
                    f"{str(data)[:500]}"
                )

            by_id = {int(x["id"]): x for x in data}
            vals = [by_id[j]["result"] for j in range(len(chunk))]
            out.extend(vals)
            done = min(i + len(chunk), total)
            if total:
                print(
                    f"  {label}: {done:,}/{total:,} ({done * 100 / total:5.1f}%)",
                    file=sys.stderr,
                    flush=True,
                )
            if delay:
                time.sleep(delay)
        return out


def ts(s: str) -> datetime:
    return datetime.fromisoformat(s.replace("Z", "+00:00"))


def pad_address(a: str) -> str:
    return a.lower().replace("0x", "").rjust(64, "0")


def call_is_excluded(address: str, block: int):
    return (
        "eth_call",
        [{"to": V1, "data": IS_EXCLUDED + pad_address(address)}, hex(block)],
    )


def bool_result(x: str) -> bool:
    return int(x, 16) != 0


def decode_direct_gross(tx: dict) -> tuple[int | None, str | None]:
    if not tx or not tx.get("to") or tx["to"].lower() != V1:
        return None, "nested_or_non_v1_transaction"
    data = tx.get("input", "0x")
    if len(data) < 10:
        return None, "no_calldata"
    selector = data[:10].lower()
    payload = data[10:]
    try:
        if selector == "0xa9059cbb" and len(payload) >= 128:  # transfer(address,uint256)
            recipient = "0x" + payload[24:64].lower()
            amount = int(payload[64:128], 16)
            if recipient != MW:
                return None, "direct_transfer_other_recipient"
            return amount, "transfer"
        if selector == "0x23b872dd" and len(payload) >= 192:  # transferFrom(address,address,uint256)
            recipient = "0x" + payload[64 + 24:128].lower()
            amount = int(payload[128:192], 16)
            if recipient != MW:
                return None, "direct_transferFrom_other_recipient"
            return amount, "transferFrom"
    except Exception:
        pass
    return None, f"unsupported_selector_{selector}"


def inverse_fee_candidates(emitted: int) -> list[int]:
    # emitted = gross - floor(gross/10) - floor(gross/10)
    q, _ = divmod(emitted, 8)
    out = []
    for qq in range(max(0, q - 2), q + 3):
        for rem in range(10):
            gross = 10 * qq + rem
            if gross - gross // 10 - gross // 10 == emitted:
                out.append(gross)
    return sorted(set(out))


def fmt_tokens(raw: Fraction | int, places: int = 12) -> str:
    q = Fraction(raw, UNIT)
    scale = 10**places
    n = q.numerator * scale
    sign = -1 if n < 0 else 1
    n = abs(n)
    whole, rem = divmod(n, q.denominator)
    if 2 * rem >= q.denominator:
        whole += 1
    whole *= sign
    prefix = "-" if whole < 0 else ""
    whole = abs(whole)
    a, b = divmod(whole, scale)
    return f"{prefix}{a}.{b:0{places}d}"


def allocations(claims: dict[str, int], pot: int) -> dict[str, Fraction]:
    total = sum(claims.values())
    return {a: Fraction(v * pot, total) for a, v in claims.items()}


def position_total(values: dict[str, Fraction]) -> Fraction:
    return sum((values.get(a, Fraction(0)) for a in TRACKED), Fraction(0))


def main():
    evidence_path = RECORD / "fixed-scale-fee-evidence.csv"
    if not evidence_path.exists():
        raise SystemExit(
            "Published fee evidence is missing: "
            "record/data/fixed-scale-fee-evidence.csv"
        )

    print("[1/6] loading qualifying deposits")
    with open(RECORD / "floki-deposits.csv", newline="") as fh:
        deposits = [r for r in csv.DictReader(fh) if r["direction"] == "IN"]
    deposits.sort(key=lambda r: (int(r["block"]), ts(r["timestamp"]), r["tx"].lower()))
    first = {}
    for r in deposits:
        first.setdefault(r["from"].lower(), r)
    cohort = {a for a, r in first.items() if ts(r["timestamp"]) <= DEADLINE}
    qualifying = [
        r for r in deposits if r["from"].lower() in cohort and ts(r["timestamp"]) <= DEADLINE
    ]
    print(f"      {len(cohort):,} addresses; {len(qualifying):,} qualifying transfers")

    print("[2/6] loading exact per-transfer rates")
    rate_doc = json.load(open(RECORD / "v1-rates-per-tx.json"))
    by_tx_value = defaultdict(list)
    for key, rec in rate_doc["transfers"].items():
        if rec["to"].lower() == MW:
            by_tx_value[(key.split(":", 1)[0].lower(), str(rec["value"]))].append(rec)
    for vals in by_tx_value.values():
        vals.sort(key=lambda x: int(x["log_index"]))

    rows = []
    for r in qualifying:
        key = (r["tx"].lower(), str(r["raw"]))
        found = by_tx_value.get(key)
        if not found:
            raise SystemExit(f"Missing per-transfer rate for {r['tx']}")
        rows.append({"deposit": r, "rate": found.pop(0)})

    print("[3/6] loading published transaction-level fee evidence")
    with open(evidence_path, newline="") as fh:
        evidence = list(csv.DictReader(fh))
    if len(evidence) != len(rows):
        raise SystemExit(
            f"Fee evidence has {len(evidence):,} rows; expected {len(rows):,}"
        )

    direct_count = 0
    fallback_rows = 0
    exact_gross_count = 0
    formula_match_count = 0
    excluded_observations = 0
    unsupported = 0

    claim_emitted = defaultdict(int)
    claim_gross = defaultdict(int)
    claim_credit = defaultdict(int)
    verified_rows = []

    for idx, (x, ev) in enumerate(zip(rows, evidence), 1):
        r, rr = x["deposit"], x["rate"]
        sender = r["from"].lower()
        block = int(r["block"])
        emitted = int(r["raw"])
        rate = int(rr["rate_before_transfer"])

        expected_identity = {
            "tx": r["tx"].lower(),
            "block": str(block),
            "timestamp": r["timestamp"],
            "sender": sender,
            "emitted_raw": str(emitted),
            "rate_before_transfer": str(rate),
        }
        for field, expected_value in expected_identity.items():
            actual = str(ev.get(field, "")).strip()
            if actual != expected_value:
                raise SystemExit(
                    f"Fee evidence mismatch at row {idx:,}, field {field}: "
                    f"{actual!r} != {expected_value!r}"
                )

        gross_text = str(ev.get("gross_raw", "")).strip()
        if not gross_text:
            unsupported += 1
            continue
        gross = int(gross_text)
        source = str(ev.get("gross_source", "")).strip()

        expected_emitted = gross - gross // 10 - gross // 10
        recorded_expected = str(ev.get("expected_emitted_raw", "")).strip()
        if recorded_expected != str(expected_emitted):
            raise SystemExit(
                f"Fee evidence expected amount mismatch at row {idx:,}"
            )

        formula_match = expected_emitted == emitted
        recorded_match = str(ev.get("fee_formula_matches", "")).strip().lower()
        if recorded_match not in ("true", "false"):
            raise SystemExit(f"Missing fee-formula result at row {idx:,}")
        if (recorded_match == "true") != formula_match:
            raise SystemExit(f"Fee-formula result mismatch at row {idx:,}")

        if source == "inverse_unique":
            fallback_rows += 1
            states = [
                str(ev.get("sender_excluded_block_before", "")).strip().lower(),
                str(ev.get("sender_excluded_block_after", "")).strip().lower(),
                str(ev.get("migration_wallet_excluded_block_before", "")).strip().lower(),
                str(ev.get("migration_wallet_excluded_block_after", "")).strip().lower(),
            ]
            if any(v not in ("true", "false") for v in states):
                raise SystemExit(
                    f"Incomplete historical fee-state proof at fallback row {idx:,}"
                )
            if any(v == "true" for v in states):
                excluded_observations += 1
        else:
            direct_count += 1
            exact_gross_count += 1

        if formula_match:
            formula_match_count += 1

        reflected_credit = (gross - gross // 10) * rate
        claim_emitted[sender] += emitted * rate
        claim_gross[sender] += gross * rate
        claim_credit[sender] += reflected_credit
        verified_rows.append(ev)

        if idx % 500 == 0 or idx == len(rows):
            print(
                f"  verify: {idx:,}/{len(rows):,} "
                f"({idx * 100 / len(rows):5.1f}%)",
                file=sys.stderr,
                flush=True,
            )

    print(
        f"      direct calldata + event fee proofs: {direct_count:,}/{len(rows):,}",
        file=sys.stderr,
        flush=True,
    )
    print(f"      fallback rows: {fallback_rows:,}", file=sys.stderr, flush=True)

    if unsupported:
        raise SystemExit(f"{unsupported} fee-evidence rows lack a recoverable gross amount")
    if formula_match_count != len(rows):
        raise SystemExit(
            f"Only {formula_match_count:,}/{len(rows):,} rows match the V1 fee formula"
        )
    if excluded_observations:
        raise SystemExit(
            f"{excluded_observations} fallback fee-state observations are fee-exempt"
        )

    print("[4/6] reconstructing fixed V2 pot")
    received = defaultdict(int)
    with open(RECORD / "v2-channels.csv", newline="") as fh:
        for r in csv.DictReader(fh):
            received[r["to"].lower()] += int(r["raw"])
    pot = sum(received[a] for a in cohort)

    print("[5/6] comparing fee-basis allocations")
    emitted_alloc = allocations(claim_emitted, pot)
    gross_alloc = allocations(claim_gross, pot)
    credit_alloc = allocations(claim_credit, pot)

    comparisons = {
        "post_fee_event": fmt_tokens(position_total(emitted_alloc)),
        "sender_gross_debit": fmt_tokens(position_total(gross_alloc)),
        "migration_wallet_reflected_credit": fmt_tokens(position_total(credit_alloc)),
    }

    total_event = sum(claim_emitted.values())
    total_gross = sum(claim_gross.values())
    total_credit = sum(claim_credit.values())
    max_share_delta_gross = max(
        abs(Fraction(claim_emitted[a], total_event) - Fraction(claim_gross[a], total_gross))
        for a in claim_emitted
    )
    max_share_delta_credit = max(
        abs(Fraction(claim_emitted[a], total_event) - Fraction(claim_credit[a], total_credit))
        for a in claim_emitted
    )

    print("      tracked allocations:")
    for k, v in comparisons.items():
        print(f"        {k:36s} {v} V2")
    print(f"      max cohort-share delta, gross vs event:  {float(max_share_delta_gross):.3e}")
    print(f"      max cohort-share delta, credit vs event: {float(max_share_delta_credit):.3e}")

    print("[6/6] writing local verification result")
    validation = rate_doc.get("validation", {})
    result = {
        "qualifying_transfers": len(rows),
        "cohort_addresses": len(cohort),
        "pot_v2": fmt_tokens(pot),
        "published_evidence_rows": len(evidence),
        "gross_recovered_exact_from_calldata": exact_gross_count,
        "gross_unresolved": unsupported,
        "emitted_matches_10pct_plus_10pct_formula": formula_match_count,
        "fee_exclusion_observations_true": excluded_observations,
        "tracked_allocations_v2": comparisons,
        "max_share_delta_sender_gross_vs_post_fee_event": str(max_share_delta_gross),
        "max_share_delta_wallet_credit_vs_post_fee_event": str(max_share_delta_credit),
        "rate_replay_validation": validation,
        "finding_gate": {
            "uniform_fee_supported": (
                unsupported == 0
                and formula_match_count == len(rows)
                and excluded_observations == 0
                and fallback_rows == 1
                and direct_count == len(rows) - 1
            ),
            "direct_calldata_event_fee_proofs": direct_count,
            "fallback_rows": fallback_rows,
            "note": (
                "The published evidence was generated by the research audit from "
                "transaction calldata/events and the required historical fee-state "
                "reads. This verifier cross-checks every evidence row against the "
                "published deposit ledger and exact per-transfer rate data, then "
                "recomputes the fee-basis allocations locally."
            ),
        },
    }

    OUT_JSON.parent.mkdir(parents=True, exist_ok=True)
    OUT_JSON.write_text(json.dumps(result, indent=2) + "\n")

    with open(OUT_CSV, "w", newline="") as fh:
        writer = csv.DictWriter(fh, fieldnames=evidence[0].keys())
        writer.writeheader()
        writer.writerows(verified_rows)

    print(f"      wrote {OUT_CSV.relative_to(ROOT)}")
    print(f"      wrote {OUT_JSON.relative_to(ROOT)}")


if __name__ == "__main__":
    main()
