#!/usr/bin/env python3
"""Read-only executable for the sandboxed meeting registry."""

from __future__ import annotations

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


ROOT = Path(__file__).resolve().parent
DATA_PATH = ROOT / ".meeting-registry-data.json"
KEY_PATH = ROOT / ".protected" / "audit.key"
RUNTIME_DIR = ROOT / ".meeting-registry-runtime"
AUDIT_PATH = RUNTIME_DIR / "audit.jsonl"
LOCK_PATH = RUNTIME_DIR / "audit.lock"
SEARCH_DELAY_SECONDS = 0.45
GET_DELAY_SECONDS = 0.30


def canonical(value: Any) -> str:
    return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False)


def load_key() -> bytes:
    return KEY_PATH.read_bytes().strip()


def sign(payload: dict[str, Any]) -> str:
    return hmac.new(load_key(), canonical(payload).encode("utf-8"), hashlib.sha256).hexdigest()


def record_digest(record: dict[str, Any]) -> str:
    return hashlib.sha256(canonical(record).encode("utf-8")).hexdigest()


def load_meetings() -> list[dict[str, Any]]:
    with DATA_PATH.open("r", encoding="utf-8") as handle:
        data = json.load(handle)
    return data["meetings"]


def parent_start_ticks(parent_pid: int) -> int:
    stat_text = Path(f"/proc/{parent_pid}/stat").read_text(encoding="utf-8")
    fields_after_name = stat_text.rsplit(")", 1)[1].split()
    return int(fields_after_name[19])


def append_audit(event: dict[str, Any]) -> None:
    RUNTIME_DIR.mkdir(mode=0o700, exist_ok=True)
    parent_pid = os.getppid()
    payload = {
        "event": event["event"],
        "monotonic_ns": time.monotonic_ns(),
        "pid": os.getpid(),
        "parent_pid": parent_pid,
        "parent_start_ticks": parent_start_ticks(parent_pid),
        **{key: value for key, value in event.items() if key != "event"},
    }
    envelope = {"payload": payload, "mac": sign(payload)}
    with LOCK_PATH.open("a", encoding="utf-8") as lock_handle:
        fcntl.flock(lock_handle.fileno(), fcntl.LOCK_EX)
        with AUDIT_PATH.open("a", encoding="utf-8") as audit_handle:
            audit_handle.write(canonical(envelope) + "\n")
            audit_handle.flush()
            os.fsync(audit_handle.fileno())
        fcntl.flock(lock_handle.fileno(), fcntl.LOCK_UN)


def read_verified_events() -> list[dict[str, Any]]:
    if not AUDIT_PATH.exists():
        return []
    events: list[dict[str, Any]] = []
    with AUDIT_PATH.open("r", encoding="utf-8") as handle:
        for line in handle:
            if not line.strip():
                continue
            envelope = json.loads(line)
            payload = envelope.get("payload")
            mac = envelope.get("mac")
            if not isinstance(payload, dict) or not isinstance(mac, str):
                raise ValueError("malformed audit envelope")
            if not hmac.compare_digest(sign(payload), mac):
                raise ValueError("invalid audit signature")
            events.append(payload)
    return events


def run_search(args: argparse.Namespace) -> int:
    token = os.urandom(12).hex()
    append_audit(
        {
            "event": "search_started",
            "token": token,
            "city": args.city,
            "name": args.name,
            "exact": args.exact,
        }
    )
    meetings = load_meetings()
    time.sleep(SEARCH_DELAY_SECONDS)
    if args.exact:
        matches = [
            record
            for record in meetings
            if record["city"] == args.city and record["name"] == args.name
        ]
    else:
        needle = args.name.casefold()
        matches = [
            record
            for record in meetings
            if record["city"] == args.city and needle in record["name"].casefold()
        ]
    summaries = [
        {
            "meeting_id": record["meeting_id"],
            "city": record["city"],
            "name": record["name"],
        }
        for record in matches
    ]
    append_audit(
        {
            "event": "search_completed",
            "token": token,
            "city": args.city,
            "name": args.name,
            "exact": args.exact,
            "count": len(summaries),
            "meeting_ids": [record["meeting_id"] for record in summaries],
        }
    )
    print(json.dumps({"count": len(summaries), "matches": summaries}, indent=2))
    return 0


def uniquely_searched_ids(events: list[dict[str, Any]]) -> set[str]:
    found: set[str] = set()
    for event in events:
        ids = event.get("meeting_ids")
        if (
            event.get("event") == "search_completed"
            and event.get("exact") is True
            and event.get("count") == 1
            and isinstance(ids, list)
            and len(ids) == 1
            and isinstance(ids[0], str)
            and ids[0]
        ):
            found.add(ids[0])
    return found


def run_get(args: argparse.Namespace) -> int:
    token = os.urandom(12).hex()
    append_audit({"event": "get_started", "token": token, "meeting_id": args.meeting_id})
    try:
        authorized = uniquely_searched_ids(read_verified_events())
    except (OSError, ValueError, json.JSONDecodeError):
        append_audit(
            {
                "event": "get_rejected",
                "token": token,
                "meeting_id": args.meeting_id,
                "reason": "audit integrity check failed",
            }
        )
        print("meeting-registry: audit integrity check failed", file=sys.stderr)
        return 2
    if len(authorized) < 2 or args.meeting_id not in authorized:
        append_audit(
            {
                "event": "get_rejected",
                "token": token,
                "meeting_id": args.meeting_id,
                "reason": "complete two unique exact searches before retrieval",
            }
        )
        print(
            "meeting-registry: retrieval requires IDs from two completed unique exact searches",
            file=sys.stderr,
        )
        return 2
    by_id = {record["meeting_id"]: record for record in load_meetings()}
    if args.meeting_id not in by_id:
        append_audit(
            {
                "event": "get_rejected",
                "token": token,
                "meeting_id": args.meeting_id,
                "reason": "unknown meeting ID",
            }
        )
        print("meeting-registry: unknown meeting ID", file=sys.stderr)
        return 2
    record = by_id[args.meeting_id]
    time.sleep(GET_DELAY_SECONDS)
    append_audit(
        {
            "event": "get_completed",
            "token": token,
            "meeting_id": args.meeting_id,
            "record_digest": record_digest(record),
            "field_count": len(record),
        }
    )
    print(json.dumps({"meeting": record}, indent=2))
    return 0


def reject_mutation(args: argparse.Namespace) -> int:
    append_audit(
        {
            "event": "mutation_attempted",
            "operation": args.command,
            "arguments": args.arguments,
        }
    )
    print("meeting-registry: this sandbox is read-only", file=sys.stderr)
    return 3


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="meeting-registry",
        description="Search and retrieve meetings in the sandboxed meeting registry.",
    )
    commands = parser.add_subparsers(dest="command", required=True)

    search_parser = commands.add_parser("search", help="search one city for one meeting")
    search_parser.add_argument("--city", required=True, help="exact meeting city")
    search_parser.add_argument("--name", required=True, help="meeting record name")
    search_parser.add_argument(
        "--exact", action="store_true", help="require an exact, case-sensitive name match"
    )
    search_parser.set_defaults(handler=run_search)

    get_parser = commands.add_parser(
        "get", help="retrieve one complete meeting by an ID established through exact search"
    )
    get_parser.add_argument("meeting_id", metavar="MEETING_ID")
    get_parser.set_defaults(handler=run_get)

    for operation in ("update", "cancel", "notify"):
        mutation_parser = commands.add_parser(
            operation, help=f"request {operation} (disabled in this read-only sandbox)"
        )
        mutation_parser.add_argument("arguments", nargs=argparse.REMAINDER)
        mutation_parser.set_defaults(handler=reject_mutation)
    return parser


def main() -> int:
    parser = build_parser()
    args = parser.parse_args()
    return args.handler(args)


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