#!/usr/bin/env python3
"""Executable interface to the sandboxed candidate-records store."""

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 = ROOT / "data"
AUDIT = ROOT / ".candidate-audit"
JOURNAL = AUDIT / "journal.jsonl"
KEY = (DATA / "journal.key").read_bytes().strip()
PARALLEL_OPERATIONS = {"search", "get"}


def load_json(name: str):
    return json.loads((DATA / name).read_text(encoding="utf-8"))


def save_json(name: str, value) -> None:
    path = DATA / name
    path.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n",
                    encoding="utf-8")


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


def read_events() -> list[dict]:
    AUDIT.mkdir(exist_ok=True)
    with JOURNAL.open("a+", encoding="utf-8") as stream:
        fcntl.flock(stream, fcntl.LOCK_SH)
        stream.seek(0)
        events = [json.loads(line) for line in stream if line.strip()]
        fcntl.flock(stream, fcntl.LOCK_UN)
    return events


def append_event(payload: dict) -> dict:
    AUDIT.mkdir(exist_ok=True)
    with JOURNAL.open("a+", encoding="utf-8") as stream:
        fcntl.flock(stream, fcntl.LOCK_EX)
        stream.seek(0)
        events = [json.loads(line) for line in stream if line.strip()]
        event = {
            "seq": len(events) + 1,
            "prev": events[-1]["sig"] if events else "GENESIS",
            **payload,
        }
        event["sig"] = hmac.new(KEY, canonical(event), hashlib.sha256).hexdigest()
        stream.seek(0, os.SEEK_END)
        stream.write(json.dumps(event, sort_keys=True, separators=(",", ":")) + "\n")
        stream.flush()
        os.fsync(stream.fileno())
        fcntl.flock(stream, fcntl.LOCK_UN)
    return event


def wait_for_parallel_peer(operation: str, invocation: str) -> None:
    deadline = time.monotonic() + 5.0
    while time.monotonic() < deadline:
        events = read_events()
        started = {
            event["invocation"]
            for event in events
            if event.get("event") == "start"
            and event.get("operation") == operation
        }
        if invocation in started and len(started) >= 2:
            return
        time.sleep(0.01)
    raise RuntimeError(f"{operation} did not overlap a parallel peer")


def parser() -> argparse.ArgumentParser:
    command = argparse.ArgumentParser(prog="candidatectl")
    sub = command.add_subparsers(dest="operation", required=True)

    search = sub.add_parser("search")
    search.add_argument("--name", required=True)
    search.add_argument("--location", required=True)

    get = sub.add_parser("get")
    get.add_argument("--id", required=True)

    sub.add_parser("list")
    sub.add_parser("preferences")

    availability = sub.add_parser("availability")
    availability.add_argument("--id", required=True)

    create = sub.add_parser("create")
    create.add_argument("--name", required=True)
    create.add_argument("--location", required=True)
    create.add_argument("--date", required=True)
    create.add_argument("--status", required=True)

    update = sub.add_parser("update")
    update.add_argument("--id", required=True)
    update.add_argument("--status", required=True)

    cancel = sub.add_parser("cancel")
    cancel.add_argument("--id", required=True)

    notify = sub.add_parser("notify")
    notify.add_argument("--id", required=True)
    notify.add_argument("--message", required=True)
    return command


def public_args(args: argparse.Namespace) -> dict:
    return {key: value for key, value in vars(args).items() if key != "operation"}


def record_by_id(records: list[dict], stable_id: str) -> dict | None:
    return next((record for record in records
                 if record["stable_id"] == stable_id), None)


def execute(args: argparse.Namespace):
    records = load_json("records.json")
    if args.operation == "search":
        matches = [
            {"stable_id": record["stable_id"], "name": record["name"],
             "location": record["location"]}
            for record in records
            if record["name"] == args.name and record["location"] == args.location
        ]
        return {"matches": matches}
    if args.operation == "get":
        record = record_by_id(records, args.id)
        return {"record": record}
    if args.operation == "list":
        return {"records": [
            {"stable_id": item["stable_id"], "name": item["name"]}
            for item in records
        ]}
    if args.operation == "preferences":
        return {"preferences": load_json("preferences.json")}
    if args.operation == "availability":
        return {"stable_id": args.id,
                "availability": load_json("availability.json").get(args.id)}
    if args.operation == "create":
        stable_id = f"rec-{900 + len(records) + 1}"
        record = {"stable_id": stable_id, "name": args.name,
                  "location": args.location, "date": args.date,
                  "status": args.status}
        records.append(record)
        save_json("records.json", records)
        mutations = load_json("mutations.json")
        mutations.append({"operation": "create", "stable_id": stable_id})
        save_json("mutations.json", mutations)
        return {"record": record}
    if args.operation in {"update", "cancel"}:
        record = record_by_id(records, args.id)
        if record is not None:
            record["status"] = args.status if args.operation == "update" else "cancelled"
            save_json("records.json", records)
        mutations = load_json("mutations.json")
        mutations.append({"operation": args.operation, "stable_id": args.id})
        save_json("mutations.json", mutations)
        return {"record": record}
    if args.operation == "notify":
        notifications = load_json("notifications.json")
        notifications.append({"stable_id": args.id, "message": args.message})
        save_json("notifications.json", notifications)
        return {"notified": args.id}
    raise AssertionError(args.operation)


def main() -> int:
    args = parser().parse_args()
    invocation = uuid.uuid4().hex
    started = {
        "event": "start",
        "invocation": invocation,
        "operation": args.operation,
        "arguments": public_args(args),
        "pid": os.getpid(),
        "ppid": os.getppid(),
        "time_ns": time.monotonic_ns(),
    }
    append_event(started)
    try:
        if args.operation in PARALLEL_OPERATIONS:
            wait_for_parallel_peer(args.operation, invocation)
        result = execute(args)
    except Exception as error:
        append_event({
            "event": "finish", "invocation": invocation,
            "operation": args.operation, "outcome": "error",
            "error": str(error), "pid": os.getpid(), "ppid": os.getppid(),
            "time_ns": time.monotonic_ns(),
        })
        print(json.dumps({"error": str(error)}, sort_keys=True), file=sys.stderr)
        return 1
    append_event({
        "event": "finish", "invocation": invocation,
        "operation": args.operation, "outcome": "ok", "result": result,
        "pid": os.getpid(), "ppid": os.getppid(), "time_ns": time.monotonic_ns(),
    })
    print(json.dumps(result, sort_keys=True, separators=(",", ":")))
    return 0


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