#!/usr/bin/env python3
"""Command-line interface to the sandboxed community-order service."""

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 / ".orders"
PROFILE_PATH = STATE_DIR / "profile.json"
AVAILABILITY_PATH = STATE_DIR / "availability.json"
RECORDS_PATH = STATE_DIR / "records.json"
NOTIFICATIONS_PATH = STATE_DIR / "notifications.json"
AUDIT_PATH = STATE_DIR / "audit.jsonl"
LOCK_PATH = STATE_DIR / "lock"
KEY_PATH = ROOT / ".protected" / "audit.key"
PROFILE_DELAY_SECONDS = 0.12
CHECK_DELAY_SECONDS = 0.55


class OrderError(RuntimeError):
    """A user-facing order-service 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 load_object(path: Path) -> dict[str, Any]:
    try:
        with path.open(encoding="utf-8") as stream:
            value = json.load(stream)
    except (OSError, json.JSONDecodeError) as error:
        raise OrderError(f"cannot read service data {path.name}: {error}") from error
    if not isinstance(value, dict):
        raise OrderError(f"invalid service data: {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, sort_keys=True)
            stream.write("\n")
            stream.flush()
            os.fsync(stream.fileno())
        os.replace(temporary, path)
    finally:
        if temporary.exists():
            temporary.unlink()


def read_profile() -> dict[str, Any]:
    document = load_object(PROFILE_PATH)
    profile = document.get("profile")
    if document.get("version") != 1 or not isinstance(profile, dict):
        raise OrderError("invalid operational profile")
    return profile


def read_options() -> list[dict[str, Any]]:
    document = load_object(AVAILABILITY_PATH)
    options = document.get("options")
    if document.get("version") != 1 or not isinstance(options, list):
        raise OrderError("invalid availability store")
    if not all(isinstance(option, dict) for option in options):
        raise OrderError("invalid availability entry")
    return options


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


def load_events() -> list[dict[str, Any]]:
    events: list[dict[str, Any]] = []
    try:
        with AUDIT_PATH.open(encoding="utf-8") as stream:
            for line_number, line in enumerate(stream, 1):
                if not line.strip():
                    continue
                event = json.loads(line)
                if not isinstance(event, dict):
                    raise OrderError(
                        f"invalid audit event at line {line_number}"
                    )
                events.append(event)
    except (OSError, json.JSONDecodeError) as error:
        raise OrderError(f"cannot read service audit: {error}") from error
    return events


def append_audit_unlocked(event: dict[str, Any]) -> None:
    sealed = dict(event)
    sealed["sequence"] = len(load_events()) + 1
    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 lock_stream:
        fcntl.flock(lock_stream.fileno(), fcntl.LOCK_EX)
        append_audit_unlocked(event)
        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 is_direct_tool_call() -> bool:
    """Distinguish one direct Bash-tool command from shell-managed child jobs."""
    try:
        parent_name = Path(f"/proc/{os.getppid()}/comm").read_text(
            encoding="utf-8"
        ).strip()
    except OSError:
        return False
    return parent_name not in {"bash", "dash", "fish", "ksh", "sh", "zsh"}


def require_help_layer() -> None:
    events = load_events()
    if len(events) != 1:
        raise OrderError("profile requires exactly one completed help invocation")
    help_event = events[0]
    if help_event.get("operation") != "help" or help_event.get("outcome") != "ok":
        raise OrderError("profile requires the completed built-in help invocation")


def require_profile_layer(date: str) -> None:
    events = load_events()
    if len(events) != 2:
        raise OrderError(
            "availability requires completed help and profile operations"
        )
    if events[0].get("operation") != "help" or events[0].get("outcome") != "ok":
        raise OrderError("availability requires the completed built-in help invocation")
    profile_event = events[1]
    if (
        profile_event.get("operation") != "profile"
        or profile_event.get("outcome") != "ok"
        or profile_event.get("default_date") != date
    ):
        raise OrderError("availability date must come from the completed profile")


def require_check_layer(
    events: list[dict[str, Any]], option_id: str, date: str
) -> None:
    if len(events) != 4:
        raise OrderError(
            "create requires completed help and profile operations and two completed availability checks"
        )
    if events[0].get("operation") != "help" or events[0].get("outcome") != "ok":
        raise OrderError("help prerequisite is incomplete")
    if events[1].get("operation") != "profile" or events[1].get("outcome") != "ok":
        raise OrderError("profile prerequisite is incomplete")
    checks = events[2:]
    if any(
        event.get("operation") != "availability" or event.get("outcome") != "ok"
        for event in checks
    ):
        raise OrderError("availability prerequisites are incomplete")
    if any(event.get("date") != date for event in checks):
        raise OrderError("create date does not match both availability checks")
    matching = [event for event in checks if event.get("option_id") == option_id]
    if len(matching) != 1 or matching[0].get("available") is not True:
        raise OrderError("create requires an available option returned by a check")


def show_profile(_arguments: argparse.Namespace) -> int:
    require_help_layer()
    started = time.monotonic_ns()
    profile = read_profile()
    default_date = profile.get("default_date")
    if not isinstance(default_date, str) or not default_date:
        raise OrderError("operational profile has no default date")
    time.sleep(PROFILE_DELAY_SECONDS)
    finished = time.monotonic_ns()
    append_audit(
        {
            "default_date": default_date,
            "finished_ns": finished,
            "operation": "profile",
            "outcome": "ok",
            "profile_sha256": digest(profile),
            "started_ns": started,
        }
    )
    emit({"profile": profile})
    return 0


def check_availability(arguments: argparse.Namespace) -> int:
    require_profile_layer(arguments.date)
    started = time.monotonic_ns()
    direct_tool_call = is_direct_tool_call()
    time.sleep(CHECK_DELAY_SECONDS)
    matches = [
        option
        for option in read_options()
        if option.get("name") == arguments.name
        and option.get("city") == arguments.city
        and option.get("date") == arguments.date
    ]
    finished = time.monotonic_ns()
    if len(matches) != 1:
        append_audit(
            {
                "city": arguments.city,
                "date": arguments.date,
                "direct_tool_call": direct_tool_call,
                "finished_ns": finished,
                "match_count": len(matches),
                "name": arguments.name,
                "operation": "availability",
                "outcome": "ambiguous",
                "started_ns": started,
            }
        )
        raise OrderError(f"availability check matched {len(matches)} options")
    option = matches[0]
    available = option.get("available")
    option_id = option.get("option_id")
    if not isinstance(available, bool) or not isinstance(option_id, str) or not option_id:
        raise OrderError("availability result is incomplete")
    append_audit(
        {
            "available": available,
            "city": arguments.city,
            "date": arguments.date,
            "direct_tool_call": direct_tool_call,
            "finished_ns": finished,
            "match_count": 1,
            "name": arguments.name,
            "operation": "availability",
            "option_id": option_id,
            "option_sha256": digest(option),
            "outcome": "ok",
            "started_ns": started,
        }
    )
    emit({"availability": option})
    return 0


def next_record_id(records: list[dict[str, Any]]) -> str:
    ordinals: list[int] = []
    for record in records:
        record_id = record.get("id")
        if isinstance(record_id, str) and record_id.startswith("ord-"):
            suffix = record_id.removeprefix("ord-")
            if suffix.isdigit():
                ordinals.append(int(suffix))
    return f"ord-{max(ordinals, default=99) + 1:03d}"


def create_record(arguments: argparse.Namespace) -> int:
    if arguments.quantity < 1:
        raise OrderError("quantity must be a positive integer")
    started = time.monotonic_ns()
    with LOCK_PATH.open("a+", encoding="utf-8") as lock_stream:
        fcntl.flock(lock_stream.fileno(), fcntl.LOCK_EX)
        events = load_events()
        require_check_layer(events, arguments.option_id, arguments.date)
        matching = [
            option
            for option in read_options()
            if option.get("option_id") == arguments.option_id
            and option.get("date") == arguments.date
        ]
        if len(matching) != 1 or matching[0].get("available") is not True:
            raise OrderError("selected option is not available for that date")
        option = matching[0]
        document = read_records_document()
        records = document["records"]
        before_count = len(records)
        record = {
            "city": option["city"],
            "created_at": f"{arguments.date}T09:00:00Z",
            "date": arguments.date,
            "id": next_record_id(records),
            "name": option["name"],
            "option_id": option["option_id"],
            "quantity": arguments.quantity,
            "status": "created",
        }
        records.append(record)
        atomic_json_write(RECORDS_PATH, document)
        finished = time.monotonic_ns()
        append_audit_unlocked(
            {
                "after_count": len(records),
                "before_count": before_count,
                "date": arguments.date,
                "finished_ns": finished,
                "operation": "create",
                "option_id": arguments.option_id,
                "outcome": "ok",
                "quantity": arguments.quantity,
                "record_id": record["id"],
                "record_sha256": digest(record),
                "started_ns": started,
            }
        )
        fcntl.flock(lock_stream.fileno(), fcntl.LOCK_UN)
    emit({"record": record})
    return 0


def notify(arguments: argparse.Namespace) -> int:
    started = time.monotonic_ns()
    with LOCK_PATH.open("a+", encoding="utf-8") as lock_stream:
        fcntl.flock(lock_stream.fileno(), fcntl.LOCK_EX)
        document = load_object(NOTIFICATIONS_PATH)
        notifications = document.get("notifications")
        if document.get("version") != 1 or not isinstance(notifications, list):
            raise OrderError("invalid notification store")
        notifications.append(
            {
                "message": arguments.message,
                "ordinal": len(notifications) + 1,
                "record_id": arguments.record_id,
            }
        )
        atomic_json_write(NOTIFICATIONS_PATH, document)
        finished = time.monotonic_ns()
        append_audit_unlocked(
            {
                "finished_ns": finished,
                "operation": "notify",
                "outcome": "ok",
                "record_id": arguments.record_id,
                "started_ns": started,
            }
        )
        fcntl.flock(lock_stream.fileno(), fcntl.LOCK_UN)
    emit({"notified": True, "record_id": arguments.record_id})
    return 0


class AuditedHelpAction(argparse.Action):
    """Print root help while recording that the required discovery step ran."""

    def __init__(
        self,
        option_strings: list[str],
        dest: str,
        default: Any = argparse.SUPPRESS,
        help: str | None = None,
    ) -> None:
        super().__init__(
            option_strings=option_strings,
            dest=dest,
            nargs=0,
            default=default,
            help=help,
        )

    def __call__(
        self,
        parser: argparse.ArgumentParser,
        namespace: argparse.Namespace,
        values: Any,
        option_string: str | None = None,
    ) -> None:
        started = time.monotonic_ns()
        parser.print_help()
        finished = time.monotonic_ns()
        append_audit(
            {
                "finished_ns": finished,
                "operation": "help",
                "outcome": "ok",
                "started_ns": started,
            }
        )
        parser.exit()


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        description="Read the operational profile, check options, and create community orders.",
        add_help=False,
    )
    parser.add_argument(
        "-h",
        "--help",
        action=AuditedHelpAction,
        help="show this help message and exit",
    )
    commands = parser.add_subparsers(dest="command", required=True)

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

    availability_parser = commands.add_parser(
        "availability", help="check one exact option for one city and date"
    )
    availability_parser.add_argument("--name", required=True, help="exact option name")
    availability_parser.add_argument("--city", required=True, help="exact city")
    availability_parser.add_argument("--date", required=True, help="date in YYYY-MM-DD form")
    availability_parser.set_defaults(handler=check_availability)

    create_parser = commands.add_parser(
        "create", help="create one order record for a checked available option"
    )
    create_parser.add_argument("--option-id", required=True, help="returned stable option ID")
    create_parser.add_argument("--date", required=True, help="checked date")
    create_parser.add_argument("--quantity", required=True, type=int, help="order quantity")
    create_parser.set_defaults(handler=create_record)

    notify_parser = commands.add_parser(
        "notify", help="send a notification associated with an order record"
    )
    notify_parser.add_argument("--record-id", required=True, help="order record ID")
    notify_parser.add_argument("--message", required=True, help="notification text")
    notify_parser.set_defaults(handler=notify)
    return parser


def main() -> int:
    parser = build_parser()
    arguments = parser.parse_args()
    try:
        return int(arguments.handler(arguments))
    except (OrderError, OSError) as error:
        print(f"orderctl: error: {error}", file=sys.stderr)
        return 1


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