#!/usr/bin/env python3
"""Original double-entry teaching lab. Python 3.11+ standard library.
Actual in-memory SQLite; fictional USD minor units; no bank or payment adapter.
Balanced postings, business meaning, atomicity and operation identity differ.
"""
import argparse
import json
import platform
import sqlite3

CASES = ("correct", "wrong-account", "wrong-party", "duplicate-naive", "duplicate-guarded", "partial-naive", "partial-atomic", "reversal")


def line(account, side, amount, party=None):
    return {"account": account, "party": party, "side": side, "amount_minor": amount}


def receipt_lines(amount, credit_account="receivable", party="A"):
    return [line("cash", "debit", amount), line(credit_account, "credit", amount, party if credit_account == "receivable" else None)]


def open_ledger(amount):
    db = sqlite3.connect(":memory:", isolation_level=None)
    db.row_factory = sqlite3.Row
    db.execute("PRAGMA foreign_keys=ON")
    db.executescript("""
      CREATE TABLE journal (
        journal_id INTEGER PRIMARY KEY, operation_key TEXT NOT NULL,
        kind TEXT NOT NULL CHECK(kind IN ('invoice','receipt','reversal','correction')),
        source_ref TEXT NOT NULL,
        reverses_journal_id INTEGER UNIQUE REFERENCES journal(journal_id));
      CREATE TABLE entry (
        entry_id INTEGER PRIMARY KEY,
        journal_id INTEGER NOT NULL REFERENCES journal(journal_id),
        account TEXT NOT NULL CHECK(account IN ('cash','receivable','revenue')),
        party TEXT,
        side TEXT NOT NULL CHECK(side IN ('debit','credit')),
        amount_minor INTEGER NOT NULL CHECK(typeof(amount_minor)='integer' AND amount_minor>0),
        CHECK((account='receivable' AND party IS NOT NULL AND party IN ('A','B')) OR (account!='receivable' AND party IS NULL)));
      CREATE TABLE operation_receipt (
        operation_key TEXT PRIMARY KEY, exact_payload TEXT NOT NULL,
        journal_id INTEGER NOT NULL REFERENCES journal(journal_id));
    """)
    # A supplied already-booked invoice; the lab does not determine when a real
    # business may recognize revenue. Collection should clear this receivable.
    result = post(db, "INVOICE-A", "invoice", "INVOICE-A", [line("receivable", "debit", amount, "A"), line("revenue", "credit", amount)])
    if result["status"] != "applied":
        raise RuntimeError("Opening invoice did not post")
    return db


def post(db, operation_key, kind, source_ref, lines, *, atomic=True, deduplicate=True, fail_after_first=False, reverses=None):
    payload = json.dumps({"kind": kind, "source_ref": source_ref, "lines": lines, "reverses": reverses}, sort_keys=True, separators=(",", ":"))
    if atomic:
        db.execute("BEGIN IMMEDIATE")
    try:
        if deduplicate:
            previous = db.execute("SELECT exact_payload,journal_id FROM operation_receipt WHERE operation_key=?", (operation_key,)).fetchone()
            if previous:
                if atomic: db.execute("ROLLBACK")
                return {"status": "replayed" if previous["exact_payload"] == payload else "conflict", "journal_id": previous["journal_id"], "error": None}
        journal_id = db.execute("INSERT INTO journal(operation_key,kind,source_ref,reverses_journal_id) VALUES (?,?,?,?)", (operation_key, kind, source_ref, reverses)).lastrowid
        for index, row in enumerate(lines):
            db.execute("INSERT INTO entry(journal_id,account,party,side,amount_minor) VALUES (?,?,?,?,?)", (journal_id, row["account"], row["party"], row["side"], row["amount_minor"]))
            if fail_after_first and index == 0:
                raise RuntimeError("injected_after_first_line")
        total = db.execute("SELECT COUNT(*) n,SUM(CASE side WHEN 'debit' THEN amount_minor ELSE -amount_minor END) net FROM entry WHERE journal_id=?", (journal_id,)).fetchone()
        if total["n"] < 2 or total["net"] != 0:
            raise RuntimeError("unbalanced_journal")
        if deduplicate:
            db.execute("INSERT INTO operation_receipt VALUES (?,?,?)", (operation_key, payload, journal_id))
        if atomic: db.execute("COMMIT")
        return {"status": "applied", "journal_id": journal_id, "error": None}
    except (RuntimeError, sqlite3.Error) as error:
        if atomic and db.in_transaction: db.execute("ROLLBACK")
        return {"status": "rolled_back" if atomic else "partial_committed", "journal_id": None, "error": str(error)}


def observe(db, amount):
    entries = [dict(row) for row in db.execute("SELECT * FROM entry ORDER BY entry_id")]
    journals = [dict(row) for row in db.execute("SELECT * FROM journal ORDER BY journal_id")]
    for journal in journals:
        rows = [e for e in entries if e["journal_id"] == journal["journal_id"]]
        journal["debits_minor"] = sum(e["amount_minor"] for e in rows if e["side"] == "debit")
        journal["credits_minor"] = sum(e["amount_minor"] for e in rows if e["side"] == "credit")
        journal["balanced"] = len(rows) >= 2 and journal["debits_minor"] == journal["credits_minor"]
    actual = {"cash": 0, "receivable_A": 0, "receivable_B": 0, "revenue": 0}
    for e in entries:
        key = "receivable_" + e["party"] if e["account"] == "receivable" else e["account"]
        actual[key] += e["amount_minor"] if e["side"] == "debit" else -e["amount_minor"]
    # Independent supplied target for exactly one collection of the booked A
    # invoice. This is not derived from journal balance or a successful post.
    expected = {"cash": amount, "receivable_A": 0, "receivable_B": 0, "revenue": -amount}
    differences = [{"bucket": key, "expected_net_debit_minor": expected[key], "actual_net_debit_minor": actual[key]} for key in expected if actual[key] != expected[key]]
    return {"journals": journals, "entries": entries, "operation_receipts": [dict(row) for row in db.execute("SELECT operation_key,journal_id FROM operation_receipt ORDER BY operation_key")], "trial_net_debit_minor": sum(actual.values()), "every_journal_balanced": all(j["balanced"] for j in journals), "actual_net_debit_minor": actual, "expected_net_debit_minor": expected, "business_differences": differences, "matches_supplied_collection_target": not differences}


def run_case(name, amount):
    db = open_ledger(amount)
    try:
        before = observe(db, amount)
        calls = []
        if name == "wrong-account":
            calls.append(post(db, "PAY-1", "receipt", "PAY-1", receipt_lines(amount, "revenue")))
        elif name == "wrong-party":
            calls.append(post(db, "PAY-1", "receipt", "PAY-1", receipt_lines(amount, party="B")))
        elif name.startswith("duplicate-"):
            guarded = name == "duplicate-guarded"
            for _ in range(2):
                calls.append(post(db, "PAY-1", "receipt", "PAY-1", receipt_lines(amount), deduplicate=guarded))
            if guarded:
                calls.append(post(db, "PAY-1", "receipt", "PAY-1", receipt_lines(amount, party="B")))
        elif name.startswith("partial-"):
            calls.append(post(db, "PAY-1", "receipt", "PAY-1", receipt_lines(amount), atomic=name == "partial-atomic", deduplicate=False, fail_after_first=True))
        elif name == "reversal":
            bad = post(db, "PAY-1", "receipt", "PAY-1", receipt_lines(amount, "revenue"))
            calls.append(bad)
            if bad["status"] != "applied":
                raise RuntimeError("Expected wrong-account fixture did not post")
            wrong_before_correction = observe(db, amount)
            original = [dict(row) for row in db.execute("SELECT account,party,side,amount_minor FROM entry WHERE journal_id=? ORDER BY entry_id", (bad["journal_id"],))]
            inverse = [{**row, "side": "credit" if row["side"] == "debit" else "debit"} for row in original]
            calls.append(post(db, "REVERSE-PAY-1", "reversal", "PAY-1", inverse, reverses=bad["journal_id"]))
            after_reversal = observe(db, amount)
            calls.append(post(db, "CORRECT-PAY-1", "correction", "PAY-1", receipt_lines(amount)))
        else:
            calls.append(post(db, "PAY-1", "receipt", "PAY-1", receipt_lines(amount)))
        result = {"case": name, "before": before, "post_results": calls, "after": observe(db, amount)}
        if name == "reversal":
            result.update({"wrong_before_correction": wrong_before_correction, "after_reversal_before_correction": after_reversal})
        return result
    finally:
        db.close()


def bounded_amount(raw):
    if not raw.isascii() or not raw.isdigit() or len(raw) > 7 or not 1 <= int(raw) <= 1_000_000:
        raise argparse.ArgumentTypeError("amount-minor must be an integer from 1 to 1000000")
    return int(raw)


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--case", choices=CASES)
    parser.add_argument("--amount-minor", type=bounded_amount, default=12500)
    args = parser.parse_args()
    cases = [args.case] if args.case else CASES
    print(json.dumps({"schema": "lockedin.ledger-posting-lab/v1", "python": platform.python_version(), "sqlite": sqlite3.sqlite_version, "currency": "USD", "amount_minor": args.amount_minor, "instruction": {"invoice_ref": "INVOICE-A", "party": "A", "operation_ref": "PAY-1", "expected_effect": "Collect the already-booked invoice exactly once"}, "scope": "Synthetic booked invoice and collection target; local ledger entries only; no payment or tax decision", "cases": [run_case(name, args.amount_minor) for name in cases]}, indent=2))


if __name__ == "__main__":
    main()
