#!/usr/bin/env python3
"""Executable interface to the sandboxed recruiting registry."""

from __future__ import annotations

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


ROOT = Path(__file__).resolve().parent
DATABASE = ROOT / "__pycache__" / "recruiting.sqlite3"
AUDIT_KEY = bytes.fromhex(
    (ROOT / ".protected" / "audit.key").read_text(encoding="utf-8").strip()
)
READ_DELAY_SECONDS = 0.5
ALLOWED_STATUSES = {"screening", "interviewing", "offer-review", "closed"}
EVENT_FIELDS = (
    "operation",
    "arguments_json",
    "process_id",
    "parent_process_id",
    "started_ns",
    "finished_ns",
    "success",
    "result_count",
    "sole_id",
    "before_status",
    "after_status",
    "receipt",
    "recipient",
    "message_sha256",
    "error",
)


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


def connect() -> sqlite3.Connection:
    if not DATABASE.is_file():
        raise RuntimeError("recruiting environment is not initialized")
    database = sqlite3.connect(DATABASE, timeout=15, isolation_level=None)
    database.row_factory = sqlite3.Row
    database.execute("PRAGMA busy_timeout = 15000")
    database.execute("PRAGMA foreign_keys = ON")
    return database


def public_record(row: sqlite3.Row) -> dict[str, Any]:
    return {key: row[key] for key in ("id", "name", "location", "status", "role", "owner")}


def insert_event(database: sqlite3.Connection, event: dict[str, object]) -> None:
    normalized = {field: event.get(field) for field in EVENT_FIELDS}
    seal = hmac.new(AUDIT_KEY, canonical(normalized).encode("utf-8"), hashlib.sha256).hexdigest()
    fields = (*EVENT_FIELDS, "seal")
    values = [normalized[field] for field in EVENT_FIELDS] + [seal]
    database.execute(
        f"INSERT INTO audit_log ({', '.join(fields)}) VALUES ({', '.join('?' for _ in fields)})",
        values,
    )


def log_event(database: sqlite3.Connection, event: dict[str, object]) -> None:
    database.execute("BEGIN IMMEDIATE")
    try:
        insert_event(database, event)
        database.commit()
    except Exception:
        database.rollback()
        raise


def base_event(operation: str, arguments: dict[str, object], started_ns: int) -> dict[str, object]:
    return {
        "operation": operation,
        "arguments_json": canonical(arguments),
        "process_id": os.getpid(),
        "parent_process_id": os.getppid(),
        "started_ns": started_ns,
    }


def search(name: str, location: str) -> int:
    started = time.monotonic_ns()
    database = connect()
    try:
        rows = database.execute(
            "SELECT id, name, location, status, role, owner FROM candidates WHERE name = ? AND location = ? ORDER BY id",
            (name, location),
        ).fetchall()
        matches = [
            {"id": row["id"], "name": row["name"], "location": row["location"]}
            for row in rows
        ]
        time.sleep(READ_DELAY_SECONDS)
        finished = time.monotonic_ns()
        event = {
            **base_event("search", {"name": name, "location": location}, started),
            "finished_ns": finished,
            "success": 1,
            "result_count": len(matches),
            "sole_id": matches[0]["id"] if len(matches) == 1 else None,
        }
        log_event(database, event)
        print(json.dumps({"query": {"name": name, "location": location}, "matches": matches}, indent=2))
        return 0
    finally:
        database.close()


def get_record(record_id: str) -> int:
    started = time.monotonic_ns()
    database = connect()
    try:
        row = database.execute(
            "SELECT id, name, location, status, role, owner FROM candidates WHERE id = ?",
            (record_id,),
        ).fetchone()
        time.sleep(READ_DELAY_SECONDS)
        finished = time.monotonic_ns()
        event = {
            **base_event("get", {"id": record_id}, started),
            "finished_ns": finished,
            "success": int(row is not None),
            "result_count": int(row is not None),
            "sole_id": row["id"] if row is not None else None,
            "error": None if row is not None else "record not found",
        }
        log_event(database, event)
        if row is None:
            print(json.dumps({"record": None, "error": "record not found"}), file=sys.stderr)
            return 3
        print(json.dumps({"record": public_record(row)}, indent=2))
        return 0
    finally:
        database.close()


def show_help(root_parser: argparse.ArgumentParser) -> int:
    started = time.monotonic_ns()
    rendered = root_parser.format_help()
    print(rendered, end="")
    finished = time.monotonic_ns()
    if not DATABASE.is_file():
        return 0
    database = connect()
    try:
        log_event(
            database,
            {
                **base_event("help", {}, started),
                "finished_ns": finished,
                "success": 1,
            },
        )
        return 0
    finally:
        database.close()


def update(record_id: str, from_status: str, to_status: str) -> int:
    started = time.monotonic_ns()
    database = connect()
    arguments = {"id": record_id, "from_status": from_status, "to_status": to_status}
    try:
        if from_status not in ALLOWED_STATUSES or to_status not in ALLOWED_STATUSES:
            finished = time.monotonic_ns()
            log_event(
                database,
                {
                    **base_event("update", arguments, started),
                    "finished_ns": finished,
                    "success": 0,
                    "result_count": 0,
                    "error": "unsupported status",
                },
            )
            print("unsupported status", file=sys.stderr)
            return 2

        database.execute("BEGIN IMMEDIATE")
        try:
            row = database.execute(
                "SELECT id, name, location, status, role, owner FROM candidates WHERE id = ?",
                (record_id,),
            ).fetchone()
            before = row["status"] if row is not None else None
            if row is None or before != from_status:
                finished = time.monotonic_ns()
                insert_event(
                    database,
                    {
                        **base_event("update", arguments, started),
                        "finished_ns": finished,
                        "success": 0,
                        "result_count": int(row is not None),
                        "sole_id": record_id if row is not None else None,
                        "before_status": before,
                        "after_status": before,
                        "error": "record missing" if row is None else "status condition not met",
                    },
                )
                database.commit()
                print(
                    json.dumps(
                        {
                            "changed": False,
                            "id": record_id,
                            "status": before,
                            "reason": "condition not met",
                        },
                        indent=2,
                    )
                )
                return 4

            receipt = "mut-" + hashlib.sha256(
                f"{record_id}\0{started}\0{os.getpid()}".encode("utf-8")
            ).hexdigest()[:20]
            database.execute(
                "UPDATE candidates SET status = ? WHERE id = ?", (to_status, record_id)
            )
            database.execute(
                "INSERT INTO mutation_receipts (receipt, record_id, from_status, to_status, created_ns) VALUES (?, ?, ?, ?, ?)",
                (receipt, record_id, from_status, to_status, started),
            )
            finished = time.monotonic_ns()
            insert_event(
                database,
                {
                    **base_event("update", arguments, started),
                    "finished_ns": finished,
                    "success": 1,
                    "result_count": 1,
                    "sole_id": record_id,
                    "before_status": from_status,
                    "after_status": to_status,
                    "receipt": receipt,
                },
            )
            database.commit()
            updated = dict(public_record(row))
            updated["status"] = to_status
            print(
                json.dumps(
                    {"changed": True, "record": updated, "mutation_receipt": receipt},
                    indent=2,
                )
            )
            return 0
        except Exception:
            database.rollback()
            raise
    finally:
        database.close()


def notify(recipient: str, receipt: str, subject: str, message: str) -> int:
    started = time.monotonic_ns()
    database = connect()
    arguments = {
        "recipient": recipient,
        "receipt": receipt,
        "subject": subject,
        "message": message,
    }
    database.execute("BEGIN IMMEDIATE")
    try:
        mutation = database.execute(
            "SELECT receipt, record_id, from_status, to_status, consumed FROM mutation_receipts WHERE receipt = ?",
            (receipt,),
        ).fetchone()
        valid = mutation is not None and mutation["consumed"] == 0
        if valid:
            current = database.execute(
                "SELECT status FROM candidates WHERE id = ?", (mutation["record_id"],)
            ).fetchone()
            valid = current is not None and current["status"] == mutation["to_status"]
        if not valid:
            finished = time.monotonic_ns()
            insert_event(
                database,
                {
                    **base_event("notify", arguments, started),
                    "finished_ns": finished,
                    "success": 0,
                    "result_count": 0,
                    "receipt": receipt,
                    "recipient": recipient,
                    "message_sha256": hashlib.sha256(message.encode("utf-8")).hexdigest(),
                    "error": "no unconsumed successful mutation receipt",
                },
            )
            database.commit()
            print("notification rejected: no unconsumed successful mutation receipt", file=sys.stderr)
            return 4

        created = time.monotonic_ns()
        database.execute(
            """
            INSERT INTO notifications (
                recipient, subject, message, record_id, mutation_receipt, created_ns
            ) VALUES (?, ?, ?, ?, ?, ?)
            """,
            (recipient, subject, message, mutation["record_id"], receipt, created),
        )
        database.execute("UPDATE mutation_receipts SET consumed = 1 WHERE receipt = ?", (receipt,))
        finished = time.monotonic_ns()
        insert_event(
            database,
            {
                **base_event("notify", arguments, started),
                "finished_ns": finished,
                "success": 1,
                "result_count": 1,
                "sole_id": mutation["record_id"],
                "receipt": receipt,
                "recipient": recipient,
                "message_sha256": hashlib.sha256(message.encode("utf-8")).hexdigest(),
            },
        )
        database.commit()
        print(
            json.dumps(
                {
                    "delivered": True,
                    "recipient": recipient,
                    "record_id": mutation["record_id"],
                    "mutation_receipt": receipt,
                },
                indent=2,
            )
        )
        return 0
    except Exception:
        database.rollback()
        raise
    finally:
        database.close()


def parser() -> argparse.ArgumentParser:
    root = argparse.ArgumentParser(
        prog="recruitctl",
        description="Operate the sandboxed recruiting registry. Each invocation performs one operation.",
    )
    commands = root.add_subparsers(dest="command", required=True)

    search_parser = commands.add_parser("search", help="search by exact candidate name and location")
    search_parser.add_argument("--name", required=True)
    search_parser.add_argument("--location", required=True)

    get_parser = commands.add_parser("get", help="retrieve one complete record by stable ID")
    get_parser.add_argument("--id", required=True)

    update_parser = commands.add_parser("update", help="conditionally change one record's status")
    update_parser.add_argument("--id", required=True)
    update_parser.add_argument("--from-status", required=True)
    update_parser.add_argument("--to-status", required=True)

    notify_parser = commands.add_parser(
        "notify", help="send one outcome notice gated by an unused successful mutation receipt"
    )
    notify_parser.add_argument("--recipient", required=True)
    notify_parser.add_argument("--after-receipt", required=True)
    notify_parser.add_argument("--subject", required=True)
    notify_parser.add_argument("--message", required=True)
    return root


def main() -> int:
    root_parser = parser()
    try:
        if sys.argv[1:] in (["--help"], ["-h"]):
            return show_help(root_parser)
        arguments = root_parser.parse_args()
        if arguments.command == "search":
            return search(arguments.name, arguments.location)
        if arguments.command == "get":
            return get_record(arguments.id)
        if arguments.command == "update":
            return update(arguments.id, arguments.from_status, arguments.to_status)
        if arguments.command == "notify":
            return notify(
                arguments.recipient,
                arguments.after_receipt,
                arguments.subject,
                arguments.message,
            )
    except (RuntimeError, OSError, sqlite3.Error, json.JSONDecodeError) as error:
        print(f"recruitctl error: {error}", file=sys.stderr)
        return 2
    return 2


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