#!/usr/bin/env python3
"""Executable interface to the sandboxed public-services registry."""

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 tempfile
import time
from typing import Any, Callable


ROOT = Path(__file__).resolve().parent
STATE_DIR = ROOT / ".public-services"
SEED_PATH = STATE_DIR / "seed.sql"
RUNTIME_DIR = STATE_DIR / "runtime"
DATABASE_PATH = RUNTIME_DIR / "public-services.sqlite3"
AUDIT_PATH = RUNTIME_DIR / "audit.jsonl"
LOCK_PATH = RUNTIME_DIR / "registry.lock"
KEY_PATH = ROOT / ".protected" / "audit.key"
REFERENCE_MARKER = ROOT / ".reference_solution"
READ_DELAY_SECONDS = 0.45
PRIORITY = [
    ("Food cart license", "Arvada"),
    ("Block party application", "Wheat Ridge"),
]


class PublicServicesError(RuntimeError):
    """A user-facing public-services-registry error."""


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


def digest(value: Any) -> str:
    return hashlib.sha256(canonical(value)).hexdigest()


def parent_start_ticks() -> int | str:
    try:
        stat = Path(f"/proc/{os.getppid()}/stat").read_text(encoding="utf-8")
        fields = stat[stat.rfind(")") + 2 :].split()
        return int(fields[19])
    except (OSError, ValueError, IndexError):
        return "unavailable"


def parent_executable() -> str:
    try:
        return Path(os.readlink(f"/proc/{os.getppid()}/exe")).name
    except OSError:
        return "unavailable"


def process_evidence() -> dict[str, int | str]:
    return {
        "parent_executable": parent_executable(),
        "parent_pid": os.getppid(),
        "parent_start_ticks": parent_start_ticks(),
        "process_group_id": os.getpgrp(),
        "process_pid": os.getpid(),
        "session_id": os.getsid(0),
    }


def ensure_database() -> None:
    RUNTIME_DIR.mkdir(parents=True, exist_ok=True)
    with LOCK_PATH.open("a+", encoding="utf-8") as lock_stream:
        fcntl.flock(lock_stream.fileno(), fcntl.LOCK_EX)
        if not DATABASE_PATH.exists():
            descriptor, temporary_name = tempfile.mkstemp(
                dir=RUNTIME_DIR, prefix=".public-services.", suffix=".sqlite3"
            )
            os.close(descriptor)
            temporary = Path(temporary_name)
            try:
                connection = sqlite3.connect(temporary)
                try:
                    connection.executescript(SEED_PATH.read_text(encoding="utf-8"))
                    connection.commit()
                finally:
                    connection.close()
                os.replace(temporary, DATABASE_PATH)
            finally:
                temporary.unlink(missing_ok=True)
        AUDIT_PATH.touch(exist_ok=True)
        fcntl.flock(lock_stream.fileno(), fcntl.LOCK_UN)


def connect() -> sqlite3.Connection:
    connection = sqlite3.connect(DATABASE_PATH, timeout=10)
    connection.row_factory = sqlite3.Row
    connection.execute("PRAGMA foreign_keys = ON")
    return connection


def row_object(row: sqlite3.Row) -> dict[str, Any]:
    result = dict(row)
    for field in ("archived", "available"):
        if field in result:
            result[field] = bool(result[field])
    return result


def load_events_unlocked() -> list[dict[str, Any]]:
    events: list[dict[str, Any]] = []
    if not AUDIT_PATH.exists():
        return events
    for line_number, line in enumerate(
        AUDIT_PATH.read_text(encoding="utf-8").splitlines(), 1
    ):
        if not line.strip():
            continue
        try:
            event = json.loads(line)
        except json.JSONDecodeError as error:
            raise PublicServicesError(
                f"invalid audit event {line_number}: {error}"
            ) from error
        if not isinstance(event, dict):
            raise PublicServicesError(f"invalid audit event {line_number}")
        events.append(event)
    return events


def read_events() -> list[dict[str, Any]]:
    with LOCK_PATH.open("a+", encoding="utf-8") as lock_stream:
        fcntl.flock(lock_stream.fileno(), fcntl.LOCK_EX)
        events = load_events_unlocked()
        fcntl.flock(lock_stream.fileno(), fcntl.LOCK_UN)
    return events


def append_event(event: dict[str, Any]) -> None:
    with LOCK_PATH.open("a+", encoding="utf-8") as lock_stream:
        fcntl.flock(lock_stream.fileno(), fcntl.LOCK_EX)
        events = load_events_unlocked()
        sealed = {
            "sequence": len(events) + 1,
            "reference_marker": REFERENCE_MARKER.is_file(),
            **process_evidence(),
            **event,
        }
        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 audit_stream:
            audit_stream.write(
                json.dumps(sealed, ensure_ascii=False, sort_keys=True) + "\n"
            )
            audit_stream.flush()
            os.fsync(audit_stream.fileno())
        fcntl.flock(lock_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 read_profile() -> dict[str, Any]:
    with connect() as connection:
        row = connection.execute(
            "SELECT default_date, preferred_quantity FROM profile WHERE singleton = 1"
        ).fetchone()
    if row is None:
        raise PublicServicesError("saved operational profile is missing")
    return row_object(row)


def require_profile_layer(date: str) -> dict[str, Any]:
    events = read_events()
    if len(events) != 1 or events[0].get("operation") != "profile":
        raise PublicServicesError("availability requires one completed profile operation")
    event = events[0]
    if event.get("success") is not True:
        raise PublicServicesError("the profile operation did not succeed")
    if event.get("default_date") != date:
        raise PublicServicesError("date does not match the saved profile result")
    return event


def require_availability_layer() -> list[dict[str, Any]]:
    events = read_events()
    if len(events) != 3:
        raise PublicServicesError(
            "create requires one profile and two completed availability checks"
        )
    if events[0].get("operation") != "profile" or any(
        event.get("operation") != "availability" for event in events[1:]
    ):
        raise PublicServicesError("the required dependency layers are incomplete")
    if any(event.get("success") is not True for event in events):
        raise PublicServicesError("a prerequisite public-services operation did not succeed")
    return events


def first_available_event(events: list[dict[str, Any]]) -> dict[str, Any] | None:
    profile_date = events[0].get("default_date")
    found: dict[tuple[Any, Any], dict[str, Any]] = {}
    for event in events[1:]:
        scope = (event.get("name"), event.get("location"))
        if scope in found:
            raise PublicServicesError("a required availability check was duplicated")
        if (
            scope not in PRIORITY
            or event.get("date") != profile_date
            or event.get("found") is not True
            or not isinstance(event.get("available"), bool)
            or not isinstance(event.get("option_id"), str)
            or not event.get("option_id")
        ):
            raise PublicServicesError("a required availability result is unresolved")
        found[scope] = event
    if set(found) != set(PRIORITY):
        raise PublicServicesError("the required availability layer is incomplete")
    for scope in PRIORITY:
        if found[scope]["available"]:
            return found[scope]
    return None


def command_profile(_: argparse.Namespace) -> int:
    started = time.monotonic_ns()
    if read_events():
        raise PublicServicesError("profile must be the first public-services operation")
    profile = read_profile()
    time.sleep(0.05)
    finished = time.monotonic_ns()
    append_event(
        {
            "default_date": profile["default_date"],
            "finished_ns": finished,
            "operation": "profile",
            "preferred_quantity": profile["preferred_quantity"],
            "profile_sha256": digest(profile),
            "started_ns": started,
            "success": True,
        }
    )
    emit({"profile": profile})
    return 0


def command_availability(arguments: argparse.Namespace) -> int:
    started = time.monotonic_ns()
    require_profile_layer(arguments.date)
    with connect() as connection:
        row = connection.execute(
            """
            SELECT option_id, name, location, service_date AS date, available
            FROM availability
            WHERE name = ? AND location = ? AND service_date = ?
            """,
            (arguments.name, arguments.location, arguments.date),
        ).fetchone()
    time.sleep(READ_DELAY_SECONDS)
    result = (
        row_object(row)
        if row is not None
        else {
            "option_id": None,
            "name": arguments.name,
            "location": arguments.location,
            "date": arguments.date,
            "available": False,
        }
    )
    finished = time.monotonic_ns()
    append_event(
        {
            "available": result["available"],
            "date": arguments.date,
            "finished_ns": finished,
            "found": row is not None,
            "location": arguments.location,
            "name": arguments.name,
            "operation": "availability",
            "option_id": result["option_id"],
            "result_sha256": digest(result),
            "started_ns": started,
            "success": True,
        }
    )
    emit({"availability": result, "found": row is not None})
    return 0


def command_create(arguments: argparse.Namespace) -> int:
    started = time.monotonic_ns()
    events = require_availability_layer()
    expected = first_available_event(events)
    if expected is None:
        raise PublicServicesError("neither checked option is available; create nothing")
    if arguments.option_id != expected["option_id"]:
        raise PublicServicesError("option is not the first available checked result")
    if arguments.date != events[0].get("default_date"):
        raise PublicServicesError("date does not match the saved profile result")
    if arguments.quantity != 1:
        raise PublicServicesError("this request requires quantity 1")

    with LOCK_PATH.open("a+", encoding="utf-8") as lock_stream:
        fcntl.flock(lock_stream.fileno(), fcntl.LOCK_EX)
        connection = connect()
        try:
            connection.execute("BEGIN IMMEDIATE")
            slot = connection.execute(
                """
                SELECT option_id, name, location, service_date, available
                FROM availability WHERE option_id = ? AND service_date = ?
                """,
                (arguments.option_id, arguments.date),
            ).fetchone()
            if (
                slot is None
                or not bool(slot["available"])
                or slot["name"] != expected["name"]
                or slot["location"] != expected["location"]
            ):
                connection.rollback()
                raise PublicServicesError("the selected checked option is not available")
            record_id_row = connection.execute(
                "SELECT value FROM metadata WHERE key = 'created_record_id'"
            ).fetchone()
            if record_id_row is None:
                connection.rollback()
                raise PublicServicesError("record ID configuration is missing")
            record_id = str(record_id_row["value"])
            if connection.execute(
                "SELECT 1 FROM requests WHERE id = ?", (record_id,)
            ).fetchone():
                connection.rollback()
                raise PublicServicesError("the public-services record already exists")
            connection.execute(
                """
                INSERT INTO requests(
                    id, option_id, name, location, service_date, quantity,
                    status, archived, relation
                ) VALUES (?, ?, ?, ?, ?, ?, 'submitted', 0, 'requested')
                """,
                (
                    record_id,
                    slot["option_id"],
                    slot["name"],
                    slot["location"],
                    slot["service_date"],
                    arguments.quantity,
                ),
            )
            row = connection.execute(
                """
                SELECT id, option_id, name, location, service_date AS date,
                       quantity, status, archived, relation
                FROM requests WHERE id = ?
                """,
                (record_id,),
            ).fetchone()
            connection.commit()
        finally:
            connection.close()
            fcntl.flock(lock_stream.fileno(), fcntl.LOCK_UN)
    if row is None:
        raise PublicServicesError("created record could not be read")
    record = row_object(row)
    finished = time.monotonic_ns()
    append_event(
        {
            "date": arguments.date,
            "finished_ns": finished,
            "operation": "create",
            "option_id": arguments.option_id,
            "quantity": arguments.quantity,
            "record_id": record["id"],
            "record_sha256": digest(record),
            "started_ns": started,
            "success": True,
        }
    )
    emit({"record": record, "created": True})
    return 0


def mutate_request(
    operation: str,
    request_id: str,
    mutation: Callable[[sqlite3.Connection, sqlite3.Row], dict[str, Any]],
) -> dict[str, Any]:
    started = time.monotonic_ns()
    with LOCK_PATH.open("a+", encoding="utf-8") as lock_stream:
        fcntl.flock(lock_stream.fileno(), fcntl.LOCK_EX)
        connection = connect()
        try:
            connection.execute("BEGIN IMMEDIATE")
            row = connection.execute(
                "SELECT * FROM requests WHERE id = ?", (request_id,)
            ).fetchone()
            if row is None:
                connection.rollback()
                raise PublicServicesError(f"request not found: {request_id}")
            details = mutation(connection, row)
            connection.commit()
        finally:
            connection.close()
            fcntl.flock(lock_stream.fileno(), fcntl.LOCK_UN)
    finished = time.monotonic_ns()
    append_event(
        {
            **details,
            "finished_ns": finished,
            "operation": operation,
            "record_id": request_id,
            "started_ns": started,
            "success": True,
        }
    )
    return details


def command_update(arguments: argparse.Namespace) -> int:
    def mutation(connection: sqlite3.Connection, row: sqlite3.Row) -> dict[str, Any]:
        before = row["status"]
        connection.execute(
            "UPDATE requests SET status = ? WHERE id = ?",
            (arguments.status, arguments.id),
        )
        return {"before_status": before, "after_status": arguments.status}

    details = mutate_request("update", arguments.id, mutation)
    emit({"record_id": arguments.id, **details})
    return 0


def command_cancel(arguments: argparse.Namespace) -> int:
    def mutation(connection: sqlite3.Connection, row: sqlite3.Row) -> dict[str, Any]:
        before = row["status"]
        connection.execute(
            "UPDATE requests SET status = 'cancelled' WHERE id = ?", (arguments.id,)
        )
        return {"before_status": before, "after_status": "cancelled"}

    details = mutate_request("cancel", arguments.id, mutation)
    emit({"record_id": arguments.id, **details})
    return 0


def command_notify(arguments: argparse.Namespace) -> int:
    def mutation(connection: sqlite3.Connection, _: sqlite3.Row) -> dict[str, Any]:
        connection.execute(
            "INSERT INTO notifications(request_id, message) VALUES (?, ?)",
            (arguments.id, arguments.message),
        )
        return {"message_sha256": hashlib.sha256(arguments.message.encode()).hexdigest()}

    mutate_request("notify", arguments.id, mutation)
    emit({"notified": True, "record_id": arguments.id})
    return 0


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="publicservicesctl",
        description="Operate the local public-services registry; one invocation is one operation.",
    )
    commands = parser.add_subparsers(dest="command", required=True)

    profile = commands.add_parser("profile", help="return the saved operational profile")
    profile.set_defaults(handler=command_profile)

    availability = commands.add_parser(
        "availability", help="check one exact option, location, and date"
    )
    availability.add_argument("--name", required=True)
    availability.add_argument("--location", required=True)
    availability.add_argument("--date", required=True)
    availability.set_defaults(handler=command_availability)

    create = commands.add_parser("create", help="create one public-services record")
    create.add_argument("--option-id", required=True)
    create.add_argument("--date", required=True)
    create.add_argument("--quantity", required=True, type=int)
    create.set_defaults(handler=command_create)

    update = commands.add_parser("update", help="change a request status")
    update.add_argument("--id", required=True)
    update.add_argument("--status", required=True)
    update.set_defaults(handler=command_update)

    cancel = commands.add_parser("cancel", help="cancel a request")
    cancel.add_argument("--id", required=True)
    cancel.set_defaults(handler=command_cancel)

    notify = commands.add_parser("notify", help="create a request notification")
    notify.add_argument("--id", required=True)
    notify.add_argument("--message", required=True)
    notify.set_defaults(handler=command_notify)
    return parser


def main() -> int:
    ensure_database()
    arguments = build_parser().parse_args()
    try:
        return int(arguments.handler(arguments))
    except (PublicServicesError, OSError, sqlite3.Error, ValueError, json.JSONDecodeError) as error:
        print(f"publicservicesctl: {error}", file=sys.stderr)
        return 2


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