#!/usr/bin/env python3
"""Check synthetic term-loan funding evidence in a temporary SQLite ledger."""

import argparse
from contextlib import closing
import json
from pathlib import Path
import sqlite3
import tempfile


TERMS = ("application_id", "facility_id", "terms_revision", "amount_cents", "currency", "destination_ref")
IDENTITY = ("application_id", "facility_id")


def matches(left, right, fields):
    return all(field in left and field in right and type(left[field]) is type(right[field])
               and left[field] == right[field] for field in fields)


def hold_reason(records):
    request = records["request"]
    credit = records["credit"] or {}
    closing_packet = records["closing"] or {}
    release = records["release"] or {}
    bank = records["bank"] or {}

    if (not all(isinstance(request.get(field), str) and request[field] for field in
                ("application_id", "facility_id", "destination_ref"))
            or type(request.get("terms_revision")) is not int or request["terms_revision"] < 1
            or type(request.get("amount_cents")) is not int or request["amount_cents"] < 1
            or request.get("currency") != "USD"):
        return "invalid_request"
    if credit.get("state") != "approved" or not credit.get("decision_ref"):
        return "missing_credit_approval"
    if not matches(request, credit, TERMS):
        return "approved_terms_mismatch"
    if (not matches(request, closing_packet, (*IDENTITY, "terms_revision"))
            or closing_packet.get("state") != "signed_and_conditions_cleared"
            or not closing_packet.get("agreement_ref")):
        return "closing_evidence_unverified"
    if (release.get("state") != "authorized" or release.get("role") != "funding_officer"
            or not release.get("release_ref")):
        return "missing_release_authority"
    if (not matches(request, release, TERMS)
            or not matches(credit, release, ("decision_ref",))
            or not matches(closing_packet, release, ("agreement_ref",))):
        return "release_evidence_mismatch"
    if bank.get("state") != "settled" or not bank.get("settlement_ref"):
        return "settlement_unconfirmed"
    if not matches(request, bank, TERMS) or not matches(release, bank, ("release_ref",)):
        return "settlement_evidence_mismatch"
    return None


def checked_records(base, overrides):
    return {
        name: None if name in overrides and overrides[name] is None
        else {**record, **overrides.get(name, {})}
        for name, record in base.items()
    }


def receipt(db, scenario, event_id):
    row = db.execute(
        "SELECT payload, outcome FROM receipts WHERE scenario = ? AND event_id = ?",
        (scenario, event_id),
    ).fetchone()
    return (json.loads(row[0]), json.loads(row[1])) if row else (None, None)


def canonical_payload(payload):
    return json.dumps(payload, sort_keys=True, separators=(",", ":"))


def check_revision(db, scenario, payload):
    supersedes = payload["supersedes"]
    revision = payload["revision"]
    if not supersedes:
        return None if revision == 1 else "missing_predecessor"
    previous, outcome = receipt(db, scenario, supersedes)
    if (previous and outcome["status"] == "held"
            and revision == previous["revision"] + 1
            and matches(previous["records"]["request"], payload["records"]["request"], IDENTITY)):
        existing = db.execute(
            "SELECT 1 FROM receipts WHERE scenario = ? AND supersedes = ?",
            (scenario, supersedes),
        ).fetchone()
        if not existing:
            return None
    return "correction_requires_review"


def record_case(path, case, records):
    scenario, event_id = case["scenario"], case["event_id"]
    payload = {
        "revision": case.get("revision", 1),
        "supersedes": case.get("supersedes"),
        "records": records,
    }
    with closing(sqlite3.connect(path)) as db, db:
        db.execute("BEGIN IMMEDIATE")
        original, saved = receipt(db, scenario, event_id)
        if saved:
            if canonical_payload(original) == canonical_payload(payload):
                return {"receipt": "reused", **saved}
            return {"receipt": "none", "status": "held", "reason": "changed_same_event"}

        revision_hold = check_revision(db, scenario, payload)
        reason = revision_hold or hold_reason(records)
        if not reason:
            settlement_ref = records["bank"]["settlement_ref"]
            existing = db.execute(
                "SELECT 1 FROM receipts WHERE scenario = ? AND settlement_ref = ?",
                (scenario, settlement_ref),
            ).fetchone()
            reason = "bank_receipt_already_matched" if existing else None
        if reason:
            outcome = {"status": "held", "reason": reason}
            settlement_ref = None
        else:
            request = records["request"]
            outcome = {
                "status": "settlement_verified",
                **{field: request[field] for field in (*IDENTITY, "amount_cents", "currency")},
                "release_ref": records["release"]["release_ref"],
                "settlement_ref": settlement_ref,
            }

        db.execute(
            "INSERT INTO receipts VALUES (?, ?, ?, ?, ?, ?)",
            (scenario, event_id, payload["supersedes"] if not revision_hold else None,
             settlement_ref, canonical_payload(payload), json.dumps(outcome)),
        )
        db.commit()
        if case.get("timeout_after_commit"):
            raise TimeoutError("Synthetic loss of the response after commit")
        return {"receipt": "written", **outcome}


def run(fixture):
    with tempfile.TemporaryDirectory() as directory:
        path = Path(directory) / "origination.sqlite"
        with closing(sqlite3.connect(path)) as db:
            db.execute("""
                CREATE TABLE receipts (
                    scenario TEXT NOT NULL,
                    event_id TEXT NOT NULL,
                    supersedes TEXT,
                    settlement_ref TEXT,
                    payload TEXT NOT NULL,
                    outcome TEXT NOT NULL,
                    PRIMARY KEY (scenario, event_id),
                    UNIQUE (scenario, settlement_ref)
                )
            """)
        results = {}
        for case in fixture["cases"]:
            records = checked_records(fixture["source_records"], case.get("overrides", {}))
            try:
                outcome = record_case(path, case, records)
            except TimeoutError:
                # Reopen the ledger and inspect the original destination key.
                with closing(sqlite3.connect(path)) as db:
                    _, saved = receipt(db, case["scenario"], case["event_id"])
                if not saved:
                    raise
                outcome = {"receipt": "recovered_after_timeout", **saved}
            results[case["name"]] = outcome
        with closing(sqlite3.connect(path)) as db:
            receipts, settlements = db.execute(
                "SELECT COUNT(*), COUNT(settlement_ref) FROM receipts"
            ).fetchone()
        return {"cases": results, "stored_receipts": receipts, "verified_settlements": settlements}


def main():
    folder = Path(__file__).resolve().parent
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--fixture", type=Path, default=folder / "fixture.json")
    parser.add_argument("--expected", type=Path, default=folder / "expected.json")
    args = parser.parse_args()
    actual = run(json.loads(args.fixture.read_text()))
    expected = json.loads(args.expected.read_text())
    print(json.dumps(actual, indent=2))
    if actual != expected:
        raise SystemExit("Output differs from the expected results.")


if __name__ == "__main__":
    main()
