#!/usr/bin/env python3
"""
rollforward.py — the first-year's "bread and butter": beginning balance + additions - disposals
= ending, tied to the current-period trial balance. Used on fixed assets, reserves/allowances,
and any continuity schedule an auditor foots every engagement.

Contract (mirrors skills/accounting): every amount is INTEGER PENCE, strict isinstance(int),
bool EXCLUDED (never coerced). Each fn returns {"value": ..., "unit": "pence"|"bool",
"provenance": str} or {"refuse": "<typed reason>"}. Never raises. Each tool carries an
INDEPENDENT oracle computed by a genuinely different arithmetic path: grade-school digit-string
addition/subtraction (no Python +/- operator on the pence values at all), not the fn re-called.
"""

_REQUIRED_ENDING = {"beginning_balance", "additions", "disposals"}
_REQUIRED_CHECK = _REQUIRED_ENDING | {"tb_ending_balance"}


def _int_pence(inp: dict, key: str, *, allow_negative: bool):
    """(value, None) or (None, error). Strict isinstance(int); bool excluded (never int()
    coerced — a corrupted True/False or a float must refuse, not be silently read as pence)."""
    if key not in inp:
        return None, f"missing field: {key}"
    v = inp[key]
    if isinstance(v, bool) or not isinstance(v, int):
        return None, (f"field {key!r} must be an integer (money is integer pence), got "
                       f"{type(v).__name__} — refusing rather than rounding or truncating")
    if not allow_negative and v < 0:
        return None, f"field {key!r} must be >= 0"
    return v, None


def _validate_ending_fields(inp: dict):
    """beginning_balance may be any sign is NOT allowed here — a rollforward's opening balance
    is a stated fixed-asset/reserve balance and is required >= 0 (the class this tool covers).
    additions/disposals are magnitudes, required >= 0 (a disposal is entered as a positive
    amount REMOVED, never a signed delta) — same discipline as accounting/depreciation.py's
    strict-int, non-negative fields."""
    for field in ("beginning_balance", "additions", "disposals"):
        if field not in inp:
            return None, f"missing field: {field}"
    beg, err = _int_pence(inp, "beginning_balance", allow_negative=False)
    if err:
        return None, err
    add, err = _int_pence(inp, "additions", allow_negative=False)
    if err:
        return None, err
    dis, err = _int_pence(inp, "disposals", allow_negative=False)
    if err:
        return None, err
    return (beg, add, dis), None


# ---------------------------------------------------------------------------
# grade-school digit-string arithmetic — the INDEPENDENT oracle path (S041 F003). No Python
# +/- operator ever touches the pence values here; this is a genuinely different algorithm
# from the fn's direct int arithmetic, not a re-call of it.
# ---------------------------------------------------------------------------

def _str_add_nonneg(a: int, b: int) -> int:
    """a + b for a,b >= 0, via manual digit-by-digit grade-school addition on decimal strings."""
    sa, sb = str(a), str(b)
    n = max(len(sa), len(sb))
    sa, sb = sa.zfill(n), sb.zfill(n)
    carry = 0
    digits = []
    for i in range(n - 1, -1, -1):
        d = (ord(sa[i]) - 48) + (ord(sb[i]) - 48) + carry
        digits.append(d % 10)
        carry = d // 10
    if carry:
        digits.append(carry)
    return int("".join(str(d) for d in reversed(digits)))


def _str_sub(a: int, b: int) -> int:
    """a - b (any sign result) via manual digit-by-digit grade-school subtraction on decimal
    strings of the magnitudes, then a manual sign fixup — never Python's - operator on a/b."""
    if a >= 0 and b >= 0:
        if a >= b:
            sa, sb = str(a), str(b)
            n = max(len(sa), len(sb))
            sa, sb = sa.zfill(n), sb.zfill(n)
            borrow = 0
            digits = []
            for i in range(n - 1, -1, -1):
                d = (ord(sa[i]) - 48) - (ord(sb[i]) - 48) - borrow
                if d < 0:
                    d += 10
                    borrow = 1
                else:
                    borrow = 0
                digits.append(d)
            while len(digits) > 1 and digits[-1] == 0:
                digits.pop()
            return int("".join(str(d) for d in reversed(digits)))
        return -_str_sub(b, a)
    raise ValueError("string-decimal subtraction oracle expects non-negative magnitudes")


def _oracle_ending(beg: int, add: int, dis: int) -> int:
    """Independent path: (beginning STR-ADD additions) then STR-SUB disposals — grade-school
    digit arithmetic, zero use of Python's native + / - on the pence magnitudes."""
    grown = _str_add_nonneg(beg, add)
    return _str_sub(grown, dis)


# ---------------------------------------------------------------------------
# Tool 1: rollforward_ending
# ---------------------------------------------------------------------------

def rollforward_ending(inp: dict) -> dict:
    """ending = beginning_balance + additions - disposals. Ending MAY be negative (an
    over-disposal is a client error the tool surfaces, not a case to silently refuse)."""
    fields, err = _validate_ending_fields(inp)
    if err:
        return {"refuse": err}
    beg, add, dis = fields
    ending = beg + add - dis
    return {
        "value": ending,
        "unit": "pence",
        "provenance": f"rollforward_ending: {beg} + {add} - {dis} = {ending}",
    }


def oracle_rollforward_ending(inp: dict):
    fields, err = _validate_ending_fields(inp)
    if err:
        return None
    beg, add, dis = fields
    return _oracle_ending(beg, add, dis)


# ---------------------------------------------------------------------------
# Tool 2: rollforward_check — ties the computed ending to the client's stated TB ending balance
# ---------------------------------------------------------------------------

def rollforward_check(inp: dict) -> dict:
    """value = True iff beginning + additions - disposals == tb_ending_balance. Compounds on
    rollforward_ending's arithmetic (same base fields) but is its OWN tool with its OWN oracle
    (never silently reuses rollforward_ending's fn output as ground truth)."""
    fields, err = _validate_ending_fields(inp)
    if err:
        return {"refuse": err}
    tb, err = _int_pence(inp, "tb_ending_balance", allow_negative=True)
    if err:
        return {"refuse": err}
    beg, add, dis = fields
    computed = beg + add - dis
    diff = tb - computed
    tie = diff == 0
    prov = (f"rollforward_check: computed ending {computed} vs TB ending {tb}; "
            f"{'ties exactly' if tie else f'difference {diff}'}")
    return {"value": tie, "unit": "bool", "provenance": prov}


def oracle_rollforward_check(inp: dict):
    fields, err = _validate_ending_fields(inp)
    if err:
        return None
    tb, err = _int_pence(inp, "tb_ending_balance", allow_negative=True)
    if err:
        return None
    beg, add, dis = fields
    # regrouped order (additions - disposals) then STR-ADD to beginning, via the string-decimal
    # path — both a different grouping AND a different (non-native) arithmetic implementation
    # from rollforward_check's fn, which computes beg + add - dis with Python operators in the
    # original left-to-right order.
    net = _str_sub(add, dis)              # may be negative
    if net >= 0:
        computed = _str_add_nonneg(beg, net)
    else:
        computed = _str_sub(beg, -net)
    return computed == tb


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

TOOLS = {
    "rollforward_ending": {
        "fn": rollforward_ending,
        "oracle": oracle_rollforward_ending,
        "cases": [
            # Anchor: £10,000.00 opening + £2,500.00 additions - £1,000.00 disposals = £11,500.00
            ({"beginning_balance": 1000000, "additions": 250000, "disposals": 100000}, 1150000),
            # No additions this period
            ({"beginning_balance": 750000, "additions": 0, "disposals": 50000}, 700000),
            # Over-disposal: ending goes negative (client error, still computed not refused)
            ({"beginning_balance": 10000, "additions": 0, "disposals": 25000}, -15000),
            # refuse: missing field
            ({"beginning_balance": 1000000, "additions": 250000}, {"refuse": "x"}),
            # refuse: float (coercion poison)
            ({"beginning_balance": 1000000.5, "additions": 0, "disposals": 0}, {"refuse": "x"}),
            # refuse: bool
            ({"beginning_balance": 1000000, "additions": True, "disposals": 0}, {"refuse": "x"}),
            # refuse: negative additions (must be a magnitude)
            ({"beginning_balance": 1000000, "additions": -5000, "disposals": 0}, {"refuse": "x"}),
        ],
    },
    "rollforward_check": {
        "fn": rollforward_check,
        "oracle": oracle_rollforward_check,
        "cases": [
            # Anchor: ties exactly
            ({"beginning_balance": 1000000, "additions": 250000, "disposals": 100000,
              "tb_ending_balance": 1150000}, True),
            # Break: TB is off by 10000 pence
            ({"beginning_balance": 1000000, "additions": 250000, "disposals": 100000,
              "tb_ending_balance": 1140000}, False),
            # Ties on a negative ending (over-disposal, but TB agrees)
            ({"beginning_balance": 10000, "additions": 0, "disposals": 25000,
              "tb_ending_balance": -15000}, True),
            # refuse: missing tb_ending_balance
            ({"beginning_balance": 1000000, "additions": 250000, "disposals": 100000},
             {"refuse": "x"}),
            # refuse: bool tb_ending_balance (coercion poison)
            ({"beginning_balance": 1000000, "additions": 0, "disposals": 0,
              "tb_ending_balance": True}, {"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}")
