#!/usr/bin/env python3
"""Read-only facilities console with signed receipts for staged lookups."""

from __future__ import annotations

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


ROOT = Path(__file__).resolve().parent
DATABASE = ROOT / "data" / "facilities.sqlite3"
RECEIPT = Path.cwd() / ".facilitydesk" / "audit.json"
FORBIDDEN = (
    "list",
    "profile",
    "availability",
    "create",
    "update",
    "cancel",
    "notify",
)


def connect() -> sqlite3.Connection:
    connection = sqlite3.connect(f"file:{DATABASE}?mode=ro", uri=True)
    connection.row_factory = sqlite3.Row
    return connection


def signing_key() -> bytes:
    with connect() as connection:
        row = connection.execute(
            "SELECT value FROM metadata WHERE key = 'receipt_key'"
        ).fetchone()
    if row is None:
        raise RuntimeError("facilities data is missing receipt metadata")
    return row["value"].encode("utf-8")


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


def signature(event: dict[str, Any]) -> str:
    unsigned = {key: value for key, value in event.items() if key != "signature"}
    return hmac.new(signing_key(), canonical(unsigned), hashlib.sha256).hexdigest()


def load_events() -> list[dict[str, Any]]:
    if not RECEIPT.exists():
        return []
    try:
        document = json.loads(RECEIPT.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError) as error:
        raise RuntimeError(f"cannot read action receipt: {error}") from error
    events = document.get("events")
    if document.get("version") != 1 or not isinstance(events, list):
        raise RuntimeError("action receipt has an unsupported format")
    previous = "GENESIS"
    for sequence, event in enumerate(events, start=1):
        if event.get("sequence") != sequence or event.get("previous") != previous:
            raise RuntimeError("action receipt chain is invalid")
        observed = event.get("signature")
        if not isinstance(observed, str) or not hmac.compare_digest(
            observed, signature(event)
        ):
            raise RuntimeError("action receipt signature is invalid")
        previous = observed
    return events


def append_event(payload: dict[str, Any]) -> None:
    events = load_events()
    event = {
        "sequence": len(events) + 1,
        "previous": events[-1]["signature"] if events else "GENESIS",
        **payload,
    }
    event["signature"] = signature(event)
    events.append(event)
    RECEIPT.parent.mkdir(parents=True, exist_ok=True)
    temporary = RECEIPT.with_suffix(".tmp")
    temporary.write_text(
        json.dumps({"version": 1, "events": events}, indent=2, ensure_ascii=False)
        + "\n",
        encoding="utf-8",
    )
    temporary.replace(RECEIPT)


def search(query: str, location: str) -> int:
    prior = load_events()
    if prior:
        append_event(
            {
                "action": "rejected-search",
                "input": {"query": query, "location": location},
                "output": None,
            }
        )
        print("search must be the first facilities operation", file=sys.stderr)
        return 2
    with connect() as connection:
        rows = connection.execute(
            """
            SELECT request_id, name, location, status
              FROM facilities_requests
             WHERE name = ? AND location = ?
             ORDER BY request_id
            """,
            (query, location),
        ).fetchall()
    output = {
        "matches": [
            {
                "stable_id": row["request_id"],
                "name": row["name"],
                "location": row["location"],
                "status": row["status"],
            }
            for row in rows
        ]
    }
    append_event(
        {
            "action": "search",
            "input": {"query": query, "location": location},
            "output": output,
        }
    )
    print(json.dumps(output, indent=2, ensure_ascii=False))
    return 0


def get(stable_id: str) -> int:
    prior = load_events()
    eligible: set[str] = set()
    if len(prior) == 1 and prior[0].get("action") == "search":
        matches = (prior[0].get("output") or {}).get("matches", [])
        if isinstance(matches, list) and len(matches) == 1:
            candidate = matches[0].get("stable_id")
            if isinstance(candidate, str):
                eligible.add(candidate)
    if stable_id not in eligible:
        append_event(
            {
                "action": "rejected-get",
                "input": {"stable_id": stable_id},
                "output": None,
            }
        )
        print(
            "get requires the unique stable ID returned by the completed search",
            file=sys.stderr,
        )
        return 2
    with connect() as connection:
        row = connection.execute(
            """
            SELECT request_id AS stable_id, name, location, status,
                   request_date AS date
              FROM facilities_requests
             WHERE request_id = ?
            """,
            (stable_id,),
        ).fetchone()
    output = {"record": dict(row) if row else None}
    append_event(
        {
            "action": "get",
            "input": {"stable_id": stable_id},
            "output": output,
        }
    )
    print(json.dumps(output, indent=2, ensure_ascii=False))
    return 0 if row else 1


def forbidden(action: str) -> int:
    append_event(
        {
            "action": f"forbidden-{action}",
            "input": {},
            "output": None,
        }
    )
    print(f"{action} is unavailable in this read-only lookup", file=sys.stderr)
    return 2


def make_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="facilitydesk",
        description=(
            "Read-only facilities request console. Search results are abbreviated; "
            "get returns the authoritative full record and accepts only the unique "
            "stable ID established by the preceding search."
        ),
    )
    commands = parser.add_subparsers(dest="command", required=True)

    search_parser = commands.add_parser(
        "search", help="find exact matches using both request fields"
    )
    search_parser.add_argument(
        "--query", required=True, help="exact facilities request name"
    )
    search_parser.add_argument(
        "--location", required=True, help="exact facilities location"
    )

    get_parser = commands.add_parser(
        "get", help="retrieve one full record resolved by the preceding search"
    )
    get_parser.add_argument(
        "--id", required=True, dest="stable_id", help="returned stable ID"
    )

    for action in FORBIDDEN:
        commands.add_parser(action, help="disabled in this read-only console")
    return parser


def main() -> int:
    parser = make_parser()
    arguments = parser.parse_args()
    if arguments.command == "search":
        return search(arguments.query, arguments.location)
    if arguments.command == "get":
        return get(arguments.stable_id)
    if arguments.command in FORBIDDEN:
        return forbidden(arguments.command)
    parser.error("unknown operation")
    return 2


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