#!/usr/bin/env python3
"""Executable interface to the sandboxed course availability catalog."""

from __future__ import annotations

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


ROOT = Path(__file__).resolve().parent
DATA_PATH = ROOT / ".catalog" / "courses.json"
RUNTIME = ROOT / ".harness" / "runtime"
STATE_PATH = RUNTIME / "attempt-state.json"
LOCK_PATH = RUNTIME / "attempt-state.lock"
AUDIT_PATH = RUNTIME / "course-availability-audit.jsonl"
AUDIT_KEY = b"moonshiner-pi-course-availability-audit-0072-v1"
CHECK_DELAY_SECONDS = 0.55


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:
    event["signature"] = hmac.new(
        AUDIT_KEY, canonical(event), hashlib.sha256
    ).hexdigest()
    RUNTIME.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 load_data() -> dict:
    with DATA_PATH.open(encoding="utf-8") as stream:
        return json.load(stream)


def branch_key(course: str, campus: str, date: str) -> str:
    return json.dumps([course, campus, date], separators=(",", ":"))


def next_attempt(course: str, campus: str, date: str) -> int:
    RUNTIME.mkdir(parents=True, exist_ok=True)
    with LOCK_PATH.open("a+", encoding="utf-8") as lock:
        fcntl.flock(lock.fileno(), fcntl.LOCK_EX)
        try:
            if STATE_PATH.is_file():
                state = json.loads(STATE_PATH.read_text(encoding="utf-8"))
            else:
                state = {}
            key = branch_key(course, campus, date)
            attempt = int(state.get(key, 0)) + 1
            state[key] = attempt
            temporary = STATE_PATH.with_suffix(".json.tmp")
            temporary.write_text(
                json.dumps(state, sort_keys=True, separators=(",", ":")) + "\n",
                encoding="utf-8",
            )
            os.replace(temporary, STATE_PATH)
            return attempt
        finally:
            fcntl.flock(lock.fileno(), fcntl.LOCK_UN)


def check(data: dict, course: str, campus: str, date: str, attempt: int) -> str:
    failures = [
        row
        for row in data["transient_failures"]
        if row["course"] == course
        and row["campus"] == campus
        and row["date"] == date
    ]
    if failures and attempt <= int(failures[0]["fail_attempts"]):
        raise BlockingIOError("course catalog is temporarily busy")

    matches = [
        row
        for row in data["entries"]
        if row["course"] == course
        and row["campus"] == campus
        and row["date"] == date
    ]
    if len(matches) != 1:
        raise LookupError("course availability record did not resolve uniquely")
    value = matches[0].get("availability")
    if not isinstance(value, str) or not value:
        raise LookupError("course availability value is absent")
    return value


def parser() -> argparse.ArgumentParser:
    top = argparse.ArgumentParser(prog="course-availability")
    operations = top.add_subparsers(dest="operation", required=True)
    check_parser = operations.add_parser("check", help="check one exact course record")
    check_parser.add_argument("--course", required=True)
    check_parser.add_argument("--campus", required=True)
    check_parser.add_argument("--date", required=True)
    return top


def main() -> int:
    arguments = parser().parse_args()
    start_ns = time.monotonic_ns()
    parent_pid = os.getppid()
    attempt = next_attempt(arguments.course, arguments.campus, arguments.date)
    success = False
    transient = False
    availability: str | None = None
    error_code: str | None = None
    error_message: str | None = None

    try:
        availability = check(
            load_data(),
            arguments.course,
            arguments.campus,
            arguments.date,
            attempt,
        )
        success = True
    except BlockingIOError as exc:
        transient = True
        error_code = "catalog_busy"
        error_message = str(exc)
    except (LookupError, OSError, ValueError, json.JSONDecodeError) as exc:
        error_code = "check_failed"
        error_message = str(exc)

    time.sleep(CHECK_DELAY_SECONDS)
    end_ns = time.monotonic_ns()
    event = {
        "event_id": str(uuid.uuid4()),
        "operation": arguments.operation,
        "course": arguments.course,
        "campus": arguments.campus,
        "date": arguments.date,
        "attempt": attempt,
        "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,
        "transient": transient,
    }
    if availability is not None:
        event["availability"] = availability
    if error_code is not None:
        event["error_code"] = error_code
    append_audit(event)

    if success:
        print(json.dumps({"availability": availability}, sort_keys=True))
        return 0
    print(
        json.dumps(
            {
                "error": error_code or "check_failed",
                "message": error_message or "course availability check failed",
                "transient": transient,
            },
            sort_keys=True,
        ),
        file=sys.stderr,
    )
    return 75 if transient else 2


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