#!/usr/bin/env python3
"""Command-line client for the sandboxed CRM service."""

from __future__ import annotations

import argparse
import json
import os
from pathlib import Path
import subprocess
import sys
import time
from typing import Any


ROOT = Path(__file__).resolve().parent
SERVICE = ROOT / ".crm" / "service.py"
RESPONSE_DEADLINE_SECONDS = 0.6
READY_FD_ENVIRONMENT_VARIABLE = "CRM_SERVICE_READY_FD"
RELEASE_FD_ENVIRONMENT_VARIABLE = "CRM_SERVICE_RELEASE_FD"


def call_service(request: dict[str, str]) -> dict[str, Any]:
    ready_reader, ready_writer = os.pipe()
    release_reader, release_writer = os.pipe()
    environment = os.environ.copy()
    environment[READY_FD_ENVIRONMENT_VARIABLE] = str(ready_writer)
    environment[RELEASE_FD_ENVIRONMENT_VARIABLE] = str(release_reader)
    try:
        process = subprocess.Popen(
            [sys.executable, "-B", str(SERVICE)],
            cwd=ROOT,
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
            env=environment,
            pass_fds=(ready_writer, release_reader),
        )
    except Exception:
        os.close(ready_reader)
        os.close(ready_writer)
        os.close(release_reader)
        os.close(release_writer)
        raise
    os.close(ready_writer)
    os.close(release_reader)

    try:
        if process.stdin is None:
            raise RuntimeError("CRM service input is unavailable")
        process.stdin.write(
            json.dumps(request, ensure_ascii=False, separators=(",", ":")) + "\n"
        )
        process.stdin.close()
        process.stdin = None

        with os.fdopen(ready_reader, "rb") as ready_pipe:
            ready = ready_pipe.read(1)
        if ready != b"1":
            os.close(release_writer)
            stdout, stderr = process.communicate()
            if process.returncode != 0:
                raise RuntimeError(stderr.strip() or "CRM service process failed")
            raise RuntimeError("CRM service returned no processing confirmation")

        response_deadline = time.monotonic() + RESPONSE_DEADLINE_SECONDS
        with os.fdopen(release_writer, "wb") as release_pipe:
            release_pipe.write(b"1")
        remaining = response_deadline - time.monotonic()
        if remaining <= 0:
            raise subprocess.TimeoutExpired(
                process.args, RESPONSE_DEADLINE_SECONDS
            )
        stdout, stderr = process.communicate(
            timeout=remaining,
        )
    except subprocess.TimeoutExpired:
        process.kill()
        process.communicate()
        raise TimeoutError(
            "response deadline exceeded after the request reached the service; "
            "commit outcome is unknown"
        ) from None

    if process.returncode != 0:
        raise RuntimeError(stderr.strip() or "CRM service process failed")
    if not stdout:
        raise RuntimeError("CRM service returned no response")
    response: Any = json.loads(stdout)
    if not isinstance(response, dict):
        raise RuntimeError("CRM service returned an invalid response")
    if response.get("ok") is not True:
        raise RuntimeError(str(response.get("error", "CRM operation failed")))
    record = response.get("record")
    if not isinstance(record, dict):
        raise RuntimeError("CRM service returned no account record")
    return record


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="crmctl",
        description="Retrieve or cancel accounts through the local CRM service.",
    )
    commands = parser.add_subparsers(dest="operation", required=True)

    get_command = commands.add_parser(
        "get", help="retrieve one complete account by stable ID"
    )
    get_command.add_argument("--id", required=True, dest="account_id")

    cancel_command = commands.add_parser(
        "cancel", help="cancel one account by stable ID"
    )
    cancel_command.add_argument("--id", required=True, dest="account_id")
    cancel_command.add_argument("--reason", required=True)

    search_command = commands.add_parser(
        "search", help="search account names (not needed for exact-ID workflows)"
    )
    search_command.add_argument("--query", required=True)
    return parser


def main() -> int:
    arguments = build_parser().parse_args()
    request = {"operation": arguments.operation}
    if arguments.operation == "search":
        request["query"] = arguments.query
    else:
        request["account_id"] = arguments.account_id
    if arguments.operation == "cancel":
        request["reason"] = arguments.reason

    try:
        record = call_service(request)
    except TimeoutError as error:
        print(f"crmctl: timeout: {error}", file=sys.stderr)
        return 124
    except (OSError, RuntimeError, ValueError, json.JSONDecodeError) as error:
        print(f"crmctl: error: {error}", file=sys.stderr)
        return 1
    print(json.dumps(record, ensure_ascii=False, indent=2, sort_keys=True))
    return 0


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