#!/usr/bin/env python3
"""Executable Vibium compatibility and CARBON-enhanced test APIs."""

from __future__ import annotations

import argparse
import json
import os
import re
import shutil
import subprocess
import sys
import time
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

import carbon_browser


RUNTIME_ROOT = Path(__file__).resolve().parents[1]
FRAMEWORK_SCRIPT = RUNTIME_ROOT / "scripts" / "carbon_framework.py"
PROTOCOL = "carbon-browser/v1"
CONSEQUENTIAL_CLASSES = {
    "destructive", "paid", "public", "privileged", "privacy-sensitive"
}
ACTION_CLASSES = {
    "read-only", "reversible", "state-changing", *CONSEQUENTIAL_CLASSES
}
VIBIUM_COMMANDS = {
    "start", "stop", "go", "back", "forward", "reload", "url", "title",
    "text", "html", "find", "map", "diff", "click", "dblclick", "fill",
    "type", "press", "check", "uncheck", "select", "hover", "focus", "drag",
    "scroll", "upload", "highlight", "value", "attr", "is", "count", "wait",
    "sleep", "screenshot", "pdf", "eval", "pages", "page", "frames", "frame",
    "viewport", "window", "media", "geolocation", "content", "cookies", "storage",
    "record", "download", "dialog", "mouse", "keys", "a11y-tree", "mcp", "pipe",
    "serve", "paths", "is-installed", "install", "version", "diagnostics",
}
READ_ONLY_COMMANDS = {
    "url", "title", "text", "html", "find", "map", "diff", "value", "attr",
    "is", "count", "screenshot", "pdf", "pages", "frames", "frame", "a11y-tree",
    "paths", "is-installed", "version",
}
SENSITIVE_FLAGS = {
    "--api-key", "--apikey", "--authorization", "--cookie", "--password",
    "--secret", "--token", "--username",
}
SENSITIVE_VALUE_COMMANDS = {"content", "fill", "type"}
CARBON_OPERATION = {
    "start": "session.start", "stop": "session.stop", "go": "page.open",
    "pages": "page.list", "page": "vibium.command", "find": "discover.find",
    "map": "discover.map", "diff": "discover.diff", "click": "action.click",
    "dblclick": "vibium.command", "fill": "action.fill", "type": "action.fill",
    "press": "action.press", "keys": "action.press", "check": "action.check",
    "uncheck": "action.check", "select": "action.select", "hover": "action.hover",
    "drag": "action.drag-drop", "scroll": "action.scroll", "upload": "action.upload",
    "wait": "wait.element", "screenshot": "capture.screenshot", "html": "capture.dom",
    "a11y-tree": "capture.accessibility", "storage": "capture.storage",
    "record": "vibium.command",
}


def now() -> str:
    return datetime.now(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z")


def json_print(value: Any) -> None:
    print(json.dumps(value, indent=2))


def root_path(raw: str) -> Path:
    root = Path(raw).expanduser().resolve()
    if not root.is_dir():
        raise ValueError("root must be an existing project directory")
    return root


def safe_project_file(root: Path, raw: str) -> Path:
    target = Path(raw).expanduser()
    if not target.is_absolute():
        target = root / target
    target = target.resolve()
    try:
        target.relative_to(root)
    except ValueError as exc:
        raise ValueError("test files must stay inside the selected project root") from exc
    if not target.is_file():
        raise ValueError(f"test file does not exist: {target}")
    return target


def resolve_vibium(binary: str | None, allow_npx: bool) -> tuple[list[str], str]:
    candidate = binary or os.environ.get("VIBIUM_BIN_PATH") or shutil.which("vibium")
    if candidate:
        resolved = shutil.which(candidate) if not Path(candidate).expanduser().is_absolute() else str(Path(candidate).expanduser())
        if not resolved or not Path(resolved).is_file() or not os.access(resolved, os.X_OK):
            raise ValueError(f"Vibium binary is not executable: {candidate}")
        return [str(Path(resolved).resolve())], "installed-binary"
    if allow_npx:
        npx = shutil.which("npx")
        if not npx:
            raise ValueError("npx is unavailable; install Vibium or provide --binary")
        return [npx, "-y", "vibium"], "npx-opt-in"
    raise ValueError(
        "Vibium is not installed. Install it with `npm install -g vibium`, set "
        "VIBIUM_BIN_PATH, pass --binary, or explicitly opt into npx with --allow-npx."
    )


def run_process(command: list[str], cwd: Path, timeout: int) -> subprocess.CompletedProcess[str]:
    try:
        return subprocess.run(
            command, cwd=cwd, text=True, capture_output=True, check=False,
            timeout=max(1, min(timeout, 3600)), env=os.environ.copy(),
        )
    except subprocess.TimeoutExpired as exc:
        raise ValueError(f"command exceeded the {timeout}-second timeout") from exc


def run_native_command(
    command: list[str], cwd: Path, timeout: int, native: list[str], dialog_wait_ms: int
) -> tuple[subprocess.CompletedProcess[str], int]:
    """Retry only dialog accept/dismiss while an asynchronous dialog is materializing."""
    dialog_action = len(native) >= 2 and native[0] == "dialog" and native[1] in {"accept", "dismiss"}
    wait_ms = max(0, min(int(dialog_wait_ms), 30_000)) if dialog_action else 0
    deadline = time.monotonic() + wait_ms / 1000
    attempts = 0
    while True:
        attempts += 1
        result = run_process(command, cwd, timeout)
        output = f"{result.stdout}\n{result.stderr}".lower()
        missing_dialog = result.returncode != 0 and any(
            phrase in output for phrase in ("no such alert", "no alert is open", "no dialog is open")
        )
        if not missing_dialog or time.monotonic() >= deadline:
            return result, attempts
        time.sleep(min(0.1, max(0.0, deadline - time.monotonic())))


def version_for(base: list[str], cwd: Path) -> str:
    result = run_process([*base, "--version"], cwd, 60)
    text = (result.stdout or result.stderr).strip()
    if result.returncode:
        return "unresolved"
    return text[:200] or "unresolved"


def vibium_manifest(version: str = "external") -> dict[str, Any]:
    return {
        "protocol": PROTOCOL,
        "id": "vibium.official-cli",
        "name": "Official Vibium CLI adapter",
        "version": version,
        "family": "vibium-webdriver-bidi",
        "transport": "cli",
        "operations": [
            "session.start", "session.stop", "page.open", "page.list", "page.switch",
            "page.close", "discover.map", "discover.diff", "discover.find", "action.click",
            "action.fill", "action.select", "action.check", "action.press", "action.hover",
            "action.scroll", "action.upload", "action.drag-drop", "wait.url", "wait.text",
            "wait.state", "wait.element", "wait.predicate", "capture.screenshot", "capture.dom",
            "capture.accessibility", "capture.storage", "record.start", "record.stop",
            "vibium.command", "vibium.test", "vibium.suite",
        ],
        "evidenceChannels": [
            "screenshot", "dom", "accessibility", "storage", "recording", "other"
        ],
        "sessionFeatures": [
            "official-vibium-binary", "webdriver-bidi", "semantic-element-refs",
            "native-cli-compatibility", "json-output", "existing-test-runner",
        ],
        "safety": {
            "actionClassification": True,
            "approvalGates": True,
            "redaction": True,
        },
    }


def create_artifact_dir(root: Path, prefix: str) -> Path:
    stamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
    directory = root / ".carbon" / "vibium" / "runs" / f"{prefix}-{stamp}-{uuid.uuid4().hex[:8]}"
    directory.mkdir(parents=True, exist_ok=False)
    return directory


def sensitive_command_values(command: list[str], native_args: list[str]) -> set[str]:
    values: set[str] = set()
    for index, item in enumerate(command):
        lowered = item.lower()
        if lowered in SENSITIVE_FLAGS and index + 1 < len(command):
            values.add(command[index + 1])
        if "=" in item and item.split("=", 1)[0].lower() in SENSITIVE_FLAGS:
            values.add(item.split("=", 1)[1])
    if native_args and native_args[0] in SENSITIVE_VALUE_COMMANDS and len(native_args) > 1:
        values.add(native_args[-1])
    if len(native_args) >= 4 and native_args[:2] == ["cookies", "set"]:
        values.add(native_args[-1])
    return {value for value in values if value}


def redact_text_values(value: str, sensitive_values: set[str]) -> str:
    result = value
    for secret in sorted(sensitive_values, key=len, reverse=True):
        result = result.replace(secret, "[REDACTED]")
    return carbon_browser.safe_value(result)


def redact_structure(value: Any, sensitive_values: set[str]) -> Any:
    if isinstance(value, dict):
        return {key: redact_structure(item, sensitive_values) for key, item in value.items()}
    if isinstance(value, list):
        return [redact_structure(item, sensitive_values) for item in value]
    if isinstance(value, str):
        return redact_text_values(value, sensitive_values)
    return value


def redacted_command(command: list[str], native_args: list[str]) -> list[str]:
    sensitive_values = sensitive_command_values(command, native_args)
    return [redact_text_values(item, sensitive_values) for item in command]


def is_image_file(path: Path) -> bool:
    try:
        if path.stat().st_size > 50_000_000:
            return False
        with path.open("rb") as source:
            header = source.read(16)
    except OSError:
        return False
    return (
        header.startswith(b"\x89PNG\r\n\x1a\n")
        or header.startswith(b"\xff\xd8\xff")
        or header.startswith((b"GIF87a", b"GIF89a"))
        or (header.startswith(b"RIFF") and header[8:12] == b"WEBP")
    )


def write_artifacts(
    directory: Path,
    command: list[str],
    result: subprocess.CompletedProcess[str],
    native_args: list[str] | None = None,
) -> list[dict[str, str]]:
    native_args = native_args or []
    native_command = native_args[0] if native_args else ""
    sensitive_values = sensitive_command_values(command, native_args)
    safe_command = redacted_command(command, native_args)
    (directory / "command.json").write_text(json.dumps(safe_command, indent=2) + "\n", encoding="utf-8")
    (directory / "stdout.txt").write_text(redact_text_values(result.stdout or "", sensitive_values), encoding="utf-8")
    (directory / "stderr.txt").write_text(redact_text_values(result.stderr or "", sensitive_values), encoding="utf-8")
    stdout_handle = str(directory / "stdout.txt")
    evidence = [{"kind": "other", "handle": stdout_handle}]
    channel = {
        "html": "dom", "a11y-tree": "accessibility", "storage": "storage",
    }.get(native_command)
    if channel:
        evidence.append({"kind": channel, "handle": stdout_handle})
    if native_command == "screenshot":
        match = re.search(r"Screenshot saved to\s+([^\"\r\n]+)", result.stdout or "")
        if match:
            source = Path(match.group(1).strip()).expanduser()
            if source.is_file() and is_image_file(source):
                copied = directory / source.name
                shutil.copy2(source, copied)
                evidence.append({"kind": "screenshot", "handle": str(copied)})
    if native_command == "record" and "saved" in (result.stdout or "").lower():
        evidence.append({"kind": "recording", "handle": stdout_handle})
    return evidence


def initialize_carbon_session(root: Path, target: str, manifest: dict[str, Any], session_dir: str | None) -> tuple[Path, dict[str, Any]]:
    if session_dir:
        return carbon_browser.ensure_session_dir(session_dir)
    args = argparse.Namespace(
        root=str(root), target=target, require_operation=[], require_evidence=[]
    )
    initialized = carbon_browser.initialize_session(args, manifest)
    return carbon_browser.ensure_session_dir(initialized["sessionDir"])


def record_execution(
    session: tuple[Path, dict[str, Any]], operation: str, status: str, actual: str,
    started: str, finished: str, evidence: list[dict[str, str]], unavailable: list[str],
) -> dict[str, Any]:
    directory, state = session
    envelope = {
        "protocol": PROTOCOL,
        "requestId": f"VIB-{uuid.uuid4().hex[:12].upper()}",
        "sessionId": state["sessionId"],
        "adapter": {"id": "vibium.official-cli", "version": state["adapter"].get("version", "external")},
        "operation": operation,
        "status": status,
        "actual": carbon_browser.safe_value(actual)[:20_000],
        "startedAt": started,
        "finishedAt": finished,
        "before": {},
        "after": {},
        "evidence": evidence,
        "unavailableEvidence": unavailable,
    }
    args = argparse.Namespace(session_dir=str(directory))
    return carbon_browser.record_result(args, envelope)


def normalize_native_args(raw: list[str]) -> list[str]:
    values = list(raw)
    if values and values[0] == "--":
        values = values[1:]
    if not values:
        raise ValueError("provide a Vibium command after --")
    command = values[0]
    if command.startswith("-") or command not in VIBIUM_COMMANDS:
        raise ValueError(f"unsupported Vibium CLI command: {command}")
    return values


def verify_authority(action_class: str, approved: bool) -> None:
    if action_class not in ACTION_CLASSES:
        raise ValueError("action-class is not recognized")
    if action_class in CONSEQUENTIAL_CLASSES and not approved:
        raise ValueError(
            f"{action_class} execution requires --approve-consequential after scoped human approval"
        )


def command_doctor(args: argparse.Namespace) -> dict[str, Any]:
    root = root_path(args.root)
    try:
        base, source = resolve_vibium(args.binary, args.allow_npx)
        version = version_for(base, root)
        return {
            "available": version != "unresolved", "source": source, "command": base,
            "version": version, "manifest": vibium_manifest(version),
            "next": "Use `carbon-runtime vibium exec --root <project> -- <vibium command>`."
        }
    except ValueError as exc:
        return {
            "available": False, "error": str(exc), "manifest": vibium_manifest(),
            "install": "npm install -g vibium",
        }


def command_manifest(args: argparse.Namespace) -> dict[str, Any]:
    if args.probe:
        root = root_path(args.root)
        base, _ = resolve_vibium(args.binary, args.allow_npx)
        return vibium_manifest(version_for(base, root))
    return vibium_manifest()


def command_exec(args: argparse.Namespace) -> dict[str, Any]:
    root = root_path(args.root)
    native = normalize_native_args(args.vibium_command)
    inferred = "read-only" if native[0] in READ_ONLY_COMMANDS else "reversible"
    action_class = args.action_class or inferred
    verify_authority(action_class, args.approve_consequential)
    base, source = resolve_vibium(args.binary, args.allow_npx)
    version = version_for(base, root)
    manifest = vibium_manifest(version)
    session = initialize_carbon_session(root, args.target, manifest, args.session_dir)
    artifact_dir = create_artifact_dir(root, "command")
    command = [*base, "--json"]
    if args.headless:
        command.append("--headless")
    command.extend(native)
    started = now()
    result, dialog_attempts = run_native_command(
        command, root, args.timeout, native, args.dialog_wait_ms
    )
    finished = now()
    evidence = write_artifacts(artifact_dir, command, result, native)
    sensitive_values = sensitive_command_values(command, native)
    output = redact_text_values((result.stdout or result.stderr).strip(), sensitive_values)
    parsed: Any = None
    try:
        parsed = json.loads(result.stdout)
    except (json.JSONDecodeError, TypeError):
        parsed = None
    native_ok = result.returncode == 0 and (not isinstance(parsed, dict) or parsed.get("ok", True) is not False)
    operation = CARBON_OPERATION.get(native[0], "vibium.command")
    unavailable = [
        channel for channel in carbon_browser.contract()["evidenceChannels"]
        if channel not in {item["kind"] for item in evidence}
    ]
    recorded = record_execution(
        session, operation, "succeeded" if native_ok else "failed", output or f"exit {result.returncode}",
        started, finished, evidence, unavailable,
    )
    return {
        "compatibility": "Vibium CLI",
        "adapter": {"source": source, "version": version},
        "command": redacted_command(native, native),
        "actionClass": action_class,
        "exitCode": result.returncode,
        "ok": native_ok,
        "nativeResult": redact_structure(parsed if parsed is not None else output, sensitive_values),
        "dialogWait": {
            "attempts": dialog_attempts,
            "waited": dialog_attempts > 1,
            "maxWaitMs": args.dialog_wait_ms if native[0] == "dialog" else 0,
        },
        "artifactDir": str(artifact_dir),
        "carbonSession": recorded["summary"],
    }


def command_run_test(args: argparse.Namespace) -> dict[str, Any]:
    root = root_path(args.root)
    test_file = safe_project_file(root, args.file) if args.file else None
    runner = list(args.runner)
    if runner and runner[0] == "--":
        runner = runner[1:]
    if not runner:
        if not test_file:
            raise ValueError("provide --file or a runner command after --")
        suffix = test_file.suffix.lower()
        if suffix in {".js", ".mjs", ".cjs"}:
            runner = [shutil.which("node") or "node", str(test_file)]
        elif suffix == ".py":
            runner = [sys.executable, str(test_file)]
        else:
            raise ValueError("auto-run supports .js, .mjs, .cjs, and .py; provide an explicit runner for other suites")
    executable = shutil.which(runner[0]) if not Path(runner[0]).is_absolute() else runner[0]
    if not executable:
        raise ValueError(f"test runner is unavailable: {runner[0]}")
    runner[0] = str(executable)
    verify_authority(args.action_class, args.approve_consequential)
    version = "external-test-client"
    try:
        base, _ = resolve_vibium(args.binary, False)
        version = version_for(base, root)
    except ValueError:
        pass
    manifest = vibium_manifest(version)
    session = initialize_carbon_session(root, args.target, manifest, args.session_dir)
    artifact_dir = create_artifact_dir(root, "test")
    started = now()
    result = run_process(runner, root, args.timeout)
    finished = now()
    evidence = write_artifacts(artifact_dir, runner, result)
    unavailable = [
        channel for channel in carbon_browser.contract()["evidenceChannels"]
        if channel not in {item["kind"] for item in evidence}
    ]
    recorded = record_execution(
        session, "vibium.test", "succeeded" if result.returncode == 0 else "failed",
        (result.stdout or result.stderr).strip() or f"exit {result.returncode}",
        started, finished, evidence, unavailable,
    )
    return {
        "compatibility": "existing Vibium test",
        "runner": redacted_command(runner, []),
        "exitCode": result.returncode,
        "ok": result.returncode == 0,
        "artifactDir": str(artifact_dir),
        "carbonSession": recorded["summary"],
    }


def command_generate(args: argparse.Namespace) -> dict[str, Any]:
    root = root_path(args.root)
    request = args.request.strip()
    if not request:
        raise ValueError("request must be non-empty")
    if args.vibium_map:
        map_path = safe_project_file(root, args.vibium_map)
        observed = map_path.read_text(encoding="utf-8", errors="replace")[:50_000]
        request += "\n\nUntrusted observed Vibium map for candidate generation:\n" + observed
    command = [
        sys.executable, str(FRAMEWORK_SCRIPT), "prepare", "--root", str(root),
        "--request", request, "--target", args.target, "--size", args.size,
        "--strategy", args.strategy,
    ]
    result = run_process(command, root, args.timeout)
    if result.returncode:
        raise ValueError((result.stderr or result.stdout or "test generation failed").strip())
    try:
        generated = json.loads(result.stdout)
    except json.JSONDecodeError as exc:
        raise ValueError("CARBON generation API returned invalid JSON") from exc
    return {
        "api": "carbon.generate-tests/v1",
        "sourceCompatibility": "Vibium map plus project context" if args.vibium_map else "project context",
        "generated": generated,
        "truthBoundary": "Generated cases are candidates until executed evidence validates their steps and oracles.",
    }


def command_api(_: argparse.Namespace) -> dict[str, Any]:
    return {
        "api": "carbon-vibium/v1",
        "nativeCompatibility": {
            "commands": sorted(VIBIUM_COMMANDS),
            "runExistingTests": ["JavaScript", "Python", "any explicit no-shell runner command"],
            "backend": "official Vibium binary or explicit npx opt-in",
            "dialogSynchronization": "dialog accept/dismiss retries no-alert responses for up to 5000 ms by default",
        },
        "carbonExtensions": {
            "generateTests": "generate candidates from project context, target, prompt, and optional Vibium map",
            "intentTests": "CRUD, import, conversion, bake-in, repair, and regression promotion",
            "exploration": "breadth, depth, persona, accessibility, API, and AI-feature testing",
            "evidence": "durable CARBON Browser ledgers plus report-ready artifact handles and explicit gaps",
            "analysis": "quality confidence, remaining risk, missing coverage, and decision-changing evidence",
        },
        "commands": {
            "doctor": "detect the official Vibium binary without installing anything",
            "manifest": "return the executable adapter manifest",
            "exec": "run a native Vibium CLI command and record it in CARBON",
            "run-test": "run an existing Vibium test or project suite and record its result",
            "generate": "generate CARBON test candidates above the Vibium browser layer",
        },
    }


def parser() -> argparse.ArgumentParser:
    root = argparse.ArgumentParser(description=__doc__)
    commands = root.add_subparsers(dest="command", required=True)
    doctor = commands.add_parser("doctor")
    doctor.add_argument("--root", default=".")
    doctor.add_argument("--binary")
    doctor.add_argument("--allow-npx", action="store_true")
    doctor.set_defaults(func=command_doctor)

    manifest = commands.add_parser("manifest")
    manifest.add_argument("--root", default=".")
    manifest.add_argument("--binary")
    manifest.add_argument("--allow-npx", action="store_true")
    manifest.add_argument("--probe", action="store_true")
    manifest.set_defaults(func=command_manifest)

    execute = commands.add_parser("exec")
    execute.add_argument("--root", required=True)
    execute.add_argument("--session-dir")
    execute.add_argument("--target", default="")
    execute.add_argument("--binary")
    execute.add_argument("--allow-npx", action="store_true")
    execute.add_argument("--headless", action="store_true")
    execute.add_argument("--timeout", type=int, default=120)
    execute.add_argument("--dialog-wait-ms", type=int, default=5_000)
    execute.add_argument("--action-class", choices=sorted(ACTION_CLASSES))
    execute.add_argument("--approve-consequential", action="store_true")
    execute.add_argument("vibium_command", nargs=argparse.REMAINDER)
    execute.set_defaults(func=command_exec)

    run_test = commands.add_parser("run-test")
    run_test.add_argument("--root", required=True)
    run_test.add_argument("--file")
    run_test.add_argument("--session-dir")
    run_test.add_argument("--target", default="")
    run_test.add_argument("--binary")
    run_test.add_argument("--timeout", type=int, default=900)
    run_test.add_argument(
        "--action-class", choices=sorted(ACTION_CLASSES), default="state-changing"
    )
    run_test.add_argument("--approve-consequential", action="store_true")
    run_test.add_argument("runner", nargs=argparse.REMAINDER)
    run_test.set_defaults(func=command_run_test)

    generate = commands.add_parser("generate")
    generate.add_argument("--root", required=True)
    generate.add_argument("--request", required=True)
    generate.add_argument("--target", default="")
    generate.add_argument("--vibium-map")
    generate.add_argument(
        "--size", choices=("quick", "medium", "large"), default="quick"
    )
    generate.add_argument(
        "--strategy", choices=("auto", "breadth", "depth", "balanced"), default="auto"
    )
    generate.add_argument("--timeout", type=int, default=120)
    generate.set_defaults(func=command_generate)

    api = commands.add_parser("api")
    api.set_defaults(func=command_api)
    return root


def main() -> int:
    args = parser().parse_args()
    try:
        output = args.func(args)
    except (ValueError, OSError) as exc:
        print(str(exc), file=sys.stderr)
        return 2
    json_print(output)
    return 0


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