#!/usr/bin/env python3
"""Direct Whitehat owner interface for WP-0025 broker readiness."""

from __future__ import annotations

import argparse
import json
import os
import re
import subprocess
import sys
from datetime import UTC, datetime
from pathlib import Path
from typing import Any, Callable


SCRIPT_DIR = Path(__file__).resolve().parent
if str(SCRIPT_DIR) not in sys.path:
    sys.path.insert(0, str(SCRIPT_DIR))

from custody_contract import (  # noqa: E402
    BROKER_INTERFACE,
    BROKER_OWNER,
    PROJECTION_INTERFACE,
    WORKPLAN_ID,
    ContractError,
    broker_subject,
    canonical_json,
    contract_digest,
    current_broker_receipt,
    file_digest,
    http_json,
    interface_artifacts,
    load_json,
    validate_broker_receipt,
    validate_projection_contract,
)


DEFAULT_ADAPTER = Path("src/whitehat_security/platform_custody.py")
DEFAULT_TEST = Path("tests/test_platform_custody_adapter.py")


class ReadinessError(RuntimeError):
    pass


def validate_reviewer(value: str) -> str:
    if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._:@/+\-]{1,127}", value):
        raise ReadinessError("reviewer must be a stable 2-128 character identifier")
    return value


def validate_note(value: str) -> str:
    note = value.strip()
    if not note or len(note) > 2000:
        raise ReadinessError("request-changes requires a note of 1-2000 characters")
    forbidden = ("BEGIN PRIVATE KEY", "BEGIN OPENSSH PRIVATE KEY", "AGE-SECRET-KEY-1", "hvs.")
    if any(marker in note for marker in forbidden):
        raise ReadinessError("note appears to contain credential material")
    return note


def safe_child(root: Path, relative: Path) -> Path:
    if relative.is_absolute():
        raise ReadinessError("adapter and test paths must be repository-relative")
    target = (root / relative).resolve()
    try:
        target.relative_to(root.resolve())
    except ValueError as exc:
        raise ReadinessError("consumer artifact escapes repository root") from exc
    return target


def verify_adapter(
    consumer_root: Path,
    *,
    adapter_path: Path = DEFAULT_ADAPTER,
    test_path: Path = DEFAULT_TEST,
    runner: Callable[..., subprocess.CompletedProcess[str]] = subprocess.run,
) -> dict[str, Any]:
    root = consumer_root.resolve()
    adapter = safe_child(root, adapter_path)
    test = safe_child(root, test_path)
    if not adapter.is_file() or not test.is_file():
        return {
            "passed": False,
            "adapter_present": adapter.is_file(),
            "test_present": test.is_file(),
            "secret_values_observed": False,
        }
    revision_result = runner(
        ["git", "rev-parse", "HEAD"],
        cwd=root,
        text=True,
        capture_output=True,
        check=False,
    )
    revision = revision_result.stdout.strip() if revision_result.returncode == 0 else ""
    tests = runner(
        [sys.executable, "-m", "pytest", "-q", str(test_path)],
        cwd=root,
        text=True,
        capture_output=True,
        check=False,
    )
    passed = bool(re.fullmatch(r"[0-9a-f]{40}", revision)) and tests.returncode == 0
    return {
        "passed": passed,
        "adapter_present": True,
        "test_present": True,
        "adapter": {
            "repo": "whitehat-security",
            "revision": revision,
            "path": str(adapter_path),
            "sha256": file_digest(adapter),
            "tests_passed": tests.returncode == 0,
        },
        "test_exit_code": tests.returncode,
        "secret_values_observed": False,
    }


def receipt_base(
    contract: dict[str, Any],
    reviewer: str,
    decision: str,
    *,
    now: datetime | None = None,
) -> dict[str, Any]:
    created_at = now or datetime.now(UTC)
    return {
        "interface": BROKER_INTERFACE,
        "version": 1,
        "workplan_id": WORKPLAN_ID,
        "owner": BROKER_OWNER,
        "reviewer": validate_reviewer(reviewer),
        "decision": decision,
        "created_at": created_at.replace(microsecond=0).isoformat().replace("+00:00", "Z"),
        "engagement_id": contract["engagement_id"],
        "target_id": contract["target"]["id"],
        "projection_contract_digest": contract_digest(contract),
        "projection_receipt_interface": PROJECTION_INTERFACE,
        "interface_artifacts": interface_artifacts(),
        "required_roles": ["attacker", "owner"],
        "mount_paths": sorted(item["mount_path"] for item in contract["identities"]),
        "secret_values_observed": False,
    }


def build_approval(
    contract: dict[str, Any],
    reviewer: str,
    verification: dict[str, Any],
    *,
    now: datetime | None = None,
) -> dict[str, Any]:
    if verification.get("passed") is not True or verification.get("secret_values_observed") is not False:
        raise ReadinessError("consumer adapter verification did not pass")
    receipt = {
        **receipt_base(contract, reviewer, "approve", now=now),
        "adapter": verification["adapter"],
        "cleanup_request_supported": True,
    }
    validate_broker_receipt(receipt, contract, now=now)
    return receipt


def build_change_request(
    contract: dict[str, Any],
    reviewer: str,
    note: str,
    *,
    now: datetime | None = None,
) -> dict[str, Any]:
    return {
        **receipt_base(contract, reviewer, "request-changes", now=now),
        "note": validate_note(note),
    }


def post_receipt(receipt: dict[str, Any], api_base: str) -> dict[str, Any]:
    response = http_json(
        "POST",
        f"{api_base.rstrip('/')}/messages/",
        {
            "from_agent": BROKER_OWNER,
            "to_agent": "railiance-platform",
            "subject": broker_subject(receipt),
            "body": canonical_json(receipt),
        },
    )
    if not isinstance(response, dict) or not response.get("id"):
        raise ReadinessError("State Hub returned an invalid message receipt")
    return response


def write_receipt(path: Path, receipt: dict[str, Any]) -> None:
    """Persist only the canonical broker receipt, never the CLI wrapper."""
    path.parent.mkdir(parents=True, exist_ok=True)
    descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
    try:
        with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
            handle.write(json.dumps(receipt, indent=2, sort_keys=True) + "\n")
    finally:
        os.chmod(path, 0o600)


def show(contract: dict[str, Any]) -> dict[str, Any]:
    return {
        "interface": BROKER_INTERFACE,
        "version": 1,
        "owner": BROKER_OWNER,
        "engagement_id": contract["engagement_id"],
        "target_id": contract["target"]["id"],
        "projection_contract_digest": contract_digest(contract),
        "projection_receipt_interface": PROJECTION_INTERFACE,
        "required_roles": ["attacker", "owner"],
        "mount_paths": sorted(item["mount_path"] for item in contract["identities"]),
        "adapter_path": str(DEFAULT_ADAPTER),
        "test_path": str(DEFAULT_TEST),
        "approve_command": (
            "python3 scripts/wp0025-broker-readiness.py approve --contract <contract.json> "
            "--consumer-root <whitehat-security> --reviewer <stable-id>"
        ),
        "live_mutation_authorized": False,
        "secret_values_observed": False,
    }


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("command", choices=["show", "verify", "approve", "request-changes", "status"])
    parser.add_argument("--contract", type=Path, required=True)
    parser.add_argument("--consumer-root", type=Path)
    parser.add_argument("--reviewer")
    parser.add_argument("--note")
    parser.add_argument("--receipt-out", type=Path)
    parser.add_argument("--state-hub", default="http://127.0.0.1:8000")
    args = parser.parse_args()
    try:
        contract = validate_projection_contract(load_json(args.contract))
        receipt_out = None
        if args.command == "show":
            result = show(contract)
        elif args.command == "status":
            try:
                receipt = current_broker_receipt(contract, args.state_hub)
            except ContractError as exc:
                result = {
                    "ready": False,
                    "engagement_id": contract["engagement_id"],
                    "reason": str(exc),
                    "secret_values_observed": False,
                }
            else:
                receipt_out = receipt
                result = {
                    "ready": True,
                    "engagement_id": contract["engagement_id"],
                    "receipt": receipt,
                    "secret_values_observed": False,
                }
        elif args.command == "verify":
            if not args.consumer_root:
                raise ReadinessError("verify requires --consumer-root")
            result = verify_adapter(args.consumer_root)
        elif args.command == "approve":
            if not args.consumer_root or not args.reviewer:
                raise ReadinessError("approve requires --consumer-root and --reviewer")
            verification = verify_adapter(args.consumer_root)
            receipt = build_approval(contract, args.reviewer, verification)
            posted = post_receipt(receipt, args.state_hub)
            receipt_out = receipt
            result = {"submitted": True, "message_id": posted["id"], "receipt": receipt}
        else:
            if not args.reviewer or not args.note:
                raise ReadinessError("request-changes requires --reviewer and --note")
            receipt = build_change_request(contract, args.reviewer, args.note)
            posted = post_receipt(receipt, args.state_hub)
            result = {"submitted": True, "message_id": posted["id"], "receipt": receipt}
        if args.receipt_out:
            if receipt_out is None:
                raise ReadinessError("--receipt-out requires approve or a ready status")
            write_receipt(args.receipt_out, receipt_out)
        print(json.dumps(result, indent=2, sort_keys=True))
        return 0
    except (ContractError, OSError, ReadinessError) as exc:
        print(f"broker readiness failed: {exc}", file=sys.stderr)
        return 1


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