#!/usr/bin/env python3
"""Sandboxed education-record service for the Pi harness seed."""

from __future__ import annotations

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


ROOT = Path(__file__).resolve().parent
STATE_DIR = ROOT / ".education"
RECORDS_PATH = STATE_DIR / "records.json"
AUDIT_PATH = STATE_DIR / "audit.jsonl"
NOTIFICATIONS_PATH = STATE_DIR / "notifications.json"
LOCK_PATH = STATE_DIR / "lock"
KEY_PATH = ROOT / ".harness" / "audit.key"
OPERATION_DELAY_SECONDS = 0.65


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


def load_json(path: Path) -> dict[str, Any]:
    with path.open(encoding="utf-8") as stream:
        value = json.load(stream)
    if not isinstance(value, dict):
        raise RuntimeError(f"invalid state file: {path.name}")
    return value


def atomic_json_write(path: Path, value: dict[str, Any]) -> None:
    descriptor, temporary_name = tempfile.mkstemp(
        dir=path.parent, prefix=f".{path.name}.", text=True
    )
    temporary = Path(temporary_name)
    try:
        with os.fdopen(descriptor, "w", encoding="utf-8") as stream:
            json.dump(value, stream, ensure_ascii=False, indent=2)
            stream.write("\n")
            stream.flush()
            os.fsync(stream.fileno())
        os.replace(temporary, path)
    finally:
        if temporary.exists():
            temporary.unlink()


def records_from(document: dict[str, Any]) -> list[dict[str, Any]]:
    records = document.get("records")
    if document.get("version") != 1 or not isinstance(records, list):
        raise RuntimeError("invalid record store")
    if not all(isinstance(record, dict) for record in records):
        raise RuntimeError("invalid record entry")
    return records


def notifications_from(document: dict[str, Any]) -> list[dict[str, Any]]:
    notifications = document.get("notifications")
    if document.get("version") != 1 or not isinstance(notifications, list):
        raise RuntimeError("invalid notification store")
    if not all(isinstance(item, dict) for item in notifications):
        raise RuntimeError("invalid notification entry")
    return notifications


def read_records() -> list[dict[str, Any]]:
    return records_from(load_json(RECORDS_PATH))


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


def write_audit_locked(event: dict[str, Any]) -> None:
    with AUDIT_PATH.open(encoding="utf-8") as stream:
        sequence = sum(1 for line in stream if line.strip()) + 1
    sealed = dict(event)
    sealed["sequence"] = sequence
    key = KEY_PATH.read_bytes().strip()
    sealed["seal"] = hmac.new(key, canonical(sealed), hashlib.sha256).hexdigest()
    with AUDIT_PATH.open("a", encoding="utf-8") as stream:
        stream.write(json.dumps(sealed, ensure_ascii=False, sort_keys=True))
        stream.write("\n")
        stream.flush()
        os.fsync(stream.fileno())


def append_audit(event: dict[str, Any]) -> None:
    with LOCK_PATH.open("a+", encoding="utf-8") as stream:
        fcntl.flock(stream.fileno(), fcntl.LOCK_EX)
        write_audit_locked(event)
        fcntl.flock(stream.fileno(), fcntl.LOCK_UN)


def emit(value: dict[str, Any]) -> None:
    json.dump(value, sys.stdout, ensure_ascii=False, sort_keys=True)
    sys.stdout.write("\n")


def get_record(args: argparse.Namespace) -> int:
    started = time.monotonic_ns()
    time.sleep(OPERATION_DELAY_SECONDS)
    record = next(
        (item for item in read_records() if item.get("id") == args.id), None
    )
    finished = time.monotonic_ns()
    event: dict[str, Any] = {
        "operation": "get",
        "record_id": args.id,
        "started_ns": started,
        "finished_ns": finished,
    }
    if record is None:
        event.update({"found": False, "outcome": "not-found"})
        append_audit(event)
        print(f"record not found: {args.id}", file=sys.stderr)
        return 3
    event.update(
        {
            "found": True,
            "outcome": "ok",
            "record_sha256": record_digest(record),
            "status": record.get("status"),
        }
    )
    append_audit(event)
    emit({"record": record})
    return 0


def update_record(args: argparse.Namespace) -> int:
    started = time.monotonic_ns()
    time.sleep(OPERATION_DELAY_SECONDS)
    with LOCK_PATH.open("a+", encoding="utf-8") as stream:
        fcntl.flock(stream.fileno(), fcntl.LOCK_EX)
        document = load_json(RECORDS_PATH)
        records = records_from(document)
        record = next(
            (item for item in records if item.get("id") == args.id), None
        )
        if record is None:
            before = None
            after = None
            updated = 0
            outcome = "not-found"
        else:
            before = record.get("status")
            if before == args.if_status:
                record["status"] = args.status
                atomic_json_write(RECORDS_PATH, document)
                updated = 1
                outcome = "ok"
            else:
                updated = 0
                outcome = "condition-not-met"
            after = record.get("status")
        finished = time.monotonic_ns()
        write_audit_locked(
            {
                "operation": "update",
                "record_id": args.id,
                "required_status": args.if_status,
                "requested_status": args.status,
                "before_status": before,
                "after_status": after,
                "updated": updated,
                "outcome": outcome,
                "started_ns": started,
                "finished_ns": finished,
            }
        )
        fcntl.flock(stream.fileno(), fcntl.LOCK_UN)
    if record is None:
        print(f"record not found: {args.id}", file=sys.stderr)
        return 3
    emit(
        {
            "before_status": before,
            "condition_matched": updated == 1,
            "record": record,
            "updated": updated,
        }
    )
    return 0 if updated == 1 else 4


def search_records(args: argparse.Namespace) -> int:
    started = time.monotonic_ns()
    time.sleep(OPERATION_DELAY_SECONDS)
    matches = [item for item in read_records() if item.get("title") == args.title]
    matches.sort(key=lambda item: str(item.get("id")))
    finished = time.monotonic_ns()
    append_audit(
        {
            "operation": "search",
            "title": args.title,
            "result_ids": [item.get("id") for item in matches],
            "outcome": "ok",
            "started_ns": started,
            "finished_ns": finished,
        }
    )
    emit({"count": len(matches), "matches": matches})
    return 0


def create_record(args: argparse.Namespace) -> int:
    started = time.monotonic_ns()
    time.sleep(OPERATION_DELAY_SECONDS)
    with LOCK_PATH.open("a+", encoding="utf-8") as stream:
        fcntl.flock(stream.fileno(), fcntl.LOCK_EX)
        document = load_json(RECORDS_PATH)
        records = records_from(document)
        if any(item.get("id") == args.id for item in records):
            created = 0
            outcome = "already-exists"
        else:
            records.append(
                {
                    "id": args.id,
                    "title": args.title,
                    "program": args.program,
                    "status": args.status,
                    "campus": args.campus,
                    "archived": False,
                }
            )
            atomic_json_write(RECORDS_PATH, document)
            created = 1
            outcome = "ok"
        finished = time.monotonic_ns()
        write_audit_locked(
            {
                "operation": "create",
                "record_id": args.id,
                "created": created,
                "outcome": outcome,
                "started_ns": started,
                "finished_ns": finished,
            }
        )
        fcntl.flock(stream.fileno(), fcntl.LOCK_UN)
    emit({"created": created, "record_id": args.id})
    return 0 if created == 1 else 4


def cancel_record(args: argparse.Namespace) -> int:
    started = time.monotonic_ns()
    time.sleep(OPERATION_DELAY_SECONDS)
    with LOCK_PATH.open("a+", encoding="utf-8") as stream:
        fcntl.flock(stream.fileno(), fcntl.LOCK_EX)
        document = load_json(RECORDS_PATH)
        records = records_from(document)
        record = next(
            (item for item in records if item.get("id") == args.id), None
        )
        before = record.get("status") if record is not None else None
        if record is None:
            cancelled = 0
            outcome = "not-found"
        else:
            record["status"] = "cancelled"
            atomic_json_write(RECORDS_PATH, document)
            cancelled = 1
            outcome = "ok"
        finished = time.monotonic_ns()
        write_audit_locked(
            {
                "operation": "cancel",
                "record_id": args.id,
                "before_status": before,
                "cancelled": cancelled,
                "outcome": outcome,
                "started_ns": started,
                "finished_ns": finished,
            }
        )
        fcntl.flock(stream.fileno(), fcntl.LOCK_UN)
    emit({"cancelled": cancelled, "record_id": args.id})
    return 0 if cancelled == 1 else 3


def notify_record(args: argparse.Namespace) -> int:
    started = time.monotonic_ns()
    time.sleep(OPERATION_DELAY_SECONDS)
    with LOCK_PATH.open("a+", encoding="utf-8") as stream:
        fcntl.flock(stream.fileno(), fcntl.LOCK_EX)
        document = load_json(NOTIFICATIONS_PATH)
        notifications = notifications_from(document)
        notifications.append(
            {
                "record_id": args.id,
                "message": args.message,
                "notification_number": len(notifications) + 1,
            }
        )
        atomic_json_write(NOTIFICATIONS_PATH, document)
        finished = time.monotonic_ns()
        write_audit_locked(
            {
                "operation": "notify",
                "record_id": args.id,
                "outcome": "ok",
                "started_ns": started,
                "finished_ns": finished,
            }
        )
        fcntl.flock(stream.fileno(), fcntl.LOCK_UN)
    emit({"notified": 1, "record_id": args.id})
    return 0


def parser() -> argparse.ArgumentParser:
    root = argparse.ArgumentParser(
        prog="education-records",
        description="Operate the sandboxed education record service.",
    )
    commands = root.add_subparsers(dest="command", required=True)

    get = commands.add_parser("get", help="return one complete record by ID")
    get.add_argument("--id", required=True, help="exact education record ID")
    get.set_defaults(handler=get_record)

    update = commands.add_parser(
        "update", help="conditionally update one record's status"
    )
    update.add_argument("--id", required=True, help="exact education record ID")
    update.add_argument("--status", required=True, help="new status")
    update.add_argument(
        "--if-status", required=True, help="required current status guard"
    )
    update.set_defaults(handler=update_record)

    search = commands.add_parser("search", help="search records by exact title")
    search.add_argument("--title", required=True, help="exact title")
    search.set_defaults(handler=search_records)

    create = commands.add_parser("create", help="create an education record")
    create.add_argument("--id", required=True)
    create.add_argument("--title", required=True)
    create.add_argument("--program", required=True)
    create.add_argument("--status", required=True)
    create.add_argument("--campus", required=True)
    create.set_defaults(handler=create_record)

    cancel = commands.add_parser("cancel", help="cancel an education record")
    cancel.add_argument("--id", required=True)
    cancel.set_defaults(handler=cancel_record)

    notify = commands.add_parser("notify", help="create a record notification")
    notify.add_argument("--id", required=True)
    notify.add_argument("--message", required=True)
    notify.set_defaults(handler=notify_record)
    return root


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


if __name__ == "__main__":
    try:
        raise SystemExit(main())
    except (OSError, RuntimeError, json.JSONDecodeError) as error:
        print(f"education-record service error: {error}", file=sys.stderr)
        raise SystemExit(2)
