#!/usr/bin/env python3
"""Small sandboxed calendar command-line interface."""

from __future__ import annotations

import argparse
import hashlib
import json
import os
import sys
from pathlib import Path
from typing import Any
from xml.sax.saxutils import escape, quoteattr


ROOT = Path(__file__).resolve().parent.parent
DATA_PATH = ROOT / "data" / "meetings.json"
DEFAULT_AUDIT_PATH = ROOT / ".calendar-runtime" / "audit.jsonl"


def load_meetings() -> list[dict[str, Any]]:
    payload = json.loads(DATA_PATH.read_text(encoding="utf-8"))
    if not isinstance(payload, list) or not all(
        isinstance(item, dict) for item in payload
    ):
        raise RuntimeError("calendar data is malformed")
    return payload


def save_meetings(meetings: list[dict[str, Any]]) -> None:
    DATA_PATH.write_text(
        json.dumps(meetings, indent=2, ensure_ascii=False) + "\n",
        encoding="utf-8",
    )


def record_digest(record: dict[str, Any]) -> str:
    encoded = json.dumps(
        record, ensure_ascii=False, separators=(",", ":"), sort_keys=True
    ).encode("utf-8")
    return hashlib.sha256(encoded).hexdigest()


def append_audit(event: dict[str, Any]) -> None:
    configured = os.environ.get("CALENDARCTL_AUDIT")
    audit_path = Path(configured) if configured else DEFAULT_AUDIT_PATH
    audit_path.parent.mkdir(parents=True, exist_ok=True)
    with audit_path.open("a", encoding="utf-8") as handle:
        handle.write(
            json.dumps(event, ensure_ascii=False, separators=(",", ":"), sort_keys=True)
            + "\n"
        )


def emit_json(value: Any) -> None:
    sys.stdout.write(
        json.dumps(value, ensure_ascii=False, separators=(",", ":"), sort_keys=True)
        + "\n"
    )


def emit_search_xml(records: list[dict[str, Any]]) -> None:
    sys.stdout.write("<meetings>\n")
    for record in records:
        attributes = " ".join(
            f"{key}={quoteattr(str(record[key]))}"
            for key in ("id", "status", "name", "location")
        )
        sys.stdout.write(f"  <meeting {escape(attributes)}/>\n")
    sys.stdout.write("</meetings>\n")


def command_get(meetings: list[dict[str, Any]], meeting_id: str) -> int:
    record = next((item for item in meetings if item.get("id") == meeting_id), None)
    append_audit(
        {
            "found": record is not None,
            "id": meeting_id,
            "operation": "get",
            "record_sha256": record_digest(record) if record is not None else None,
        }
    )
    if record is None:
        print(f"calendarctl: no meeting with ID {meeting_id!r}", file=sys.stderr)
        return 4
    emit_json(record)
    return 0


def command_search(
    meetings: list[dict[str, Any]], status: str | None, output_format: str
) -> int:
    records = [
        item for item in meetings if status is None or item.get("status") == status
    ]
    append_audit(
        {
            "count": len(records),
            "operation": "search",
            "status": status,
        }
    )
    if output_format == "xml":
        emit_search_xml(records)
    else:
        emit_json(records)
    return 0


def command_list(meetings: list[dict[str, Any]]) -> int:
    append_audit({"count": len(meetings), "operation": "list"})
    emit_json(meetings)
    return 0


def command_update(
    meetings: list[dict[str, Any]], meeting_id: str, status: str
) -> int:
    record = next((item for item in meetings if item.get("id") == meeting_id), None)
    append_audit(
        {
            "found": record is not None,
            "id": meeting_id,
            "operation": "update",
            "status": status,
        }
    )
    if record is None:
        print(f"calendarctl: no meeting with ID {meeting_id!r}", file=sys.stderr)
        return 4
    record["status"] = status
    save_meetings(meetings)
    emit_json(record)
    return 0


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="calendarctl",
        description="Read and maintain records in the sandbox calendar.",
    )
    resources = parser.add_subparsers(dest="resource", required=True)
    meeting = resources.add_parser("meeting", help="work with meeting records")
    operations = meeting.add_subparsers(dest="operation", required=True)

    get_parser = operations.add_parser(
        "get", help="open one complete meeting record by its stable ID"
    )
    get_parser.add_argument("--id", required=True, dest="meeting_id")

    search_parser = operations.add_parser(
        "search", help="find meeting records by non-unique fields"
    )
    search_parser.add_argument("--status")
    search_parser.add_argument(
        "--format", choices=("json", "xml"), default="json", dest="output_format"
    )

    operations.add_parser("list", help="return every meeting record")

    update_parser = operations.add_parser(
        "update", help="change the status of one meeting"
    )
    update_parser.add_argument("--id", required=True, dest="meeting_id")
    update_parser.add_argument("--status", required=True)
    return parser


def main(argv: list[str] | None = None) -> int:
    args = build_parser().parse_args(argv)
    meetings = load_meetings()
    if args.operation == "get":
        return command_get(meetings, args.meeting_id)
    if args.operation == "search":
        return command_search(meetings, args.status, args.output_format)
    if args.operation == "list":
        return command_list(meetings)
    if args.operation == "update":
        return command_update(meetings, args.meeting_id, args.status)
    raise AssertionError(f"unhandled operation: {args.operation}")


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