#!/usr/bin/env python3
"""
leadsheet.py — LEAD-SHEET TIE-OUT: group trial-balance accounts to lead-sheet lines, and prove
each group's sum ties to the reported financial-statement line. An account whose group is not
one of the reported lead-sheet lines is a TYPED REFUSAL — it is never silently dropped from the
tie-out (a dropped account is exactly the failure mode that lets a real misstatement hide).

Contract: integer pence, strict isinstance(int) with bool excluded. Never raises. The tool's
independent oracle uses a DIFFERENT traversal: instead of the fn's single dict-accumulation pass
over accounts, it re-scans the FULL account list once per reported group (nested-loop
regrouping) and re-derives the unmapped-account check via set membership instead of a per-item
loop.
"""


def _validate(inp: dict):
    """Returns (accounts, reported) or (None, None) with an error string."""
    if "accounts" not in inp:
        return None, None, "missing field: accounts"
    accounts = inp["accounts"]
    if not isinstance(accounts, list):
        return None, None, "field 'accounts' must be a list"
    if len(accounts) == 0:
        return None, None, "empty account population: nothing to tie to the lead sheet"

    if "reported" not in inp:
        return None, None, "missing field: reported"
    reported = inp["reported"]
    if not isinstance(reported, dict) or not reported:
        return None, None, "field 'reported' must be a non-empty record of {group: amount_pence}"

    for g, v in reported.items():
        if not isinstance(g, str) or not g.strip():
            return None, None, f"reported has a non-string or empty group key: {g!r}"
        if isinstance(v, bool) or not isinstance(v, int):
            return None, None, (f"reported[{g!r}] must be an integer (money is integer pence), "
                                 f"got {type(v).__name__}")

    for i, a in enumerate(accounts):
        if not isinstance(a, dict):
            return None, None, f"accounts[{i}] must be a record (a dict), got {type(a).__name__}"
        for field in ("id", "group", "amount_pence"):
            if field not in a:
                return None, None, f"accounts[{i}] missing field: {field}"
        if not isinstance(a["id"], str) or not a["id"].strip():
            return None, None, f"accounts[{i}].id must be a non-empty string"
        if not isinstance(a["group"], str) or not a["group"].strip():
            return None, None, f"accounts[{i}] ({a['id']!r}).group must be a non-empty string"
        amt = a["amount_pence"]
        if isinstance(amt, bool) or not isinstance(amt, int):
            return None, None, (f"accounts[{i}] ({a['id']!r}).amount_pence must be an integer "
                                 f"(money is integer pence), got {type(amt).__name__}")

    # the typed, never-silently-dropped refusal: every account's group must be a reported line
    for a in accounts:
        if a["group"] not in reported:
            return None, None, (f"unmapped account: {a['id']!r} carries group {a['group']!r}, "
                                 f"which is not one of the reported lead-sheet lines "
                                 f"{sorted(reported)} — refusing rather than dropping it silently")

    return accounts, reported, None


# ---------------------------------------------------------------------------
# Tool: leadsheet_tie
# ---------------------------------------------------------------------------

def leadsheet_tie(inp: dict) -> dict:
    """value: sorted tuple of (group, computed_pence, reported_pence, tie_bool) — one row per
    reported lead-sheet line, computed via a single dict-accumulation pass over accounts."""
    accounts, reported, err = _validate(inp)
    if err:
        return {"refuse": err}

    computed = {g: 0 for g in reported}
    for a in accounts:
        computed[a["group"]] += a["amount_pence"]

    rows = tuple(sorted(
        (g, computed[g], reported[g], computed[g] == reported[g])
        for g in reported
    ))
    n_tie = sum(1 for _, _, _, t in rows if t)
    return {
        "value": rows,
        "unit": "rows",
        "provenance": (f"leadsheet_tie: {len(rows)} lead-sheet lines, {n_tie} tie exactly, "
                       f"{len(rows) - n_tie} do not: {rows}"),
    }


def oracle_leadsheet_tie(inp: dict):
    """Independent path: unmapped-account check via SET membership (not the fn's per-item loop);
    per-group sums via a nested full re-scan of the account list (one pass per reported group,
    not one shared accumulation pass)."""
    accounts, reported, err = _validate(inp)
    if err:
        return None

    account_groups = {a["group"] for a in accounts}
    if not account_groups.issubset(set(reported)):
        return None  # _validate already proved this can't happen; defensive symmetry only

    rows = []
    for g in sorted(reported):
        total = 0
        for a in accounts:
            if a["group"] == g:
                total += a["amount_pence"]
        rows.append((g, total, reported[g], total == reported[g]))
    return tuple(rows)


# ---------------------------------------------------------------------------
# TOOLS registry
# ---------------------------------------------------------------------------

_BASE_ACCOUNTS = [
    {"id": "A1", "group": "Cash", "amount_pence": 500000},
    {"id": "A2", "group": "Cash", "amount_pence": 200000},
    {"id": "A3", "group": "AP", "amount_pence": -300000},
]

TOOLS = {
    "leadsheet_tie": {
        "fn": leadsheet_tie,
        "oracle": oracle_leadsheet_tie,
        "cases": [
            # Anchor: both lines tie exactly
            ({"accounts": _BASE_ACCOUNTS, "reported": {"Cash": 700000, "AP": -300000}},
             (("AP", -300000, -300000, True), ("Cash", 700000, 700000, True))),
            # AP is misstated on the reported side: does not tie
            ({"accounts": _BASE_ACCOUNTS, "reported": {"Cash": 700000, "AP": -250000}},
             (("AP", -300000, -250000, False), ("Cash", 700000, 700000, True))),
            # A reported line with zero accounts still appears, computed 0
            ({"accounts": _BASE_ACCOUNTS, "reported": {"Cash": 700000, "AP": -300000, "Accrued": 0}},
             (("AP", -300000, -300000, True), ("Accrued", 0, 0, True), ("Cash", 700000, 700000, True))),
            # refuse: unmapped account (never silently dropped)
            ({"accounts": _BASE_ACCOUNTS + [{"id": "A9", "group": "Misc", "amount_pence": 1000}],
              "reported": {"Cash": 700000, "AP": -300000}}, {"refuse": "x"}),
            # refuse: missing field
            ({"accounts": [{"id": "A1", "group": "Cash"}], "reported": {"Cash": 0}}, {"refuse": "x"}),
            # refuse: bool amount_pence (coercion poison)
            ({"accounts": [{"id": "A1", "group": "Cash", "amount_pence": True}],
              "reported": {"Cash": 1}}, {"refuse": "x"}),
            # refuse: reported value not int
            ({"accounts": _BASE_ACCOUNTS, "reported": {"Cash": "700000", "AP": -300000}},
             {"refuse": "x"}),
            # refuse: empty accounts
            ({"accounts": [], "reported": {"Cash": 0}}, {"refuse": "x"}),
        ],
    },
}


# ---------------------------------------------------------------------------
# Self-test
# ---------------------------------------------------------------------------

if __name__ == "__main__":
    failures = []
    count = 0
    for name, spec in TOOLS.items():
        fn, oracle = spec["fn"], spec["oracle"]
        for inp, expected in spec["cases"]:
            count += 1
            out = fn(inp)
            if isinstance(expected, dict) and "refuse" in expected:
                if "refuse" not in out:
                    failures.append(f"FAIL [{name}] expected refuse, got {out!r} for {inp}")
                continue
            if "refuse" in out:
                failures.append(f"FAIL [{name}] got refuse on valid input {inp}: {out['refuse']}")
                continue
            if out["value"] != expected:
                failures.append(f"FAIL [{name}] fn={out['value']!r} expected={expected!r} inp={inp}")
            ov = oracle(inp)
            if ov != expected:
                failures.append(f"FAIL [{name}] oracle={ov!r} expected={expected!r} inp={inp}")
    if failures:
        for f in failures:
            print(f)
        raise SystemExit(f"FAILED {len(failures)} of {count}")
    print(f"ALL PASS n={count}")
