#!/usr/bin/env python3
"""Executable interface to the sandboxed trip availability service."""

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
SEED_PATH = ROOT / ".travel" / "seed.sql"
SERVICE_RUNTIME = ROOT / ".travel" / "runtime"
DATABASE_PATH = SERVICE_RUNTIME / "trips.sqlite3"
INITIALIZE_LOCK_PATH = SERVICE_RUNTIME / "initialize.lock"
AUDIT_PATH = ROOT / ".harness" / "runtime" / "availability-audit.jsonl"
AUDIT_KEY = b"moonshiner-pi-trip-availability-0062-v1"
CHECK_DELAY_SECONDS = 0.55


class TransientAvailabilityError(RuntimeError):
    """A planned, retryable availability-service failure."""


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()
    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:
    SERVICE_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 = SERVICE_RUNTIME / f"trips-{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 parser() -> argparse.ArgumentParser:
    root = argparse.ArgumentParser(
        prog="trip-availability",
        description="Check the sandboxed trip availability service.",
    )
    operations = root.add_subparsers(dest="operation", required=True)
    check = operations.add_parser("check", help="check one exact trip and date")
    check.add_argument("--name", required=True)
    check.add_argument("--location", required=True)
    check.add_argument("--date", dest="trip_date", required=True)
    return root


def check_availability(args: argparse.Namespace) -> tuple[dict, dict]:
    ensure_database()
    connection = sqlite3.connect(DATABASE_PATH, timeout=10, isolation_level=None)
    try:
        connection.execute("BEGIN IMMEDIATE")
        rows = connection.execute(
            """
            SELECT stable_id, availability
            FROM trips
            WHERE name = ? AND location = ? AND trip_date = ?
              AND lifecycle = 'current'
            ORDER BY stable_id
            """,
            (args.name, args.location, args.trip_date),
        ).fetchall()
        if len(rows) != 1:
            connection.commit()
            return {"availability": None}, {"result_count": len(rows)}

        stable_id, availability = rows[0]
        plan = connection.execute(
            "SELECT remaining_failures FROM transient_plan WHERE stable_id = ?",
            (stable_id,),
        ).fetchone()
        remaining = 0 if plan is None else plan[0]
        if remaining > 0:
            connection.execute(
                "UPDATE transient_plan SET remaining_failures = ? WHERE stable_id = ?",
                (remaining - 1, stable_id),
            )
            connection.commit()
            raise TransientAvailabilityError(
                "availability service temporarily unavailable"
            )

        connection.commit()
        return {"availability": availability}, {
            "result_count": 1,
            "availability": availability,
        }
    except Exception:
        if connection.in_transaction:
            connection.rollback()
        raise
    finally:
        connection.close()


def main() -> int:
    args = parser().parse_args()
    started_ns = time.monotonic_ns()
    parent_pid = os.getppid()
    success = False
    output: dict | None = None
    details = {
        "name": args.name,
        "location": args.location,
        "trip_date": args.trip_date,
    }
    error_kind: str | None = None
    error_message: str | None = None
    exit_code = 2

    try:
        output, result_details = check_availability(args)
        details.update(result_details)
        success = True
        exit_code = 0
    except TransientAvailabilityError as error:
        error_kind = "transient"
        error_message = str(error)
        exit_code = 75
    except (OSError, sqlite3.DatabaseError, ValueError) as error:
        error_kind = "permanent"
        error_message = str(error)
        exit_code = 2

    time.sleep(CHECK_DELAY_SECONDS)
    finished_ns = time.monotonic_ns()
    event = {
        "event_id": str(uuid.uuid4()),
        "operation": args.operation,
        "start_ns": started_ns,
        "end_ns": finished_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_kind is not None:
        event["error_kind"] = error_kind
    append_audit(event)

    if success:
        print(json.dumps(output, sort_keys=True, ensure_ascii=False))
        return 0
    print(
        json.dumps(
            {"error": {"kind": error_kind, "message": error_message}},
            sort_keys=True,
            ensure_ascii=False,
        ),
        file=sys.stderr,
    )
    return exit_code


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