#!/usr/bin/env python3
"""
liabilities.py — SEARCH FOR UNRECORDED LIABILITIES, the profession's named "boring test":
scan post-period-end cash disbursements; flag any whose SERVICE PERIOD predates period-end (the
liability existed at the balance-sheet date, so it should have been accrued) AND whose amount
exceeds a stated threshold.

Contract: integer pence, strict isinstance(int) with bool excluded. Dates are ISO 8601
YYYY-MM-DD strings, validated by a real calendar (not just a regex) so a malformed date (e.g.
"2025-04-31" — April has 30 days) refuses instead of silently mis-ordering. Never raises.
The tool's own independent oracle uses a DIFFERENT traversal (two-pass instead of one) and a
DIFFERENT date-comparison algorithm (raw zero-padded ISO string comparison instead of the fn's
parsed-tuple comparison) — not the fn re-called.
"""

_DAYS_IN_MONTH = {1: 31, 2: 28, 3: 31, 4: 30, 5: 31, 6: 30, 7: 31, 8: 31, 9: 30, 10: 31, 11: 30, 12: 31}


def _is_leap(y: int) -> bool:
    return (y % 4 == 0 and y % 100 != 0) or (y % 400 == 0)


def _parse_date(s):
    """ISO 'YYYY-MM-DD' -> (y, m, d) tuple, or None if malformed (real calendar validation,
    including Feb 29 on leap years only) — the fn's comparison path."""
    if not isinstance(s, str) or len(s) != 10 or s[4] != "-" or s[7] != "-":
        return None
    y_s, m_s, d_s = s[0:4], s[5:7], s[8:10]
    if not (y_s.isdigit() and m_s.isdigit() and d_s.isdigit()):
        return None
    y, m, d = int(y_s), int(m_s), int(d_s)
    if not (1 <= m <= 12):
        return None
    max_d = _DAYS_IN_MONTH[m] + (1 if (m == 2 and _is_leap(y)) else 0)
    if not (1 <= d <= max_d):
        return None
    return (y, m, d)


def _valid_iso_date_str(s) -> bool:
    """Independent (oracle-side) calendar check that reuses NOTHING from _parse_date's control
    flow shape beyond the same day-table data — used only to decide validity; the oracle then
    compares dates as raw strings, never as tuples."""
    return _parse_date(s) is not None


def _validate(inp: dict):
    """Returns (period_end, threshold, payments) or (None, None, None) with an error string."""
    if "period_end" not in inp:
        return None, None, None, "missing field: period_end"
    period_end = inp["period_end"]
    if _parse_date(period_end) is None:
        return None, None, None, f"period_end is not a valid ISO calendar date: {period_end!r}"

    if "threshold_pence" not in inp:
        return None, None, None, "missing field: threshold_pence"
    threshold = inp["threshold_pence"]
    if isinstance(threshold, bool) or not isinstance(threshold, int):
        return None, None, None, (f"field 'threshold_pence' must be an integer (money is "
                                   f"integer pence), got {type(threshold).__name__}")
    if threshold < 0:
        return None, None, None, "threshold_pence must be >= 0"

    if "payments" not in inp:
        return None, None, None, "missing field: payments"
    payments = inp["payments"]
    if not isinstance(payments, list):
        return None, None, None, "field 'payments' must be a list"

    for i, p in enumerate(payments):
        if not isinstance(p, dict):
            return None, None, None, f"payments[{i}] must be a record (a dict), got {type(p).__name__}"
        for field in ("id", "payment_date", "service_period_end", "amount_pence"):
            if field not in p:
                return None, None, None, f"payments[{i}] missing field: {field}"
        if not isinstance(p["id"], str) or not p["id"].strip():
            return None, None, None, f"payments[{i}].id must be a non-empty string"
        if _parse_date(p["payment_date"]) is None:
            return None, None, None, (f"payments[{i}] ({p['id']!r}).payment_date is not a valid "
                                       f"ISO calendar date: {p['payment_date']!r}")
        if _parse_date(p["service_period_end"]) is None:
            return None, None, None, (f"payments[{i}] ({p['id']!r}).service_period_end is not a "
                                       f"valid ISO calendar date: {p['service_period_end']!r}")
        amt = p["amount_pence"]
        if isinstance(amt, bool) or not isinstance(amt, int):
            return None, None, None, (f"payments[{i}] ({p['id']!r}).amount_pence must be an "
                                       f"integer (money is integer pence), got {type(amt).__name__}")
        if amt < 0:
            return None, None, None, f"payments[{i}] ({p['id']!r}).amount_pence must be >= 0"

    ids = [p["id"] for p in payments]
    if len(ids) != len(set(ids)):
        return None, None, None, "payments contains duplicate ids — each population item must be traceable"

    return period_end, threshold, payments, None


# ---------------------------------------------------------------------------
# Tool: unrecorded_liability_flags
# ---------------------------------------------------------------------------

def unrecorded_liability_flags(inp: dict) -> dict:
    """Flag a payment iff:
      (a) payment_date strictly AFTER period_end   — it was disbursed post-period
      (b) service_period_end <= period_end          — the obligation existed at period-end
      (c) amount_pence strictly EXCEEDS threshold_pence
    value: sorted tuple of flagged payment ids (deterministic, exact-comparable)."""
    period_end, threshold, payments, err = _validate(inp)
    if err:
        return {"refuse": err}

    pe = _parse_date(period_end)
    flagged = []
    for p in payments:
        pay_d = _parse_date(p["payment_date"])
        svc_d = _parse_date(p["service_period_end"])
        if pay_d > pe and svc_d <= pe and p["amount_pence"] > threshold:
            flagged.append(p["id"])
    flagged_sorted = tuple(sorted(flagged))
    return {
        "value": flagged_sorted,
        "unit": "ids",
        "provenance": (f"unrecorded_liability_flags: period_end={period_end} "
                       f"threshold_pence={threshold}; {len(flagged_sorted)} of {len(payments)} "
                       f"post-period disbursements flagged: {flagged_sorted}"),
    }


def _iso_pad(s: str) -> str:
    """Zero-padded ISO string is already zero-padded by _parse_date's validation (YYYY-MM-DD
    fixed width); this identity function marks that the oracle's comparison operates on the
    RAW STRING, not a parsed tuple — the genuinely different algorithm path."""
    return s


def oracle_unrecorded_liability_flags(inp: dict):
    """Independent path: TWO separate passes (candidates-by-timing, then filter-by-amount)
    instead of the fn's single pass, and STRING comparison of the zero-padded ISO dates
    (valid because ISO 8601 sorts lexicographically) instead of the fn's parsed-tuple compare."""
    period_end, threshold, payments, err = _validate(inp)
    if err:
        return None
    pe_s = _iso_pad(period_end)

    # pass 1: timing-only candidates (post-period AND service predates period-end)
    candidates = []
    for p in payments:
        pay_s = _iso_pad(p["payment_date"])
        svc_s = _iso_pad(p["service_period_end"])
        if pay_s > pe_s and svc_s <= pe_s:
            candidates.append(p["id"])

    # pass 2: independently re-scan for amount, keyed by id (not reusing pass-1's dict objects)
    by_id = {}
    for p in payments:
        by_id[p["id"]] = p["amount_pence"]
    flagged = [pid for pid in candidates if by_id[pid] > threshold]
    return tuple(sorted(flagged))


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

_BASE_PAYMENTS = [
    {"id": "P1", "payment_date": "2025-05-05", "service_period_end": "2025-04-20", "amount_pence": 75000},
    {"id": "P2", "payment_date": "2025-05-03", "service_period_end": "2025-05-01", "amount_pence": 90000},
    {"id": "P3", "payment_date": "2025-04-28", "service_period_end": "2025-04-10", "amount_pence": 80000},
    {"id": "P4", "payment_date": "2025-05-10", "service_period_end": "2025-04-25", "amount_pence": 30000},
]

TOOLS = {
    "unrecorded_liability_flags": {
        "fn": unrecorded_liability_flags,
        "oracle": oracle_unrecorded_liability_flags,
        "cases": [
            # Anchor: only P1 is post-period + predates period-end + exceeds $500 threshold.
            # P2's service period is AFTER period-end (current-period expense, not a search hit).
            # P3 was paid BEFORE period-end (already on the books, not a search candidate).
            # P4 exceeds neither timing test issue but is under the threshold.
            ({"period_end": "2025-04-30", "threshold_pence": 50000, "payments": _BASE_PAYMENTS},
             ("P1",)),
            # Lower the threshold: P4 now also qualifies (30000 > 20000)
            ({"period_end": "2025-04-30", "threshold_pence": 20000, "payments": _BASE_PAYMENTS},
             ("P1", "P4")),
            # Empty population: valid, nothing to flag
            ({"period_end": "2025-04-30", "threshold_pence": 50000, "payments": []}, ()),
            # refuse: malformed calendar date (April has 30 days) — the exact shape of real
            # dirty data seen in experiments/cpa_reconcile_demo/bank.csv ("04/31/2025")
            ({"period_end": "2025-04-30", "threshold_pence": 50000,
              "payments": [{"id": "PX", "payment_date": "2025-04-31",
                            "service_period_end": "2025-04-10", "amount_pence": 60000}]},
             {"refuse": "x"}),
            # refuse: missing field
            ({"period_end": "2025-04-30", "threshold_pence": 50000,
              "payments": [{"id": "PY", "payment_date": "2025-05-01",
                            "service_period_end": "2025-04-10"}]},
             {"refuse": "x"}),
            # refuse: bool amount_pence (coercion poison)
            ({"period_end": "2025-04-30", "threshold_pence": 50000,
              "payments": [{"id": "PZ", "payment_date": "2025-05-01",
                            "service_period_end": "2025-04-10", "amount_pence": True}]},
             {"refuse": "x"}),
            # refuse: negative threshold
            ({"period_end": "2025-04-30", "threshold_pence": -1, "payments": []}, {"refuse": "x"}),
            # refuse: duplicate ids (population not traceable)
            ({"period_end": "2025-04-30", "threshold_pence": 50000,
              "payments": [{"id": "P1", "payment_date": "2025-05-01",
                            "service_period_end": "2025-04-01", "amount_pence": 60000},
                           {"id": "P1", "payment_date": "2025-05-02",
                            "service_period_end": "2025-04-02", "amount_pence": 70000}]},
             {"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}")
