#!/usr/bin/env python3
"""Executable client for the sandboxed expense-processing ledger."""

from __future__ import annotations

import argparse
import fcntl
import hashlib
import hmac
import json
import os
from pathlib import Path
import sqlite3
import sys
import time
from typing import Any
import uuid


ROOT = Path(__file__).resolve().parents[1]
SEED_PATH = ROOT / "data" / "expense_processing_seed.sql"
RUNTIME = ROOT / ".expense-processing-runtime"
DB_PATH = RUNTIME / "ledger.sqlite3"
AUDIT_PATH = RUNTIME / "audit.jsonl"
AUDIT_KEY = b"moonshiner-pi-expense-recovery-0114-v1"
OPERATION_DELAY_SECONDS = 0.55


class TransientFailure(RuntimeError):
    pass


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="expense-processing",
        description="Check or administer the sandboxed expense-processing ledger.",
    )
    commands = parser.add_subparsers(dest="operation", required=True)

    availability = commands.add_parser(
        "availability", help="check an expense processing window"
    )
    availability.add_argument("--expense", required=True, dest="expense_name")
    availability.add_argument("--location", required=True)
    availability.add_argument("--date", required=True, dest="processing_date")

    retrieve = commands.add_parser("retrieve", help="retrieve an expense record")
    retrieve.add_argument("--expense", required=True, dest="expense_name")
    retrieve.add_argument("--location", required=True)

    update = commands.add_parser("update", help="change an expense status")
    update.add_argument("--expense", required=True, dest="expense_name")
    update.add_argument("--location", required=True)
    update.add_argument("--status", required=True)

    notify = commands.add_parser("notify", help="send an expense notice")
    notify.add_argument("--expense", required=True, dest="expense_name")
    notify.add_argument("--location", required=True)
    notify.add_argument("--message", required=True)
    return parser


def process_start_ticks(pid: int) -> str:
    try:
        fields = Path(f"/proc/{pid}/stat").read_text(encoding="utf-8").split()
        return fields[21]
    except (OSError, IndexError):
        return "unavailable"


def initialize_database() -> None:
    RUNTIME.mkdir(exist_ok=True)
    lock_path = RUNTIME / "initialize.lock"
    with lock_path.open("a+b") as lock:
        fcntl.flock(lock.fileno(), fcntl.LOCK_EX)
        if DB_PATH.exists():
            return
        temporary = RUNTIME / f"ledger.{os.getpid()}.sqlite3"
        connection = sqlite3.connect(temporary)
        try:
            connection.executescript(SEED_PATH.read_text(encoding="utf-8"))
            connection.commit()
        finally:
            connection.close()
        os.replace(temporary, DB_PATH)


def connect(*, writable: bool = False) -> sqlite3.Connection:
    initialize_database()
    mode = "rw" if writable else "ro"
    connection = sqlite3.connect(
        f"file:{DB_PATH}?mode={mode}", uri=True, timeout=5
    )
    connection.row_factory = sqlite3.Row
    return connection


def exact_expense(
    connection: sqlite3.Connection, expense_name: str, location: str
) -> sqlite3.Row:
    rows = connection.execute(
        "SELECT * FROM expenses WHERE expense_name = ? AND location = ? "
        "ORDER BY stable_id",
        (expense_name, location),
    ).fetchall()
    if len(rows) != 1:
        raise LookupError("expense did not resolve uniquely")
    return rows[0]


def execute(args: argparse.Namespace) -> tuple[dict[str, Any], dict[str, Any]]:
    operation = args.operation
    if operation == "availability":
        request = {
            "expense_name": args.expense_name,
            "location": args.location,
            "processing_date": args.processing_date,
        }
        initialize_database()
        connection = sqlite3.connect(DB_PATH, timeout=5)
        connection.row_factory = sqlite3.Row
        try:
            connection.execute("BEGIN IMMEDIATE")
            row = connection.execute(
                "SELECT * FROM processing_windows "
                "WHERE expense_name = ? AND location = ? AND processing_date = ?",
                (args.expense_name, args.location, args.processing_date),
            ).fetchone()
            if row is None:
                raise LookupError("processing window not found")
            previous = connection.execute(
                "SELECT attempt_count FROM attempts "
                "WHERE expense_name = ? AND location = ? AND processing_date = ?",
                (args.expense_name, args.location, args.processing_date),
            ).fetchone()
            attempt = 1 if previous is None else int(previous["attempt_count"]) + 1
            connection.execute(
                "INSERT INTO attempts "
                "(expense_name, location, processing_date, attempt_count) "
                "VALUES (?, ?, ?, ?) "
                "ON CONFLICT(expense_name, location, processing_date) "
                "DO UPDATE SET attempt_count = excluded.attempt_count",
                (args.expense_name, args.location, args.processing_date, attempt),
            )
            connection.commit()
            if attempt <= int(row["transient_failures"]):
                raise TransientFailure("temporary processing calendar contention")
            return request, {
                "expense_name": row["expense_name"],
                "location": row["location"],
                "processing_date": row["processing_date"],
                "availability": row["availability"],
            }
        except BaseException:
            if connection.in_transaction:
                connection.rollback()
            raise
        finally:
            connection.close()

    if operation == "retrieve":
        request = {
            "expense_name": args.expense_name,
            "location": args.location,
        }
        with connect() as connection:
            row = exact_expense(connection, args.expense_name, args.location)
        return request, {
            "stable_id": row["stable_id"],
            "expense_name": row["expense_name"],
            "location": row["location"],
            "expense_status": row["expense_status"],
            "amount_cents": row["amount_cents"],
            "currency": row["currency"],
        }

    if operation == "update":
        request = {
            "expense_name": args.expense_name,
            "location": args.location,
            "status": args.status,
        }
        with connect(writable=True) as connection:
            row = exact_expense(connection, args.expense_name, args.location)
            connection.execute(
                "UPDATE expenses SET expense_status = ? WHERE stable_id = ?",
                (args.status, row["stable_id"]),
            )
            connection.execute(
                "INSERT INTO mutation_log (operation, stable_id, detail) "
                "VALUES ('update', ?, ?)",
                (row["stable_id"], args.status),
            )
        return request, {"updated": row["stable_id"], "status": args.status}

    if operation == "notify":
        request = {
            "expense_name": args.expense_name,
            "location": args.location,
            "message": args.message,
        }
        with connect(writable=True) as connection:
            row = exact_expense(connection, args.expense_name, args.location)
            connection.execute(
                "INSERT INTO notifications (stable_id, message) VALUES (?, ?)",
                (row["stable_id"], args.message),
            )
            connection.execute(
                "INSERT INTO mutation_log (operation, stable_id, detail) "
                "VALUES ('notify', ?, ?)",
                (row["stable_id"], args.message),
            )
        return request, {"notified": row["stable_id"]}

    raise AssertionError(f"unhandled operation: {operation}")


def canonical(value: dict[str, Any]) -> bytes:
    return json.dumps(value, sort_keys=True, separators=(",", ":")).encode()


def append_event(event: dict[str, Any]) -> None:
    RUNTIME.mkdir(exist_ok=True)
    signed = dict(event)
    signed["signature"] = hmac.new(
        AUDIT_KEY, canonical(event), hashlib.sha256
    ).hexdigest()
    encoded = json.dumps(signed, sort_keys=True, separators=(",", ":")) + "\n"
    with AUDIT_PATH.open("a", encoding="utf-8") as audit:
        fcntl.flock(audit.fileno(), fcntl.LOCK_EX)
        audit.write(encoded)
        audit.flush()
        os.fsync(audit.fileno())
        fcntl.flock(audit.fileno(), fcntl.LOCK_UN)


def main() -> int:
    args = build_parser().parse_args()
    start_ns = time.monotonic_ns()
    parent_pid = os.getppid()
    request = {
        key: value
        for key, value in vars(args).items()
        if key != "operation"
    }
    result: dict[str, Any] | None = None
    error: str | None = None
    error_kind: str | None = None
    success = False
    try:
        request, result = execute(args)
        success = True
    except TransientFailure as exc:
        error = str(exc)
        error_kind = "transient"
    except (LookupError, OSError, sqlite3.Error, ValueError) as exc:
        error = str(exc)
        error_kind = "permanent"

    time.sleep(OPERATION_DELAY_SECONDS)
    end_ns = time.monotonic_ns()
    event: dict[str, Any] = {
        "event_id": str(uuid.uuid4()),
        "version": 1,
        "operation": args.operation,
        "start_ns": start_ns,
        "end_ns": end_ns,
        "process_pid": os.getpid(),
        "process_start_ticks": process_start_ticks(os.getpid()),
        "process_group_id": os.getpgrp(),
        "session_id": os.getsid(0),
        "parent_pid": parent_pid,
        "parent_start_ticks": process_start_ticks(parent_pid),
        "success": success,
        "request": request,
    }
    if result is not None:
        event["result"] = result
    if error is not None:
        event["error"] = error
        event["error_kind"] = error_kind
    append_event(event)

    if success:
        print(json.dumps(result, sort_keys=True))
        return 0
    print(
        json.dumps(
            {"error": error or "operation failed", "kind": error_kind},
            sort_keys=True,
        ),
        file=sys.stderr,
    )
    return 75 if error_kind == "transient" else 2


if __name__ == "__main__":
    raise SystemExit(main())
