#!/usr/bin/env python3
"""Executable interface to the sandboxed workforce-planning ledger."""

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 / ".availability"
PROFILE_PATH = STATE_DIR / "profile.json"
OPTIONS_PATH = STATE_DIR / "options.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"
READ_DELAY_SECONDS = 0.55


class PlanningError(RuntimeError):
    """A user-facing workforce-planning error."""


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


def load_object(path: Path) -> dict[str, Any]:
    with path.open(encoding="utf-8") as stream:
        value = json.load(stream)
    if not isinstance(value, dict):
        raise PlanningError(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 load_events() -> list[dict[str, Any]]:
    events: list[dict[str, Any]] = []
    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 PlanningError(f"invalid audit event at line {line_number}")
            events.append(event)
    return events


def record_digest(value: dict[str, Any]) -> str:
    return hashlib.sha256(canonical(value)).hexdigest()


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 saved_profile() -> dict[str, Any]:
    document = load_object(PROFILE_PATH)
    profile = document.get("profile")
    if document.get("version") != 1 or not isinstance(profile, dict):
        raise PlanningError("invalid saved operational profile")
    return profile


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


def require_profile_layer(date: str) -> None:
    with LOCK_PATH.open("a+", encoding="utf-8") as lock_stream:
        fcntl.flock(lock_stream.fileno(), fcntl.LOCK_EX)
        events = load_events()
        if (
            len(events) != 1
            or events[0].get("operation") != "profile"
            or events[0].get("outcome") != "ok"
        ):
            raise PlanningError(
                "availability requires one completed profile read and no other operation"
            )
        if events[0].get("default_date") != date:
            raise PlanningError("date does not match the saved profile result")
        fcntl.flock(lock_stream.fileno(), fcntl.LOCK_UN)


def read_profile(_: 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)
        if load_events():
            raise PlanningError("profile read must be the first workforce operation")
        profile = saved_profile()
        default_date = profile.get("default_date")
        if not isinstance(default_date, str) or not default_date:
            raise PlanningError("saved profile has no default date")
        finished = time.monotonic_ns()
        append_audit_unlocked(
            {
                "default_date": default_date,
                "finished_ns": finished,
                "operation": "profile",
                "outcome": "ok",
                "profile_sha256": record_digest(profile),
                "started_ns": started,
            }
        )
        fcntl.flock(lock_stream.fileno(), fcntl.LOCK_UN)
    emit({"profile": profile})
    return 0


def check_availability(arguments: argparse.Namespace) -> int:
    started = time.monotonic_ns()
    require_profile_layer(arguments.date)
    time.sleep(READ_DELAY_SECONDS)
    matches: list[dict[str, Any]] = []
    for option in option_list():
        if (
            option.get("name") == arguments.name
            and option.get("department") == arguments.department
        ):
            availability = option.get("availability")
            available = (
                availability.get(arguments.date)
                if isinstance(availability, dict)
                else None
            )
            matches.append(
                {
                    "available": available,
                    "date": arguments.date,
                    "department": option.get("department"),
                    "name": option.get("name"),
                    "option_id": option.get("id"),
                }
            )
    matches.sort(key=lambda match: str(match.get("option_id")))
    finished = time.monotonic_ns()
    append_audit(
        {
            "date": arguments.date,
            "department": arguments.department,
            "finished_ns": finished,
            "name": arguments.name,
            "operation": "availability",
            "outcome": "ok",
            "results": matches,
            "started_ns": started,
        }
    )
    emit({"count": len(matches), "matches": matches})
    return 0


def first_available_from_events(events: list[dict[str, Any]]) -> str | None:
    priority = [("Amina Yusuf", "Design"), ("Tomas Becker", "Finance")]
    found: dict[tuple[Any, Any], list[dict[str, Any]]] = {}
    for event in events:
        if event.get("operation") != "availability":
            continue
        results = event.get("results")
        if not isinstance(results, list):
            raise PlanningError("an availability result is invalid")
        found[(event.get("name"), event.get("department"))] = results
    if set(found) != set(priority):
        raise PlanningError("required availability layer is incomplete")
    for scope in priority:
        results = found[scope]
        if len(results) != 1 or not isinstance(results[0].get("available"), bool):
            raise PlanningError("a required availability result is unresolved")
        if results[0]["available"]:
            option_id = results[0].get("option_id")
            if not isinstance(option_id, str) or not option_id:
                raise PlanningError("available option has no stable identifier")
            return option_id
    return None


def create_record(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)
        events = load_events()
        if (
            len(events) != 3
            or events[0].get("operation") != "profile"
            or [event.get("operation") for event in events[1:]]
            != ["availability", "availability"]
            or any(event.get("outcome") != "ok" for event in events)
        ):
            raise PlanningError(
                "create requires one profile read and two completed availability checks"
            )
        resolved_date = events[0].get("default_date")
        if arguments.date != resolved_date:
            raise PlanningError("date does not match the saved profile result")
        expected_option = first_available_from_events(events[1:])
        if expected_option is None:
            raise PlanningError("no checked option is available; create nothing")
        if arguments.option_id != expected_option:
            raise PlanningError("option is not the first available checked result")
        document = load_object(RECORDS_PATH)
        records = document.get("records")
        if document.get("version") != 1 or not isinstance(records, list):
            raise PlanningError("invalid planning record store")
        if not all(isinstance(record, dict) for record in records):
            raise PlanningError("invalid planning record entry")
        next_number = max(
            [
                int(record["id"].split("-")[-1])
                for record in records
                if isinstance(record.get("id"), str)
                and record["id"].startswith("plan-")
                and record["id"].split("-")[-1].isdigit()
            ]
            or [0]
        ) + 1
        record = {
            "created_by": events[0].get("profile_sha256"),
            "date": arguments.date,
            "id": f"plan-{next_number:04d}",
            "option_id": arguments.option_id,
            "quantity": arguments.quantity,
        }
        records.append(record)
        atomic_json_write(RECORDS_PATH, document)
        finished = time.monotonic_ns()
        append_audit_unlocked(
            {
                "finished_ns": finished,
                "operation": "create",
                "outcome": "ok",
                "record": record,
                "record_sha256": record_digest(record),
                "started_ns": started,
            }
        )
        fcntl.flock(lock_stream.fileno(), fcntl.LOCK_UN)
    emit({"created": True, "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 PlanningError("invalid notification store")
        notifications.append(
            {
                "message": arguments.message,
                "option_id": arguments.option_id,
                "ordinal": len(notifications) + 1,
            }
        )
        atomic_json_write(NOTIFICATIONS_PATH, document)
        finished = time.monotonic_ns()
        append_audit_unlocked(
            {
                "finished_ns": finished,
                "operation": "notify",
                "option_id": arguments.option_id,
                "outcome": "ok",
                "started_ns": started,
            }
        )
        fcntl.flock(lock_stream.fileno(), fcntl.LOCK_UN)
    emit({"notified": True, "option_id": arguments.option_id})
    return 0


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="availabilityctl",
        description=(
            "Read the saved profile, check exact workforce availability, and "
            "create planning records."
        ),
    )
    commands = parser.add_subparsers(dest="command", required=True)

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

    availability_parser = commands.add_parser(
        "availability", help="check one exact person, department, and date"
    )
    availability_parser.add_argument("--name", required=True)
    availability_parser.add_argument("--department", required=True)
    availability_parser.add_argument("--date", required=True)
    availability_parser.set_defaults(handler=check_availability)

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

    notify_parser = commands.add_parser(
        "notify", help="send a notification about an option"
    )
    notify_parser.add_argument("--option-id", required=True)
    notify_parser.add_argument("--message", required=True)
    notify_parser.set_defaults(handler=notify)
    return parser


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


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