#!/usr/bin/env python3
"""Compare synthetic origination records and keep local, immutable receipts.

Python 3.10+ and its SQLite library are sufficient. The lab performs no lender,
payment, credit bureau, document delivery, or servicing API calls.
"""
import argparse
from collections import Counter
from datetime import datetime
import hashlib
import json
from pathlib import Path
import sqlite3
from typing import get_args


# Each contract names the fields supplied by the synthetic source and saved
# record. Amounts ending in _cents are integer USD cents. APR is in basis points.
CHECKS = {
    "LO-01": dict(
        product_id=str, product_revision=int, jurisdictions=list, max_principal_cents=int,
        approval_ref=str,
    ),
    "LO-02": dict(
        pricing_schedule=str, product_revision=int, apr_bps=int, fee_cents=int, approval_ref=str,
    ),
    "LO-03": dict(campaign_id=str, creative_revision=int, landing_revision=int, approval_ref=str),
    "LO-04": dict(
        audience_id=str, selection_revision=int, notice_revision=int, eligible_count=int,
        approval_ref=str,
    ),
    "LO-05": dict(
        channel_id=str, cohort_start=str, cohort_end=str, eligible_leads=int, funded_loans=int,
        complaints=int, review_decision=str,
    ),
    "LO-06": dict(lead_id=str, application_start_id=str, attribution_token=str, intake_receipt=str),
    "LO-07": dict(
        application_id=str, request_id=str, original_receipt_at=str, classification=str,
        legal_completeness=str,
    ),
    "LO-08": dict(
        application_id=str, notice_ref=str, original_receipt_at=str, reply_received_at=str,
        reply_due_at=str, required_item=str,
    ),
    "LO-09": dict(
        application_id=str, consumer_token=str, identity_report=str, report_revision=int,
        identity_state=str,
    ),
    "LO-10": dict(
        application_id=str, consumer_token=str, bureau_report=str, permitted_purpose_ref=str,
        report_state=str, credit_score=int | None,
    ),
    "LO-11": dict(
        application_id=str, evidence_id=str, evidence_revision=int, gross_income_cents=int,
        income_period=str, income_basis=str,
    ),
    "LO-12": dict(
        application_id=str, fraud_case=str, evidence_revision=int, disposition=str,
        delegated_reviewer=str,
    ),
    "LO-13": dict(
        application_id=str, snapshot_id=str, snapshot_revision=int, evidence_count=int,
        credit_receipt=str, credit_state=str,
    ),
    "LO-14": dict(
        application_id=str, decision_id=str, decision_revision=int, rule_revision=int,
        approved_principal_cents=int, actual_reasons=list, decision_authority=str,
    ),
    "LO-15": dict(
        application_id=str, exception_id=str, authority_ref=str, exception_outcome=str,
        decision_ref=str,
    ),
    "LO-16": dict(
        application_id=str, offer_id=str, terms_revision=int, response=str, document_ready=bool,
        binding_act=bool,
    ),
    "LO-17": dict(
        application_id=str, decision_id=str, actual_reasons=list, notice_revision=int,
        delivery_receipt=str, sent_at=str,
    ),
    "LO-18": dict(
        application_id=str, terms_revision=int, package_revision=int, principal_cents=int,
        delivery_route=str, risk_notice_state=str,
    ),
    "LO-19": dict(
        application_id=str, agreement_id=str, agreement_revision=int, envelope_id=str,
        execution_state=str, governing_law_evidence=str,
    ),
    "LO-20": dict(
        application_id=str, election=str, election_revision=int, handoff_receipt=str,
        payment_authority_state=str,
    ),
    "LO-21": dict(
        application_id=str, payout_intent_id=str, authorization_revision=int,
        payout_amount_cents=int, currency=str, destination_token=str, prior_attempt_state=str,
        authority_state=str,
    ),
    "LO-22": dict(
        application_id=str, loan_id=str, payout_intent_id=str, original_payment_ref=str,
        payout_amount_cents=int, currency=str, processor_state=str, cash_settlement_state=str,
        recipient_availability=str, settlement_ref=str | None,
    ),
    "LO-23": dict(
        application_id=str, original_payment_ref=str, original_cash_outcome=str,
        investigation_ref=str, remedy=str,
    ),
    "LO-24": dict(
        application_id=str, loan_id=str, opening_id=str, agreement_id=str, settlement_ref=str,
        opening_principal_cents=int, currency=str, booking_stage=str,
    ),
    "LO-25": dict(
        application_id=str, loan_id=str, opening_id=str, package_revision=int, servicing_owner=str,
        receipt_kind=str, acceptance_ref=str | None,
    ),
}


# These values describe the selected synthetic scenarios. They are not lender
# policy, an exhaustive status map, or universal production release conditions.
REQUIRED_STATE = {
    "LO-09": {"identity_state": "accepted"},
    "LO-10": {"report_state": "no_score", "credit_score": None},
    "LO-11": {"income_basis": "gross", "income_period": "month"},
    "LO-12": {"disposition": "cleared"},
    "LO-13": {"credit_state": "accepted"},
    "LO-16": {"binding_act": False},
    "LO-19": {"execution_state": "executed"},
    "LO-20": {"payment_authority_state": "pending_authority_review"},
    "LO-21": {"prior_attempt_state": "none_open", "authority_state": "release_permitted", "currency": "USD"},
    "LO-22": {"cash_settlement_state": "settled", "currency": "USD"},
    "LO-23": {"remedy": "trace_original_hold_replacement"},
    "LO-24": {"booking_stage": "opening_accepted", "currency": "USD"},
    "LO-25": {"receipt_kind": "substantive_acceptance"},
}


def is_typed(value, field_type):
    allowed_types = get_args(field_type) or (field_type,)
    if type(value) not in allowed_types:
        return False
    if type(value) is str:
        return bool(value.strip())
    if type(value) is int:
        return value >= 0
    if type(value) is list:
        return bool(value) and all(type(item) is str and item.strip() for item in value)
    return True


def iso_time(value):
    timestamp = datetime.fromisoformat(value.replace("Z", "+00:00"))
    if timestamp.utcoffset() is None:
        raise ValueError("Timestamps require an explicit offset")
    return timestamp


def compare(process_id, source, saved):
    for field, field_type in CHECKS[process_id].items():
        if field not in source or not is_typed(source[field], field_type):
            return f"invalid_source:{field}"
        if field not in saved or not is_typed(saved[field], field_type):
            return f"invalid_saved:{field}"
        if type(source[field]) is not type(saved[field]) or source[field] != saved[field]:
            return f"mismatch:{field}"
    for field, expected_state in REQUIRED_STATE.get(process_id, {}).items():
        if source[field] != expected_state:
            return f"unconfirmed:{field}"
    if process_id == "LO-08":
        timestamps = {}
        for field in ("reply_received_at", "reply_due_at"):
            try:
                timestamps[field] = iso_time(source[field])
            except ValueError:
                return f"invalid_source:{field}"
        if timestamps["reply_received_at"] > timestamps["reply_due_at"]:
            return "after_original_deadline"
    if process_id == "LO-22" and source["settlement_ref"] is None:
        return "missing_authoritative_settlement"
    if process_id == "LO-25" and source["acceptance_ref"] is None:
        return "missing_substantive_acceptance_ref"
    return None


def open_receipts(db):
    db.executescript("""
        CREATE TABLE IF NOT EXISTS receipts (
            process_id TEXT NOT NULL,
            event_key TEXT NOT NULL,
            entity_key TEXT NOT NULL,
            revision INTEGER NOT NULL,
            payload_digest TEXT NOT NULL,
            payload TEXT NOT NULL,
            result TEXT NOT NULL,
            reason TEXT,
            supersedes TEXT,
            PRIMARY KEY (process_id, event_key)
        );
        CREATE TABLE IF NOT EXISTS quarantined_replays (
            process_id TEXT NOT NULL,
            event_key TEXT NOT NULL,
            payload_digest TEXT NOT NULL,
            payload TEXT NOT NULL,
            PRIMARY KEY (process_id, event_key, payload_digest)
        );
    """)


def record_case(db, case):
    for field in ("case_id", "process_id", "event_key", "entity_key", "source_ref", "saved_ref"):
        if not is_typed(case.get(field), str):
            raise ValueError(f"Expected a nonempty string in {field}")
    if type(case.get("revision")) is not int or case["revision"] < 1:
        raise ValueError("Expected a positive integer receipt revision")
    payload = {key: value for key, value in case.items() if key != "case_id"}
    encoded = json.dumps(payload, sort_keys=True, separators=(",", ":"), allow_nan=False)
    payload_digest = hashlib.sha256(encoded.encode()).hexdigest()
    process_id, event_key, entity_key, revision = (
        case["process_id"], case["event_key"], case["entity_key"], case["revision"]
    )
    key = process_id, event_key
    prior = db.execute("""
        SELECT payload_digest, result, reason FROM receipts WHERE process_id = ? AND event_key = ?
    """, key).fetchone()
    if prior is not None:
        if prior[0] == payload_digest:
            return prior[1], prior[2], "replay"
        db.execute("INSERT OR IGNORE INTO quarantined_replays VALUES (?, ?, ?, ?)", (*key, payload_digest, encoded))
        return "hold", "conflicting_event_payload", "quarantine"

    latest = db.execute("""
        SELECT event_key, revision FROM receipts
        WHERE process_id = ? AND entity_key = ? AND reason IS NOT 'invalid_revision_lineage'
        ORDER BY revision DESC LIMIT 1
    """, (process_id, entity_key)).fetchone()
    supersedes = case.get("supersedes")
    if latest is None:
        valid_revision = revision == 1 and supersedes is None
    else:
        valid_revision = revision == latest[1] + 1 and supersedes == latest[0]
    if not valid_revision:
        reason = "invalid_revision_lineage"
    else:
        reason = compare(process_id, case["source"], case["saved"])
        if process_id == "LO-22" and reason is None and supersedes is not None:
            original = db.execute("""
                SELECT payload FROM receipts
                WHERE process_id = ? AND entity_key = ? AND revision = 1
                  AND reason IS NOT 'invalid_revision_lineage'
            """, (process_id, entity_key)).fetchone()
            original_source = json.loads(original[0])["source"]
            original_identity = (
                "application_id", "loan_id", "payout_intent_id",
                "original_payment_ref", "payout_amount_cents", "currency",
            )
            if any(case["source"][field] != original_source.get(field) for field in original_identity):
                reason = "original_payout_identity_changed"
    result = "hold" if reason else "verified"
    db.execute("INSERT INTO receipts VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", (
        process_id, event_key, entity_key, revision, payload_digest, encoded, result, reason, supersedes,
    ))
    return result, reason, "record"


def operation_readback(db):
    evidence = {}
    for process_id in ("LO-22", "LO-24", "LO-25"):
        scope = process_id, "loan/LN-1042"
        current = db.execute("""
            SELECT revision, result, payload FROM receipts
            WHERE process_id = ? AND entity_key = ? AND reason IS NOT 'invalid_revision_lineage'
            ORDER BY revision DESC LIMIT 1
        """, scope).fetchone()
        if current is None or current[1] != "verified":
            return "hold_incomplete_operation_readback"
        conflict = db.execute("""
            SELECT 1 FROM quarantined_replays AS conflict
            JOIN receipts AS original
              ON original.process_id = conflict.process_id AND original.event_key = conflict.event_key
            WHERE original.process_id = ? AND original.entity_key = ? AND original.revision >= ?
        """, (*scope, current[0])).fetchone()
        if conflict is not None:
            return "hold_incomplete_operation_readback"
        evidence[process_id] = json.loads(current[2])["saved"]

    payout, booking, servicing = (evidence[process_id] for process_id in ("LO-22", "LO-24", "LO-25"))
    identities = {(row["application_id"], row["loan_id"]) for row in evidence.values()}
    if (
        identities != {("APP-1042", "LN-1042")}
        or (payout["settlement_ref"], payout["payout_amount_cents"], payout["currency"]) !=
           (booking["settlement_ref"], booking["opening_principal_cents"], booking["currency"])
        or servicing["opening_id"] != booking["opening_id"]
    ):
        return "hold_mismatched_operation_readback"
    return "servicing_acceptance_verified"


def run(cases, database, *, selected_process):
    results = []
    actions = Counter({"record": 0, "replay": 0, "quarantine": 0})
    with sqlite3.connect(database) as db:
        open_receipts(db)
        db.execute("BEGIN IMMEDIATE")
        for case in cases:
            result, reason, action = record_case(db, case)
            actions[action] += 1
            results.append({"case_id": case["case_id"], "result": result, "reason": reason})
        scope = (selected_process,) if selected_process else ()
        where = " WHERE process_id = ?" if selected_process else ""
        stored_receipts = db.execute("SELECT COUNT(*) FROM receipts" + where, scope).fetchone()[0]
        stored_conflicts = db.execute("SELECT COUNT(*) FROM quarantined_replays" + where, scope).fetchone()[0]
        operation_result = None if selected_process else operation_readback(db)

    return {
        "results": results,
        "result_counts": dict(Counter(row["result"] for row in results)),
        "receipt_actions": dict(actions),
        "stored_event_receipts": stored_receipts,
        "stored_quarantined_payloads": stored_conflicts,
        "operation_result": operation_result,
        "external_actions": 0,
    }


def check_expected(report, expected, process_id):
    expected_rows = [
        {key: value for key, value in row.items() if key != "process_id"}
        for row in expected["results"]
        if process_id is None or row["process_id"] == process_id
    ]
    if report["results"] != expected_rows:
        raise SystemExit("Observed case results differ from the independently specified expected rows.")
    if report["external_actions"] != expected["external_actions"]:
        raise SystemExit("Unexpected external action count.")
    if process_id is None and report["operation_result"] != expected["operation_result"]:
        raise SystemExit("Observed terminal outcome differs from the expected operation outcome.")
    scope = process_id or "all_processes"
    counts_by_scope = expected["stored_counts"]
    counts = counts_by_scope.get(scope, counts_by_scope["other_process"])
    if (report["stored_event_receipts"], report["stored_quarantined_payloads"]) != (counts["receipts"], counts["quarantined"]):
        raise SystemExit("Stored receipt counts differ from the expected counts. Use a dedicated database for this fixture.")


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--fixture", required=True, type=Path)
    parser.add_argument("--expected", required=True, type=Path)
    parser.add_argument("--db", required=True, type=Path)
    parser.add_argument("--process", choices=CHECKS)
    args = parser.parse_args()
    fixture = json.loads(args.fixture.read_text(encoding="utf-8"))
    expected = json.loads(args.expected.read_text(encoding="utf-8"))
    cases = [
        case for case in fixture["cases"]
        if args.process is None or case["process_id"] == args.process
    ]
    report = run(cases, args.db, selected_process=args.process)
    print(json.dumps(report, indent=2))
    check_expected(report, expected, args.process)


if __name__ == "__main__":
    main()
