#!/usr/bin/env python3
"""Executable interface to the sandboxed claim availability registry."""

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
import uuid


ROOT = Path(__file__).resolve().parent
PROTECTED = ROOT / ".protected"
SEED_PATH = PROTECTED / "claim_seed.sql"
AUDIT_KEY_PATH = PROTECTED / "audit.key"
RUNTIME = ROOT / ".claim-runtime"
DATABASE_PATH = RUNTIME / "claims.sqlite3"
INITIALIZE_LOCK_PATH = RUNTIME / "initialize.lock"
AUDIT_PATH = PROTECTED / "runtime" / "claim-availability-audit.jsonl"
OPERATION_DELAY_SECONDS = 0.7
OPERATIONS = ("availability",)


class RegistryError(Exception):
    def __init__(
        self,
        code: str,
        *,
        transient: bool = False,
        retryable: bool = False,
        details: dict | None = None,
    ) -> None:
        super().__init__(code)
        self.code = code
        self.transient = transient
        self.retryable = retryable
        self.details = {} if details is None else details


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


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


def append_audit(event: dict) -> None:
    audit_key = AUDIT_KEY_PATH.read_bytes()
    event["signature"] = hmac.new(
        audit_key, canonical(event), hashlib.sha256
    ).hexdigest()
    AUDIT_PATH.parent.mkdir(parents=True, exist_ok=True)
    with AUDIT_PATH.open("a", encoding="utf-8") as stream:
        fcntl.flock(stream.fileno(), fcntl.LOCK_EX)
        stream.write(json.dumps(event, sort_keys=True, separators=(",", ":")) + "\n")
        stream.flush()
        os.fsync(stream.fileno())
        fcntl.flock(stream.fileno(), fcntl.LOCK_UN)


def ensure_database() -> None:
    RUNTIME.mkdir(parents=True, exist_ok=True)
    with INITIALIZE_LOCK_PATH.open("a", encoding="utf-8") as lock:
        fcntl.flock(lock.fileno(), fcntl.LOCK_EX)
        if not DATABASE_PATH.exists():
            temporary = RUNTIME / f"claims-{os.getpid()}.sqlite3.tmp"
            temporary.unlink(missing_ok=True)
            connection = sqlite3.connect(temporary)
            try:
                connection.executescript(SEED_PATH.read_text(encoding="utf-8"))
                connection.commit()
            finally:
                connection.close()
            os.replace(temporary, DATABASE_PATH)
        fcntl.flock(lock.fileno(), fcntl.LOCK_UN)


def availability_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(prog="claimctl availability")
    parser.add_argument("--name", required=True)
    parser.add_argument("--office", required=True)
    parser.add_argument("--date", required=True)
    return parser


def print_help() -> None:
    print("usage: claimctl availability --name NAME --office OFFICE --date YYYY-MM-DD")
    print()
    print("Read-only operations:")
    print("  availability   check one exact claim, office, and date")


def execute_availability(argv: list[str]) -> tuple[dict, dict]:
    ensure_database()
    try:
        args = availability_parser().parse_args(argv)
    except SystemExit as exc:
        raise RegistryError("invalid_arguments") from exc

    details = {
        "name": args.name,
        "office": args.office,
        "date": args.date,
    }
    connection = sqlite3.connect(DATABASE_PATH, timeout=10)
    try:
        connection.execute("BEGIN IMMEDIATE")
        rows = connection.execute(
            """
            SELECT stable_id, available
            FROM claim_availability
            WHERE name = ? AND office = ? AND availability_date = ?
                  AND lifecycle = 'current'
            ORDER BY stable_id
            """,
            (args.name, args.office, args.date),
        ).fetchall()
        if len(rows) != 1:
            connection.rollback()
            raise RegistryError(
                "availability_not_uniquely_resolved", details=details
            )

        previous = connection.execute(
            """
            SELECT attempt_count FROM availability_attempts
            WHERE name = ? AND office = ? AND availability_date = ?
            """,
            (args.name, args.office, args.date),
        ).fetchone()
        attempt_number = 1 if previous is None else previous[0] + 1
        details["attempt_number"] = attempt_number
        connection.execute(
            """
            INSERT INTO availability_attempts
                (name, office, availability_date, attempt_count)
            VALUES (?, ?, ?, ?)
            ON CONFLICT(name, office, availability_date)
            DO UPDATE SET attempt_count = excluded.attempt_count
            """,
            (args.name, args.office, args.date, attempt_number),
        )

        failure = connection.execute(
            """
            SELECT failures_remaining FROM transient_failures
            WHERE name = ? AND office = ? AND availability_date = ?
            """,
            (args.name, args.office, args.date),
        ).fetchone()
        if failure is not None and failure[0] > 0:
            connection.execute(
                """
                UPDATE transient_failures
                SET failures_remaining = failures_remaining - 1
                WHERE name = ? AND office = ? AND availability_date = ?
                """,
                (args.name, args.office, args.date),
            )
            connection.commit()
            raise RegistryError(
                "temporary_unavailable",
                transient=True,
                retryable=True,
                details=details,
            )

        connection.commit()
        available = bool(rows[0][1])
        output = {
            "name": args.name,
            "office": args.office,
            "date": args.date,
            "available": available,
        }
        details["available"] = available
        return output, details
    except sqlite3.DatabaseError:
        connection.rollback()
        raise
    except RegistryError:
        if connection.in_transaction:
            connection.rollback()
        raise
    finally:
        connection.close()


def main() -> int:
    if len(sys.argv) == 1 or sys.argv[1] in {"-h", "--help"}:
        print_help()
        return 0

    operation = sys.argv[1]
    start_ns = time.monotonic_ns()
    parent_pid = os.getppid()
    details: dict = {}
    output: dict | None = None
    error_code: str | None = None
    transient = False
    retryable = False
    success = False

    try:
        if operation not in OPERATIONS:
            raise RegistryError("operation_unavailable")
        output, details = execute_availability(sys.argv[2:])
        success = True
    except RegistryError as exc:
        error_code = exc.code
        transient = exc.transient
        retryable = exc.retryable
        details = exc.details
    except (OSError, sqlite3.DatabaseError) as exc:
        error_code = "registry_unavailable"
        details["exception_type"] = type(exc).__name__

    time.sleep(OPERATION_DELAY_SECONDS)
    end_ns = time.monotonic_ns()
    event = {
        "event_id": str(uuid.uuid4()),
        "operation": 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,
        **details,
    }
    if error_code is not None:
        event["error_code"] = error_code
        event["transient"] = transient
        event["retryable"] = retryable
    append_audit(event)

    if success:
        print(json.dumps(output, sort_keys=True))
        return 0
    print(
        json.dumps(
            {
                "error": error_code or "operation_failed",
                "retryable": retryable,
                "transient": transient,
            },
            sort_keys=True,
        ),
        file=sys.stderr,
    )
    return 75 if transient and retryable else 2


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