"""AutoDebug CLI entrypoints."""

from __future__ import annotations

import json
import os
import platform
import re
import shutil
import subprocess
import sys
import time
import uuid
from collections.abc import Generator
from datetime import datetime, timezone
from importlib import metadata
from pathlib import Path
from typing import Any, Dict, List, Optional

import click
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

from autodebug import __version__


def _create_api_session() -> requests.Session:
    """Create a requests session with retry logic for transient failures."""
    session = requests.Session()

    retry_non_idempotent = os.environ.get("HIKAFLOW_RETRY_NON_IDEMPOTENT", "").lower() in (
        "1",
        "true",
        "yes",
    )
    allowed_methods = ["GET", "HEAD", "OPTIONS", "TRACE"]
    if retry_non_idempotent:
        allowed_methods.extend(["POST", "PUT", "DELETE", "PATCH"])

    retry_strategy = Retry(
        total=3,
        backoff_factor=1,  # Wait 1s, 2s, 4s between retries
        status_forcelist=[429, 500, 502, 503, 504],
        allowed_methods=allowed_methods,
    )

    adapter = HTTPAdapter(max_retries=retry_strategy)
    session.mount("https://", adapter)
    session.mount("http://", adapter)

    return session


def _api_base() -> str:
    base = get_api_base()
    if not base:
        raise SystemExit("API base not configured. Run `hikaflow login` or set HIKAFLOW_ENDPOINT.")
    return base.rstrip("/")


def _api_headers() -> Dict[str, str]:
    token = get_valid_token()
    if not token:
        raise SystemExit("Not authenticated. Run `hikaflow login` first.")
    return {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json",
    }


def _get_run_trace_id(run_id: str) -> Optional[str]:
    session = _create_api_session()
    resp = session.get(f"{_api_base()}/v1/runs/{run_id}", headers=_api_headers(), timeout=30)
    resp.raise_for_status()
    data = resp.json()
    return data.get("trace_id")


def _prepare_local_replay(directory: Path) -> Path:
    """Prepare a local rrweb replay HTML file from downloaded chunks."""
    try:
        import zstandard as zstd
    except Exception:
        raise SystemExit("zstandard not installed. Run: pip install zstandard") from None

    dom_chunks = sorted(directory.glob("dom-*.jsonl.zst")) + sorted(directory.glob("dom-*.jsonl"))
    if not dom_chunks:
        raise SystemExit("No DOM chunks found in local directory.")

    events: list[dict] = []
    for chunk in dom_chunks:
        if chunk.suffix.endswith("zst"):
            data = chunk.read_bytes()
            dctx = zstd.ZstdDecompressor()
            text = dctx.decompress(data).decode("utf-8", errors="ignore")
        else:
            text = chunk.read_text(encoding="utf-8", errors="ignore")
        for line in text.splitlines():
            if not line.strip():
                continue
            try:
                events.append(json.loads(line))
            except json.JSONDecodeError:
                click.echo("Warning: skipped malformed replay event line.", err=True)

    events_path = directory / "replay-events.json"
    events_path.write_text(json.dumps(events), encoding="utf-8")

    html = """<!doctype html>
<html>
  <head>
    <meta charset="utf-8"/>
    <title>Hikaflow Session Replay</title>
    <link rel="stylesheet" href="https://unpkg.com/rrweb-player@latest/dist/style.css"/>
    <style>
      body { background: #0f172a; margin: 0; padding: 24px; color: #e2e8f0; font-family: ui-sans-serif, system-ui; }
      #player { border: 1px solid #1f2937; border-radius: 12px; overflow: hidden; background: #111827; }
    </style>
  </head>
  <body>
    <h2>Hikaflow Session Replay (Local)</h2>
    <div id="player"></div>
    <script src="https://unpkg.com/rrweb-player@latest/dist/index.js"></script>
    <script>
      fetch('replay-events.json').then(r => r.json()).then(events => {
        new rrwebPlayer({
          target: document.getElementById('player'),
          props: { events }
        });
      });
    </script>
  </body>
</html>
"""
    html_path = directory / "replay.html"
    html_path.write_text(html, encoding="utf-8")
    return html_path
from autodebug.config import (
    CONFIG_DIR,
    clear_credentials,
    get_api_base,
    get_valid_token,
    load_credentials,
    save_credentials,
)


from autodebug.repo_utils import _normalize_repo_slug  # noqa: E402  # consolidated


def _get_repo_slug() -> Optional[str]:
    if not shutil.which("git"):
        return None
    try:
        result = subprocess.run(
            ["git", "remote", "get-url", "origin"],
            capture_output=True,
            text=True,
            check=False,
        )
        if result.returncode == 0:
            return _normalize_repo_slug(result.stdout.strip())
        remotes = subprocess.run(
            ["git", "remote"],
            capture_output=True,
            text=True,
            check=False,
        )
        if remotes.returncode == 0:
            names = [r for r in remotes.stdout.splitlines() if r.strip()]
            if names:
                result = subprocess.run(
                    ["git", "remote", "get-url", names[0]],
                    capture_output=True,
                    text=True,
                    check=False,
                )
                if result.returncode == 0:
                    return _normalize_repo_slug(result.stdout.strip())
    except Exception:
        return None
    return None


def _read_jsonl_events(run_dir: Path) -> Generator[Dict[str, Any], None, None]:
    """Read events from either compressed or uncompressed jsonl file."""
    # Try compressed first (new format)
    zst_path = run_dir / "events.jsonl.zst"
    if zst_path.exists():
        try:
            import zstandard as zstd
        except Exception:
            raise SystemExit("zstandard not installed. Run: pip install zstandard") from None
        dctx = zstd.ZstdDecompressor()
        with zst_path.open("rb") as f:
            reader = dctx.stream_reader(f)
            data = reader.read().decode("utf-8")
            for line in data.splitlines():
                if line.strip():
                    try:
                        yield json.loads(line)
                    except json.JSONDecodeError:
                        click.echo("Warning: skipped malformed event line in compressed transcript.", err=True)
        return

    # Fall back to uncompressed (old format)
    jsonl_path = run_dir / "events.jsonl"
    if jsonl_path.exists():
        for line in jsonl_path.read_text().splitlines():
            if line.strip():
                try:
                    yield json.loads(line)
                except json.JSONDecodeError:
                    click.echo("Warning: skipped malformed event line in transcript.", err=True)


@click.group()
@click.version_option(__version__)
def cli() -> None:
    """AutoDebug command-line interface."""
    click.echo(
        "Warning: 'autodebug' command is deprecated. Use 'hikaflow' instead.",
        err=True,
    )


@cli.command()
@click.option("--verbose", "-v", is_flag=True, help="Show detailed output.")
def doctor(verbose: bool) -> None:
    """Check environment and dependencies."""
    from autodebug.doctor import format_report, run_doctor

    results = run_doctor(verbose=verbose)
    click.echo(format_report(results))

    attempted = _maybe_install_or_start_docker(results)
    if attempted:
        results = run_doctor(verbose=verbose)
        click.echo(format_report(results))

    # Exit with error if any checks failed
    if results["failed"] > 0:
        raise SystemExit(1)


def _is_interactive() -> bool:
    return sys.stdin.isatty() and sys.stdout.isatty()


def _docker_check_from_results(results) -> Optional[object]:
    for check in results.get("checks", []):
        if getattr(check, "name", None) == "docker":
            return check
    return None


def _run_command(cmd: list[str]) -> bool:
    try:
        result = subprocess.run(cmd, check=False)
        return result.returncode == 0
    except Exception as exc:
        click.echo(f"Failed to run command: {' '.join(cmd)} ({exc})", err=True)
        return False


def _install_docker() -> bool:
    platform = sys.platform
    if platform == "darwin":
        if shutil.which("brew"):
            click.echo("Installing Docker Desktop via Homebrew...")
            return _run_command(["brew", "install", "--cask", "docker"])
        click.echo("Opening Docker Desktop download page...")
        return _run_command(["open", "https://docs.docker.com/desktop/install/mac-install/"])

    if platform.startswith("linux"):
        if shutil.which("curl"):
            click.echo(
                "Docker install via remote shell script is disabled for safety. "
                "Install Docker manually from https://docs.docker.com/engine/install/."
            )
            return False
        if shutil.which("apt-get"):
            click.echo("Installing Docker via apt (requires sudo)...")
            ok = _run_command(["sudo", "apt-get", "update"])
            if not ok:
                return False
            return _run_command(["sudo", "apt-get", "install", "-y", "docker.io"])
        click.echo("Unsupported Linux package manager. Please install Docker manually.")
        return False

    if platform.startswith("win"):
        if shutil.which("winget"):
            click.echo("Installing Docker Desktop via winget...")
            return _run_command(["winget", "install", "-e", "--id", "Docker.DockerDesktop"])
        click.echo("winget not available. Please install Docker Desktop manually.")
        return False

    click.echo("Unsupported platform. Please install Docker manually.")
    return False


def _start_docker() -> bool:
    platform = sys.platform
    if platform == "darwin":
        click.echo("Starting Docker Desktop...")
        return _run_command(["open", "-a", "Docker"])

    if platform.startswith("linux"):
        if shutil.which("systemctl"):
            click.echo("Starting Docker service (requires sudo)...")
            return _run_command(["sudo", "systemctl", "start", "docker"])
        if shutil.which("service"):
            click.echo("Starting Docker service (requires sudo)...")
            return _run_command(["sudo", "service", "docker", "start"])
        click.echo("Could not determine how to start Docker on this system.")
        return False

    if platform.startswith("win"):
        click.echo("Starting Docker Desktop...")
        return _run_command([
            "powershell",
            "-Command",
            "Start-Process -FilePath 'C:\\\\Program Files\\\\Docker\\\\Docker\\\\Docker Desktop.exe'",
        ])

    click.echo("Unsupported platform. Please start Docker manually.")
    return False


def _maybe_install_or_start_docker(results: dict) -> bool:
    if not _is_interactive():
        return False
    if os.environ.get("CI"):
        return False

    docker_check = _docker_check_from_results(results)
    if not docker_check or docker_check.passed:
        return False

    message = docker_check.message or ""
    if "not found" in message.lower():
        click.echo("Docker is required to run captures and replays in an isolated environment.")
        if click.confirm("Docker not found. Install Docker now?", default=False):
            installed = _install_docker()
            if installed:
                click.echo("Docker install initiated. Waiting a few seconds for setup...")
                time.sleep(5)
            return True
    elif "not running" in message.lower():
        click.echo("Docker is required to run captures and replays in an isolated environment.")
        if click.confirm("Docker is installed but not running. Start it now?", default=False):
            started = _start_docker()
            if started:
                click.echo("Waiting for Docker to start...")
                time.sleep(5)
            return True
    return False


@cli.command()
@click.option("--dev", is_flag=True, help="Use local dev auth token (localhost/private only).")
@click.option("--device-auth", is_flag=True, help="Use device authorization flow (for headless/SSH environments).")
@click.option("--token", envvar="HIKAFLOW_TOKEN", help="Manual JWT token (for CI/CD).")
@click.option("--api-base", envvar="HIKAFLOW_ENDPOINT", help="API base URL override.")
def login(dev: bool, device_auth: bool, token: str | None, api_base: str | None) -> None:
    """Authenticate with Hikaflow.

    By default, opens a browser for OAuth authentication.
    Use --device-auth for headless environments (SSH, containers).
    Use --token for CI/CD pipelines with a pre-generated token.

    Examples:
        hikaflow login                    # Browser OAuth (default)
        hikaflow login --device-auth      # Device flow for SSH
        hikaflow login --token <jwt>      # Manual token for CI
    """
    from autodebug.oauth import device_auth_flow, pkce_browser_flow

    # Store API base if provided
    if api_base:
        data = load_credentials()
        data["api_base"] = api_base
        save_credentials(data)

    effective_api_base = api_base or get_api_base()

    # Dev token flow (localhost only)
    if dev:
        from autodebug.auth import save_token

        dev_token = os.environ.get("HIKAFLOW_DEV_TOKEN", "").strip()
        if not dev_token:
            click.echo("Error: --dev requires HIKAFLOW_DEV_TOKEN to be set.", err=True)
            raise SystemExit(1)
        save_token(dev_token)
        if not api_base:
            data = load_credentials()
            data["api_base"] = "http://localhost:8000"
            save_credentials(data)
            click.echo("API base set to: http://localhost:8000")
        click.echo("Dev token saved to credentials.")
        return

    # Manual token flow (for CI/CD)
    if token:
        data = load_credentials()
        data["access_token"] = token
        # Remove any stale refresh token since manual tokens don't have one
        data.pop("refresh_token", None)
        data.pop("expires_at", None)
        save_credentials(data)
        click.echo(f"Token saved to {CONFIG_DIR / 'credentials.json'}")
        return

    # Device authorization flow (for headless)
    if device_auth:
        success, message = device_auth_flow(effective_api_base)
        if success:
            click.echo(message)
            click.echo(f"Credentials saved to {CONFIG_DIR / 'credentials.json'}")
        else:
            click.echo(f"Error: {message}", err=True)
            raise SystemExit(1)
        return

    # Browser PKCE flow (default)
    success, message = pkce_browser_flow(effective_api_base)
    if success:
        click.echo(message)
        click.echo(f"Credentials saved to {CONFIG_DIR / 'credentials.json'}")
    else:
        click.echo(f"Error: {message}", err=True)
        click.echo("")
        click.echo("Tip: If you're in a headless environment, try:")
        click.echo("  hikaflow login --device-auth")
        raise SystemExit(1)


@cli.command()
@click.option("--all", "clear_all", is_flag=True, help="Clear all configuration including project settings.")
def logout(clear_all: bool) -> None:
    """Log out and clear stored credentials.

    By default, only clears authentication tokens.
    Use --all to also clear project configuration.

    Examples:
        hikaflow logout         # Clear tokens only
        hikaflow logout --all   # Clear all config
    """
    if clear_all:
        clear_credentials()
        click.echo(f"All configuration cleared from {CONFIG_DIR}")
    else:
        data = load_credentials()
        # Remove only auth-related fields
        data.pop("access_token", None)
        data.pop("refresh_token", None)
        data.pop("expires_at", None)
        data.pop("token", None)  # Legacy field
        save_credentials(data)
        click.echo("Logged out successfully.")


@cli.command()
def whoami() -> None:
    """Show current authentication status.

    Displays information about the currently logged-in user
    and the configured project/API settings.

    Examples:
        hikaflow whoami
    """
    creds = load_credentials()

    click.echo("Hikaflow CLI Status")
    click.echo("=" * 40)

    # Check authentication
    token = get_valid_token()
    if token:
        click.echo("Authentication: Logged in")

        # Try to decode JWT to show user info (without verification)
        try:
            import base64
            import json
            # JWT has 3 parts: header.payload.signature
            payload_part = token.split(".")[1]
            # Add padding if needed
            padding = 4 - len(payload_part) % 4
            if padding != 4:
                payload_part += "=" * padding
            payload = json.loads(base64.urlsafe_b64decode(payload_part))

            if payload.get("email"):
                click.echo(f"User: {payload.get('email')}")
            if payload.get("sub"):
                click.echo(f"User ID: {payload.get('sub')[:20]}...")
            if payload.get("org_id"):
                click.echo(f"Org ID: {payload.get('org_id')[:20]}...")
        except Exception:
            pass  # Can't decode token, that's ok

        # Check expiration
        expires_at = creds.get("expires_at")
        if expires_at:
            import time
            remaining = expires_at - time.time()
            if remaining > 0:
                hours = int(remaining // 3600)
                minutes = int((remaining % 3600) // 60)
                click.echo(f"Token expires in: {hours}h {minutes}m")
            else:
                click.echo("Token: Expired (will refresh on next request)")
    else:
        click.echo("Authentication: Not logged in")
        click.echo("")
        click.echo("Run 'hikaflow login' to authenticate.")

    click.echo("")

    # Show configuration
    click.echo("Configuration:")
    click.echo(f"  Config dir: {CONFIG_DIR}")
    click.echo(f"  API base: {get_api_base()}")

    if creds.get("project_id"):
        click.echo(f"  Project ID: {creds.get('project_id')}")
    else:
        click.echo("  Project ID: Not configured")
        click.echo("  Run 'hikaflow init --project-id <id>' to set up a project.")
    if creds.get("repo_slug"):
        click.echo(f"  Repo: {creds.get('repo_slug')}")


@cli.command()
@click.option("--org-id", help="Organization ID for the project.")
@click.option("--project-id", help="Project ID (must be a valid UUID).")
@click.option("--project-name", help="Project name (creates new project if --project-id not provided).")
@click.option("--api-base", envvar="HIKAFLOW_ENDPOINT", help="API base URL.")
def init(org_id: str | None, project_id: str | None, project_name: str | None, api_base: str | None) -> None:
    """Configure project settings for future runs.

    Sets the project and optional organization ID that will be used
    when uploading test runs to Hikaflow.

    You can either:
    - Provide an existing project UUID with --project-id
    - Create a new project with --project-name (requires API access)

    Examples:
        hikaflow init --project-id 550e8400-e29b-41d4-a716-446655440000
        hikaflow init --project-name "my-project"
        hikaflow init --project-id abc-123 --org-id org-456
    """
    import uuid as uuid_module

    data = load_credentials()

    if api_base:
        data["api_base"] = api_base

    repo_slug = _get_repo_slug()
    if repo_slug:
        data["repo_slug"] = repo_slug

    # If project-name provided, try to create a new project via API
    if project_name and not project_id:
        api = data.get("api_base") or api_base or "https://api.hikaflow.dev"
        token = get_valid_token()
        if token:
            try:
                session = _create_api_session()
                new_project_id = str(uuid_module.uuid4())
                response = session.post(
                    f"{api.rstrip('/')}/v1/projects",
                    json={
                        "name": project_name,
                        "description": f"Created via CLI for {project_name}",
                        "repo": repo_slug,
                    },
                    timeout=10,
                )
                if response.status_code == 200:
                    result = response.json()
                    project_id = result.get("id") or result.get("project_id")
                    click.echo(f"Created project: {project_name} (ID: {project_id})")
                else:
                    click.echo(f"Failed to create project: {response.text}", err=True)
                    click.echo("You can manually provide --project-id instead.", err=True)
                    raise SystemExit(1)
            except Exception as e:
                click.echo(f"Failed to create project: {e}", err=True)
                click.echo("You can manually provide --project-id instead.", err=True)
                raise SystemExit(1)
        else:
            click.echo("Not logged in. Run 'hikaflow login' first or provide --project-id.", err=True)
            raise SystemExit(1)

    if not project_id:
        click.echo("Missing project id. Provide --project-id or --project-name.", err=True)
        raise SystemExit(1)

    # Validate UUID format
    try:
        uuid_module.UUID(project_id)
    except ValueError:
        click.echo(f"Invalid project_id format: '{project_id}' is not a valid UUID.", err=True)
        click.echo("Project IDs must be UUIDs like: 550e8400-e29b-41d4-a716-446655440000", err=True)
        raise SystemExit(1)

    if org_id:
        data["org_id"] = org_id
    data["project_id"] = project_id
    save_credentials(data)
    click.echo(f"Project configuration saved to {CONFIG_DIR / 'credentials.json'}")


@cli.command(context_settings={"ignore_unknown_options": True, "allow_extra_args": True})
@click.pass_context
def run(ctx: click.Context) -> None:
    """Run a command under AutoDebug instrumentation."""
    cmd: List[str] = ctx.args
    if not cmd:
        click.echo("Missing command. Usage: hikaflow run -- <command>", err=True)
        raise SystemExit(1)

    creds = load_credentials()
    api_base = get_api_base()
    token = get_valid_token()
    project_id = creds.get("project_id")

    upload_urls: Optional[Dict[str, str]] = None
    run_id = None

    if api_base and token and project_id:
        try:
            env_fingerprint = _env_fingerprint_summary()
            test_name = _extract_test_name(cmd)
            session = _create_api_session()
            response = session.post(
                f"{api_base.rstrip('/')}/v1/runs",
                json={
                    "project_id": project_id,
                    "entry_cmd": {"argv": cmd},
                    "env_fingerprint": env_fingerprint,
                    "test_name": test_name,
                },
                headers={"Authorization": f"Bearer {token}"},
                timeout=30,
            )
            response.raise_for_status()
            payload = response.json()
            run_id = payload.get("run_id")
            upload_urls = payload.get("upload_urls")
        except Exception as exc:  # pylint: disable=broad-except
            click.echo(f"Failed to create run: {exc}", err=True)

    if run_id is None:
        run_id = str(uuid.uuid4())

    run_dir = Path(".autodebug") / "runs" / run_id
    run_dir.mkdir(parents=True, exist_ok=True)

    manifest = {
        "run_id": run_id,
        "created_at": datetime.now(timezone.utc).isoformat(),
        "entry_cmd": {"argv": cmd},
        "env_fingerprint": _env_fingerprint_summary(),
        "tool_versions": {
            "autodebug_cli": __version__,
            "python": sys.version.split(" ")[0],
        },
        "event_ordering": {"global_seq_start": 0},
    }
    (run_dir / "manifest.json").write_text(
        json.dumps(manifest, indent=2, sort_keys=True), encoding="utf-8"
    )
    click.echo(f"Run id: {run_id}")
    click.echo(f"Local bundle path: {run_dir}")

    env = os.environ.copy()
    env["AUTODEBUG_MODE"] = "record"
    env["AUTODEBUG_RUN_ID"] = run_id
    env["AUTODEBUG_RUN_DIR"] = str(run_dir)
    env["AUTODEBUG_ENDPOINT"] = api_base or env.get("AUTODEBUG_ENDPOINT", "")
    env["AUTODEBUG_TOKEN"] = token or env.get("AUTODEBUG_TOKEN", "")

    # Capture duration
    import time
    start_time = time.time()
    proc = os.spawnve(os.P_WAIT, sys.executable, [sys.executable, "-m", "autodebug_probe.bootstrap", "--", *cmd], env)
    duration = time.time() - start_time

    # Update manifest with duration
    manifest["duration"] = round(duration, 3)
    manifest["exit_code"] = proc
    (run_dir / "manifest.json").write_text(
        json.dumps(manifest, indent=2, sort_keys=True), encoding="utf-8"
    )

    if upload_urls:
        try:
            _upload_bundle(run_dir, upload_urls)
            io_counts = _count_io_events(run_dir)
            total_io = sum(io_counts.values())
            session = _create_api_session()
            response = session.post(
                f"{api_base.rstrip('/')}/v1/runs/{run_id}/finalize",
                json={"duration": round(duration, 3), "exit_code": proc, "io_count": total_io},
                headers={"Authorization": f"Bearer {token}"},
                timeout=30,
            )
            response.raise_for_status()
        except Exception as exc:  # pylint: disable=broad-except
            click.echo(f"Failed to upload/finalize run: {exc}", err=True)

    raise SystemExit(proc)


def _extract_test_name(cmd: List[str]) -> str:
    """Extract a meaningful test name from the command.

    Examples:
    - ["pytest", "test_calculator.py"] -> "test_calculator.py"
    - ["pytest", "tests/test_api.py::test_login"] -> "test_api.py::test_login"
    - ["python", "-m", "pytest", "test_foo.py"] -> "test_foo.py"
    - ["python", "test_script.py"] -> "test_script.py"
    """
    # Look for test file patterns in the command
    for arg in reversed(cmd):
        # Skip flags and options
        if arg.startswith("-"):
            continue
        # Look for pytest node IDs (file::test) or test files
        if "::" in arg:
            return arg.split("/")[-1]  # Get the filename::test part
        # Look for test file patterns
        basename = arg.split("/")[-1]
        if basename.startswith("test_") and basename.endswith(".py"):
            return basename
        if basename.endswith("_test.py"):
            return basename
        if basename.endswith(".py") and "test" in basename.lower():
            return basename

    # Fall back to command summary
    if cmd:
        # If it's pytest or python -m pytest, note that
        if cmd[0] == "pytest" or (len(cmd) >= 3 and cmd[0] == "python" and cmd[1] == "-m" and cmd[2] == "pytest"):
            return "pytest run"
        return " ".join(cmd[:2]) if len(cmd) > 1 else cmd[0]
    return "unknown"


def _env_fingerprint_summary() -> Dict[str, object]:
    packages = {}
    for dist in metadata.distributions():
        name = dist.metadata.get("Name")
        version = dist.version
        if name and version:
            packages[name] = version
    return {
        "python_version": platform.python_version(),
        "python_implementation": platform.python_implementation(),
        "platform": f"{sys.platform}-{platform.machine()}",
        "installed_packages": dict(sorted(packages.items())),
        "env_vars_read": {},
    }


def _upload_bundle(run_dir: Path, upload_urls: Dict[str, str]) -> None:
    for filename, url in upload_urls.items():
        path = run_dir / filename
        if not path.exists():
            raise FileNotFoundError(f"Missing bundle file: {path}")
        with path.open("rb") as handle:
            resp = requests.put(url, data=handle, timeout=60)
            resp.raise_for_status()


def _count_io_events(run_dir: Path) -> Dict[str, int]:
    """Count I/O events from transcript files."""
    import zstandard as zstd

    counts = {"http": 0, "sql": 0, "redis": 0, "grpc": 0, "mongodb": 0, "aws": 0}
    file_mapping = {
        "deps_http.jsonl.zst": "http",
        "deps_sql.jsonl.zst": "sql",
        "deps_redis.jsonl.zst": "redis",
        "deps_grpc.jsonl.zst": "grpc",
        "deps_mongodb.jsonl.zst": "mongodb",
        "deps_aws.jsonl.zst": "aws",
    }

    for filename, key in file_mapping.items():
        path = run_dir / filename
        if path.exists():
            try:
                with path.open("rb") as f:
                    dctx = zstd.ZstdDecompressor()
                    with dctx.stream_reader(f) as reader:
                        text = reader.read().decode("utf-8")
                        # Count non-empty lines (each line is a JSON record)
                        counts[key] = sum(1 for line in text.strip().split("\n") if line.strip())
            except Exception:
                pass  # If decompression fails, leave count as 0

    return counts


@cli.command()
@click.argument("left_path", type=click.Path(exists=True))
@click.argument("right_path", type=click.Path(exists=True))
@click.option("--by-type", is_flag=True, help="Group differences by record type.")
@click.option("--max-diffs", default=20, help="Maximum differences to show.")
def diff(left_path: str, right_path: str, by_type: bool, max_diffs: int) -> None:
    """Compare two transcripts to find divergences.

    Examples:
        autodebug diff run-123/deps_http.jsonl run-456/deps_http.jsonl
        autodebug diff .autodebug/runs/abc/events.jsonl .autodebug/runs/def/events.jsonl
    """
    from autodebug.diff import (
        diff_by_type,
        diff_transcripts,
        format_diff_report,
        format_type_diff_report,
        load_transcript,
    )

    left = load_transcript(Path(left_path))
    right = load_transcript(Path(right_path))

    if by_type:
        results = diff_by_type(left, right)
        click.echo(format_type_diff_report(results))
    else:
        result = diff_transcripts(left, right, left_path, right_path)
        click.echo(format_diff_report(result, max_diffs=max_diffs))


@cli.command("replay-debug")
@click.argument("transcript_path", type=click.Path(exists=True))
def replay_debug(transcript_path: str) -> None:
    """Interactively step through a transcript.

    Commands: next, step, continue, break, list, inspect, search, quit

    Examples:
        autodebug replay-debug .autodebug/runs/abc/events.jsonl
    """
    from autodebug.replay_debug import load_and_debug

    load_and_debug(Path(transcript_path))


@cli.command()
@click.argument("harness_path", type=click.Path(exists=True))
@click.option("--runs", default=100, help="Number of validation runs.")
@click.option("--parallel", default=4, help="Number of parallel runs.")
@click.option("--timeout", default=120, help="Timeout per run in seconds.")
def flakiness(harness_path: str, runs: int, parallel: int, timeout: int) -> None:
    """Analyze test flakiness by running validation many times.

    Examples:
        autodebug flakiness ./repro.tar.gz --runs 100
        autodebug flakiness ./harness --runs 50 --parallel 8
    """
    from autodebug.flakiness import analyze_flakiness, format_flakiness_report

    def progress(completed: int, total: int) -> None:
        click.echo(f"\rProgress: {completed}/{total}", nl=False)

    click.echo(f"Running {runs} validation runs...")
    result = analyze_flakiness(
        Path(harness_path),
        num_runs=runs,
        parallel=parallel,
        timeout=timeout,
        progress_callback=progress,
    )
    click.echo("")  # New line after progress
    click.echo(format_flakiness_report(result))


@cli.command("flaky-scan")
@click.argument("test_path", type=click.Path(exists=True))
@click.option("--runs", default=5, help="Number of times to run each test.")
@click.option("--parallel", default=4, help="Number of parallel test executions.")
@click.option("--timeout", default=60, help="Timeout per test in seconds.")
@click.option("--quarantine", is_flag=True, help="Add flaky tests to .flaky-quarantine file.")
@click.option("--threshold", default=0.8, help="Pass rate threshold for flakiness (0-1).")
def flaky_scan(test_path: str, runs: int, parallel: int, timeout: int, quarantine: bool, threshold: float) -> None:
    """Scan test suite for flaky tests.

    Runs each test multiple times to detect non-deterministic behavior.
    Tests that pass sometimes and fail sometimes are marked as flaky.

    Examples:
        autodebug flaky-scan ./tests --runs 10
        autodebug flaky-scan ./tests --runs 5 --quarantine
        autodebug flaky-scan ./tests/test_api.py --runs 20
    """
    from autodebug.flaky_quarantine import (
        QUARANTINE_FILE,
        add_to_quarantine,
        format_flaky_scan_report,
        scan_for_flaky_tests,
    )

    def progress(completed: int, total: int, current_test: str) -> None:
        test_name = current_test.split("::")[-1] if "::" in current_test else current_test
        click.echo(f"\rScanning: {completed}/{total} - {test_name[:40]:<40}", nl=False)

    click.echo(f"Scanning {test_path} for flaky tests...")
    click.echo(f"Running each test {runs} times with {parallel} parallel workers")
    click.echo("")

    result = scan_for_flaky_tests(
        test_path,
        runs=runs,
        parallel=parallel,
        timeout=timeout,
        flaky_threshold=threshold,
        progress_callback=progress,
    )

    click.echo("")  # New line after progress
    click.echo(format_flaky_scan_report(result))

    if quarantine and result.flaky_tests:
        added = add_to_quarantine(result.flaky_tests)
        click.echo("")
        click.echo(f"Added {added} flaky tests to {QUARANTINE_FILE}")
        click.echo(f"Run pytest with: pytest --deselect-from-file={QUARANTINE_FILE}")
        click.echo("Or use: hikaflow quarantine --skip to generate pytest args")


@cli.command("quarantine")
@click.option("--list", "list_tests", is_flag=True, help="List all quarantined tests.")
@click.option("--skip", "skip_args", is_flag=True, help="Print pytest args to skip quarantined tests.")
@click.option("--remove", multiple=True, help="Remove test(s) from quarantine by nodeid.")
@click.option("--clear", is_flag=True, help="Clear all quarantined tests.")
def quarantine_cmd(list_tests: bool, skip_args: bool, remove: tuple, clear: bool) -> None:
    """Manage the flaky test quarantine file.

    Examples:
        autodebug quarantine --list
        autodebug quarantine --skip
        autodebug quarantine --remove "tests/test_api.py::test_flaky"
        autodebug quarantine --clear
    """
    from autodebug.flaky_quarantine import (
        QUARANTINE_FILE,
        generate_pytest_deselect_args,
        list_quarantined_tests,
        remove_from_quarantine,
    )

    qf_path = Path.cwd() / QUARANTINE_FILE

    if clear:
        if qf_path.exists():
            qf_path.unlink()
            click.echo(f"Cleared {QUARANTINE_FILE}")
        else:
            click.echo(f"No {QUARANTINE_FILE} found")
        return

    if remove:
        removed = remove_from_quarantine(list(remove))
        click.echo(f"Removed {removed} test(s) from quarantine")
        return

    if skip_args:
        args = generate_pytest_deselect_args()
        if args:
            click.echo(" ".join(args))
        else:
            click.echo("# No quarantined tests")
        return

    # Default: list quarantined tests
    tests = list_quarantined_tests()
    if not tests:
        click.echo(f"No quarantined tests in {QUARANTINE_FILE}")
        return

    click.echo(f"Quarantined tests ({len(tests)}):")
    click.echo("-" * 60)
    for test in sorted(tests, key=lambda t: t.nodeid):
        click.echo(f"  {test.nodeid}")
        click.echo(f"    Pass rate: {test.pass_rate*100:.0f}% | Flakiness: {test.flakiness_score:.0f}%")
        click.echo(f"    Quarantined: {test.quarantined_at[:10]}")


@cli.command()
@click.argument("harness_path", type=click.Path(exists=True))
@click.argument("transcript_path", type=click.Path(exists=True))
@click.option("--method", type=click.Choice(["ddmin", "hierarchical", "causal"]),
              default="hierarchical", help="Minimization method.")
@click.option("--output", "-o", type=click.Path(), help="Output path for minimal transcript.")
def minimize(harness_path: str, transcript_path: str, method: str, output: str) -> None:
    """Minimize a transcript while preserving the failure.

    Uses delta debugging to find the smallest transcript that still
    reproduces the failure.

    Examples:
        autodebug minimize ./harness ./transcript.jsonl -o minimal.jsonl
    """
    import json as json_module

    from autodebug.diff import load_transcript
    from autodebug.minimize import format_minimization_report, minimize_transcript

    transcript = load_transcript(Path(transcript_path))
    click.echo(f"Loaded {len(transcript)} records from {transcript_path}")
    click.echo(f"Minimizing using {method} method...")

    result = minimize_transcript(
        transcript,
        Path(harness_path),
        method=method,
    )

    click.echo(format_minimization_report(result))

    if output and "minimal_transcript" in result:
        with open(output, "w", encoding="utf-8") as f:
            for record in result.get("minimal_transcript"):
                f.write(json_module.dumps(record, sort_keys=True) + "\n")
        click.echo(f"\nMinimal transcript saved to {output}")


@cli.command("validate-profile")
@click.argument("harness_path", type=click.Path(exists=True))
def validate_profile(harness_path: str) -> None:
    """Profile validation runs to identify bottlenecks.

    Measures Docker build time, container startup, test execution,
    and provides optimization recommendations.

    Examples:
        autodebug validate-profile ./repro.tar.gz
        autodebug validate-profile ./harness
    """
    from autodebug.profile import format_profile_report, profile_validation

    click.echo("Profiling validation run...")
    result = profile_validation(Path(harness_path))
    click.echo(format_profile_report(result))


@cli.command("openclaw-eval")
@click.argument("run_path", type=click.Path(exists=True, file_okay=False))
@click.option("--model", help="OpenClaw model override (e.g. openai/gpt-5.2).")
@click.option("--timeout", "timeout_seconds", default=120, show_default=True, type=int, help="OpenClaw timeout.")
@click.option("--local/--gateway", default=True, show_default=True, help="Use local embedded OpenClaw runtime.")
@click.option("--output", "-o", type=click.Path(), help="Optional output file for full JSON result.")
def openclaw_eval(
    run_path: str,
    model: str | None,
    timeout_seconds: int,
    local: bool,
    output: str | None,
) -> None:
    """Evaluate a captured run with OpenClaw and suggest improvements.

    This is an optional meta-evaluation path for testing Hikaflow itself.
    It asks OpenClaw for structured feedback focused on test gaps and concrete
    reliability improvements.
    """
    if not shutil.which("openclaw"):
        click.echo("Error: `openclaw` command not found in PATH.", err=True)
        click.echo("Install OpenClaw first, then retry `hikaflow openclaw-eval`.", err=True)
        raise SystemExit(1)

    if timeout_seconds <= 0:
        click.echo("Error: --timeout must be a positive integer.", err=True)
        raise SystemExit(1)

    from autodebug.openclaw_eval import run_openclaw_eval

    click.echo(f"Running OpenClaw evaluation for: {run_path}")
    try:
        result = run_openclaw_eval(
            Path(run_path),
            model=model,
            timeout_seconds=timeout_seconds,
            local=local,
        )
    except Exception as exc:
        click.echo(f"OpenClaw evaluation failed: {exc}", err=True)
        raise SystemExit(1) from exc

    evaluation = result.get("evaluation", {})
    click.echo("\nOpenClaw Summary")
    click.echo("-" * 40)
    click.echo(f"Priority: {evaluation.get('priority', 'unknown')}")
    click.echo(f"Summary: {evaluation.get('summary', 'n/a')}")

    gaps = evaluation.get("gaps") or []
    if gaps:
        click.echo("\nGaps:")
        for item in gaps:
            click.echo(f"  - {item}")

    improvements = evaluation.get("improvements") or []
    if improvements:
        click.echo("\nImprovements:")
        for item in improvements:
            click.echo(f"  - {item}")

    next_tests = evaluation.get("next_tests") or []
    if next_tests:
        click.echo("\nNext Tests:")
        for item in next_tests:
            click.echo(f"  - {item}")

    if output:
        out_path = Path(output)
        out_path.parent.mkdir(parents=True, exist_ok=True)
        out_path.write_text(json.dumps(result, indent=2, sort_keys=True), encoding="utf-8")
        click.echo(f"\nSaved full OpenClaw evaluation to: {out_path}")


@cli.command()
@click.argument("bundle_path", type=click.Path(exists=True))
@click.option("--output", "-o", type=click.Path(), help="Output directory for optimized files.")
def optimize(bundle_path: str, output: str) -> None:
    """Optimize transcript bundle for storage.

    Applies deduplication and high compression to reduce storage costs.

    Examples:
        autodebug optimize .autodebug/runs/abc/
        autodebug optimize ./bundle -o ./optimized
    """
    from worker.optimize import format_optimization_report, optimize_bundle_directory

    bundle_dir = Path(bundle_path)
    if not bundle_dir.is_dir():
        click.echo(f"Error: {bundle_path} is not a directory", err=True)
        raise SystemExit(1)

    output_dir = Path(output) if output else bundle_dir
    if output:
        output_dir.mkdir(parents=True, exist_ok=True)
        # Copy files to output dir first
        import shutil
        for f in bundle_dir.iterdir():
            if f.is_file():
                shutil.copy2(f, output_dir / f.name)
        bundle_dir = output_dir

    click.echo(f"Optimizing bundle at {bundle_dir}...")
    results = optimize_bundle_directory(bundle_dir)

    for filename, result in results.items():
        click.echo(f"\n{filename}:")
        click.echo(format_optimization_report(result))

    if results:
        total_original = sum(r.original_size for r in results.values())
        total_optimized = sum(r.optimized_size for r in results.values())
        total_reduction = (1 - total_optimized / total_original) * 100 if total_original > 0 else 0
        click.echo(f"\nTotal: {total_original:,} -> {total_optimized:,} bytes ({total_reduction:.1f}% reduction)")


@cli.command()
@click.argument("transcript_path", type=click.Path(exists=True))
@click.option("--format", "-f", "fmt", type=click.Choice(["har", "curl", "postman"]),
              default="har", help="Export format.")
@click.option("--output", "-o", type=click.Path(), help="Output file path.")
def export(transcript_path: str, fmt: str, output: str) -> None:
    """Export HTTP records to external formats.

    Supports HAR (HTTP Archive), curl commands, and Postman collections.

    Examples:
        autodebug export ./deps_http.jsonl -f har -o requests.har
        autodebug export ./deps_http.jsonl -f curl
        autodebug export ./deps_http.jsonl -f postman -o collection.json
    """
    from autodebug.diff import load_transcript
    from autodebug.export import export_to_curl, export_to_har, export_to_postman

    records = load_transcript(Path(transcript_path))
    http_records = [r for r in records if r.get("type") == "HTTP"]

    if not http_records:
        click.echo("No HTTP records found in transcript", err=True)
        raise SystemExit(1)

    click.echo(f"Found {len(http_records)} HTTP records")

    if fmt == "har":
        result = export_to_har(http_records)
        content = json.dumps(result, indent=2)
    elif fmt == "curl":
        content = export_to_curl(http_records)
    elif fmt == "postman":
        name = Path(transcript_path).stem if not output else Path(output).stem
        result = export_to_postman(http_records, collection_name=name)
        content = json.dumps(result, indent=2)
    else:
        click.echo(f"Unknown format: {fmt}", err=True)
        raise SystemExit(1)

    if output:
        Path(output).write_text(content, encoding="utf-8")
        click.echo(f"Exported to {output}")
    else:
        click.echo(content)


@cli.command("auto-debug")
@click.argument("run_path", type=click.Path(exists=True))
@click.option("--harness", "-h", type=click.Path(exists=True), help="Path to repro harness.")
@click.option("--hypotheses", "-n", default=3, help="Number of hypotheses to generate.")
@click.option("--validate/--no-validate", default=True, help="Validate hypotheses against harness.")
@click.option("--output", "-o", type=click.Path(), help="Output file for results.")
def auto_debug(run_path: str, harness: str, hypotheses: int, validate: bool, output: str) -> None:
    """Autonomous debugging with LLM-powered fix generation.

    Analyzes a failure, generates fix hypotheses using LLMs,
    and optionally validates them against the repro harness.

    Requires ANTHROPIC_API_KEY or OPENAI_API_KEY environment variable.

    Examples:
        autodebug auto-debug .autodebug/runs/abc/
        autodebug auto-debug ./run --harness ./repro.tar.gz -n 5
        autodebug auto-debug ./run --no-validate -o results.json
    """
    import os as os_module

    from autodebug.autonomous.analyzer import analyze_failure, format_analysis_report
    from autodebug.autonomous.hypothesis import (
        CostTracker,
        format_hypothesis_report,
        generate_hypotheses_with_fallback,
    )

    # Check for API keys
    has_anthropic = bool(os_module.environ.get("ANTHROPIC_API_KEY"))
    has_openai = bool(os_module.environ.get("OPENAI_API_KEY"))

    if not has_anthropic and not has_openai:
        click.echo("Error: ANTHROPIC_API_KEY or OPENAI_API_KEY environment variable required", err=True)
        raise SystemExit(1)

    run_dir = Path(run_path)

    # Load manifest and find exception
    manifest_path = run_dir / "manifest.json"
    if not manifest_path.exists():
        click.echo(f"Error: No manifest.json found in {run_path}", err=True)
        raise SystemExit(1)

    manifest = json.loads(manifest_path.read_text())

    # Find exception from events or result
    exception = None
    for record in _read_jsonl_events(run_dir):
        if record.get("type") == "EXCEPTION":
            exception = record
            break

    # Also check for result.json
    result_path = run_dir / "result.json"
    if result_path.exists() and not exception:
        result_data = json.loads(result_path.read_text())
        if result_data.get("exception"):
            exception = result_data["exception"]

    if not exception:
        click.echo("Error: No exception found in run", err=True)
        raise SystemExit(1)

    # Load causal slice if available
    slice_path = run_dir / "slice.json"
    causal_slice = None
    if slice_path.exists():
        causal_slice = json.loads(slice_path.read_text())

    # Analyze failure
    click.echo("Analyzing failure...")
    analysis = analyze_failure(exception, causal_slice, manifest)
    click.echo(format_analysis_report(analysis))

    # Generate hypotheses
    click.echo(f"\nGenerating {hypotheses} hypotheses...")
    cost_tracker = CostTracker()

    generated = generate_hypotheses_with_fallback(
        analysis.context,
        num_hypotheses=hypotheses,
        cost_tracker=cost_tracker,
    )

    if not generated:
        click.echo("Error: Failed to generate any hypotheses", err=True)
        raise SystemExit(1)

    click.echo(format_hypothesis_report(generated))
    click.echo(f"\nLLM cost: {cost_tracker}")

    # Validate if harness provided and validation enabled
    if validate and harness:
        from autodebug.autonomous.proof import format_proof_certificate, generate_proof_certificate
        from autodebug.autonomous.ranking import format_ranking_report, rank_fixes
        from autodebug.autonomous.validator import format_validation_report, validate_hypotheses

        click.echo(f"\nValidating {len(generated)} hypotheses against harness...")

        def progress(completed: int, total: int) -> None:
            click.echo(f"\rValidating: {completed}/{total}", nl=False)

        validations = validate_hypotheses(
            generated,
            Path(harness),
            original_exception=exception,
            progress_callback=progress,
        )
        click.echo("")  # Newline after progress

        click.echo(format_validation_report(validations))

        # Generate certificates for proven fixes
        certificates = []
        for v in validations:
            if v.fixes_failure:
                cert = generate_proof_certificate(
                    original_run_id=manifest.get("run_id", "unknown"),
                    original_exception=exception,
                    hypothesis_id=v.hypothesis.id,
                    patch_hash=v.hypothesis.patch.hash,
                    validation_results=[
                        {"exit_code": r.exit_code, "passed": r.passed, "output": r.output}
                        for r in v.runs
                    ],
                    verdict=v.verdict,
                )
                certificates.append(cert)
                click.echo(format_proof_certificate(cert))

        # Rank fixes
        ranked = rank_fixes(validations, certificates)
        click.echo(format_ranking_report(ranked))

        # Save results if output specified
        if output:
            results = {
                "run_id": manifest.get("run_id"),
                "analysis": {
                    "exception_type": analysis.exception_type,
                    "exception_message": analysis.exception_message,
                    "category": analysis.category.name if analysis.category else None,
                    "location": analysis.failure_location,
                },
                "hypotheses": [
                    {
                        "id": h.id,
                        "root_cause": h.root_cause,
                        "confidence": h.confidence,
                        "strategy": h.strategy,
                        "patch": h.patch.raw_diff,
                    }
                    for h in generated
                ],
                "validations": [
                    {
                        "hypothesis_id": v.hypothesis.id,
                        "verdict": v.verdict,
                        "pass_rate": v.pass_rate,
                    }
                    for v in validations
                ],
                "ranked_fixes": [
                    {
                        "rank": r.rank,
                        "hypothesis_id": r.hypothesis.id,
                        "score": r.score,
                        "is_proven": r.is_proven,
                    }
                    for r in ranked
                ],
                "certificates": [c.to_dict() for c in certificates],
                "cost": str(cost_tracker),
            }
            Path(output).write_text(json.dumps(results, indent=2), encoding="utf-8")
            click.echo(f"\nResults saved to {output}")

    elif validate and not harness:
        click.echo("\nSkipping validation: No harness provided. Use --harness to validate fixes.")


@cli.command()
@click.argument("run_path", type=click.Path(exists=True))
@click.option("--expires", default=7, help="Days until link expires (0 = never).")
@click.option("--open", "open_browser", is_flag=True, help="Open share link in browser.")
def share(run_path: str, expires: int, open_browser: bool) -> None:
    """Create a shareable link for a run.

    Uploads the run bundle to cloud storage and returns a shareable URL
    that others can use to view the replay without authentication.

    Examples:
        autodebug share .autodebug/runs/abc/
        autodebug share ./run-123 --expires 30
        autodebug share ./run-123 --open
    """
    import webbrowser

    run_dir = Path(run_path)

    # Verify it's a valid run directory
    manifest_path = run_dir / "manifest.json"
    if not manifest_path.exists():
        click.echo(f"Error: No manifest.json found in {run_path}", err=True)
        click.echo("This doesn't appear to be a valid run directory.", err=True)
        raise SystemExit(1)

    manifest = json.loads(manifest_path.read_text())
    run_id = manifest.get("run_id")

    if not run_id:
        click.echo("Error: manifest.json missing run_id", err=True)
        raise SystemExit(1)

    # Load credentials
    creds = load_credentials()
    api_base = get_api_base()
    token = get_valid_token()

    if not token:
        click.echo("Error: Not logged in. Run 'hikaflow login' first.", err=True)
        raise SystemExit(1)

    click.echo(f"Sharing run: {run_id}")

    # First check if run exists on server, if not upload it
    try:
        check_resp = requests.get(
            f"{api_base.rstrip('/')}/v1/runs/{run_id}",
            headers={"Authorization": f"Bearer {token}"},
            timeout=10,
        )

        if check_resp.status_code == 404:
            # Run doesn't exist on server, need to upload
            click.echo("Run not found on server. Uploading bundle...")

            # Get upload URLs
            project_id = creds.get("project_id")
            if not project_id:
                click.echo("Error: No project configured. Run 'hikaflow init' first.", err=True)
                raise SystemExit(1)

            create_resp = requests.post(
                f"{api_base.rstrip('/')}/v1/runs",
                json={
                    "project_id": project_id,
                    "entry_cmd": manifest.get("entry_cmd", {}),
                    "env_fingerprint": manifest.get("env_fingerprint", {}),
                },
                headers={"Authorization": f"Bearer {token}"},
                timeout=10,
            )
            create_resp.raise_for_status()
            create_data = create_resp.json()

            # Upload bundle files
            upload_urls = create_data.get("upload_urls", {})
            _upload_bundle(run_dir, upload_urls)

            # Finalize the run
            finalize_resp = requests.post(
                f"{api_base.rstrip('/')}/v1/runs/{run_id}/finalize",
                headers={"Authorization": f"Bearer {token}"},
                timeout=10,
            )
            finalize_resp.raise_for_status()
            click.echo("Bundle uploaded successfully.")

        elif check_resp.status_code != 200:
            click.echo(f"Error checking run: {check_resp.text}", err=True)
            raise SystemExit(1)

    except requests.RequestException as e:
        click.echo(f"Error communicating with server: {e}", err=True)
        raise SystemExit(1)

    # Create share link
    try:
        share_resp = requests.post(
            f"{api_base.rstrip('/')}/v1/runs/{run_id}/share",
            json={"expires_in_days": expires if expires > 0 else None},
            headers={"Authorization": f"Bearer {token}"},
            timeout=10,
        )
        share_resp.raise_for_status()
        share_data = share_resp.json()

        share_url = share_data["share_url"]
        expires_at = share_data.get("expires_at")

        click.echo("")
        click.echo("Share link created!")
        click.echo(f"URL: {share_url}")
        if expires_at:
            click.echo(f"Expires: {expires_at}")
        else:
            click.echo("Expires: Never")

        if open_browser:
            click.echo("\nOpening in browser...")
            webbrowser.open(share_url)

    except requests.RequestException as e:
        click.echo(f"Error creating share link: {e}", err=True)
        raise SystemExit(1)


@cli.command("analyze-failure")
@click.argument("run_path", type=click.Path(exists=True))
@click.option("--output", "-o", type=click.Path(), help="Output file for analysis.")
def analyze_failure_cmd(run_path: str, output: str) -> None:
    """Analyze a failure without generating fixes.

    Categorizes the failure and extracts context for debugging.

    Examples:
        autodebug analyze-failure .autodebug/runs/abc/
    """
    from autodebug.autonomous.analyzer import analyze_failure, format_analysis_report

    run_dir = Path(run_path)

    # Load manifest
    manifest_path = run_dir / "manifest.json"
    manifest = None
    if manifest_path.exists():
        manifest = json.loads(manifest_path.read_text())

    # Find exception
    exception = None
    for record in _read_jsonl_events(run_dir):
        if record.get("type") == "EXCEPTION":
            exception = record
            break

    result_path = run_dir / "result.json"
    if result_path.exists() and not exception:
        result_data = json.loads(result_path.read_text())
        if result_data.get("exception"):
            exception = result_data["exception"]

    if not exception:
        click.echo("Error: No exception found in run", err=True)
        raise SystemExit(1)

    # Load causal slice
    slice_path = run_dir / "slice.json"
    causal_slice = None
    if slice_path.exists():
        causal_slice = json.loads(slice_path.read_text())

    # Analyze
    analysis = analyze_failure(exception, causal_slice, manifest)
    click.echo(format_analysis_report(analysis))

    if output:
        result = {
            "exception_type": analysis.exception_type,
            "exception_message": analysis.exception_message,
            "location": analysis.failure_location,
            "category": analysis.category.name if analysis.category else None,
            "recommended_strategies": analysis.recommended_strategies,
            "context": analysis.context,
        }
        Path(output).write_text(json.dumps(result, indent=2), encoding="utf-8")
        click.echo(f"\nAnalysis saved to {output}")


@cli.command("generate-tests")
@click.argument("source_path", type=click.Path(exists=True))
@click.option("--output", "-o", type=click.Path(), help="Output file for generated tests.")
@click.option("--coverage", default=80, help="Target coverage percentage.")
@click.option("--max-tests", default=5, help="Maximum tests per function.")
@click.option("--strategies", "-s", multiple=True,
              type=click.Choice(["happy_path", "edge_cases", "error_cases", "null_checks", "type_variants", "integration"]),
              help="Test generation strategies to use.")
@click.option("--validate/--no-validate", default=False, help="Run generated tests to validate them.")
@click.option("--function", "-f", "target_function", help="Generate tests only for this function.")
def generate_tests_cmd(
    source_path: str,
    output: str,
    coverage: int,
    max_tests: int,
    strategies: tuple,
    validate: bool,
    target_function: str,
) -> None:
    """Generate tests for a Python file using AI.

    Analyzes the source code structure, identifies testable functions,
    and generates comprehensive pytest-compatible tests.

    Requires ANTHROPIC_API_KEY or OPENAI_API_KEY environment variable.

    Examples:
        autodebug generate-tests ./src/user.py
        autodebug generate-tests ./src/api.py -o tests/test_api.py
        autodebug generate-tests ./src/utils.py --coverage 90 --max-tests 10
        autodebug generate-tests ./src/auth.py -s happy_path -s edge_cases
        autodebug generate-tests ./src/user.py -f create_user
    """
    import os as os_module

    from autodebug.autonomous.test_generator import (
        CostTracker,
        analyze_source_file,
        format_generation_report,
        generate_tests,
        validate_generated_tests,
    )

    # Check for API keys
    has_anthropic = bool(os_module.environ.get("ANTHROPIC_API_KEY"))
    has_openai = bool(os_module.environ.get("OPENAI_API_KEY"))

    if not has_anthropic and not has_openai:
        click.echo("Error: ANTHROPIC_API_KEY or OPENAI_API_KEY environment variable required", err=True)
        raise SystemExit(1)

    source_file = Path(source_path)

    if not source_file.suffix == ".py":
        click.echo(f"Error: {source_path} is not a Python file", err=True)
        raise SystemExit(1)

    # Determine output path
    if output:
        output_path = Path(output)
    else:
        output_path = source_file.parent / f"test_{source_file.name}"

    # Parse strategies
    strategy_list = list(strategies) if strategies else ["happy_path", "edge_cases", "error_cases"]

    click.echo(f"Analyzing {source_path}...")

    # First analyze the file
    analysis = analyze_source_file(source_file)

    # Filter to target function if specified
    if target_function:
        matching = [f for f in analysis.functions if f.name == target_function or f.qualified_name == target_function]
        if not matching:
            click.echo(f"Error: Function '{target_function}' not found", err=True)
            click.echo(f"Available functions: {', '.join(f.name for f in analysis.functions)}", err=True)
            raise SystemExit(1)
        click.echo(f"Targeting function: {target_function}")

    click.echo(f"Found {len(analysis.functions)} functions, {len(analysis.classes)} classes")
    click.echo(f"Total complexity: {analysis.total_complexity}")
    click.echo(f"Strategies: {', '.join(strategy_list)}")
    click.echo("")

    cost_tracker = CostTracker()

    def progress(current: int, total: int, message: str) -> None:
        click.echo(f"\r[{current}/{total}] {message:<60}", nl=False)

    try:
        suite = generate_tests(
            source_file,
            strategies=strategy_list,
            target_coverage=coverage,
            max_tests_per_function=max_tests,
            output_path=output_path,
            cost_tracker=cost_tracker,
            progress_callback=progress,
        )
    except Exception as e:
        click.echo("")
        click.echo(f"Error generating tests: {e}", err=True)
        raise SystemExit(1)

    click.echo("")  # Newline after progress
    click.echo(format_generation_report(suite, cost_tracker))

    # Validate if requested
    if validate:
        click.echo("")
        click.echo("Validating generated tests...")

        results = validate_generated_tests(suite, source_file.parent)

        click.echo(f"Results: {results['passed']} passed, {results['failed']} failed, {results['errors']} errors")

        if results["failed"] > 0 or results["errors"] > 0:
            click.echo("")
            click.echo("Some tests failed. Review the generated file and fix as needed.")
            if results.get("output"):
                click.echo("")
                click.echo("Test output:")
                for line in results["output"].splitlines()[:30]:
                    click.echo(f"  {line}")

    click.echo("")
    click.echo(f"Tests saved to: {output_path}")
    click.echo("")
    click.echo("Next steps:")
    click.echo(f"  1. Review the generated tests: {output_path}")
    click.echo("  2. Run the tests: pytest " + str(output_path))
    click.echo("  3. Adjust fixtures and mocks as needed")


@cli.command("replay-prod")
@click.argument("error_source", type=str)
@click.option("--output", "-o", type=click.Path(), help="Output directory for replay bundle.")
@click.option("--auto-debug/--no-auto-debug", default=False, help="Automatically run auto-debug on the error.")
def replay_prod(error_source: str, output: str, auto_debug: bool) -> None:
    """Replay a production error locally.

    Downloads production error context and converts it to a replay bundle
    that can be used for local debugging.

    ERROR_SOURCE can be:
    - A local file path (./errors/abc123.json.gz)
    - An error ID to fetch from API (abc123-def456-...)

    Examples:
        autodebug replay-prod ./errors/20240115_abc123.json.gz
        autodebug replay-prod abc123-def456 --auto-debug
        autodebug replay-prod abc123-def456 -o ./debug-session/
    """
    from autodebug_probe.prod_mode import convert_to_replay_format, load_prod_error

    error_path = Path(error_source)

    # Check if it's a local file or an error ID
    if error_path.exists():
        click.echo(f"Loading error from local file: {error_source}")
        error_context = load_prod_error(error_source)
    else:
        # Assume it's an error ID, fetch from API
        click.echo(f"Fetching error from API: {error_source}")

        creds = load_credentials()
        api_base = get_api_base()
        token = get_valid_token()

        if not token:
            click.echo("Error: Not logged in. Run 'hikaflow login' first.", err=True)
            raise SystemExit(1)

        try:
            response = requests.get(
                f"{api_base.rstrip('/')}/v1/prod-errors/{error_source}",
                headers={"Authorization": f"Bearer {token}"},
                timeout=30,
            )
            response.raise_for_status()
            error_context = response.json()
        except requests.RequestException as e:
            click.echo(f"Error fetching from API: {e}", err=True)
            raise SystemExit(1)

    # Display error info
    exc = error_context.get("exception", {})
    request = error_context.get("request", {})

    click.echo("")
    click.echo("Production Error Details")
    click.echo("=" * 50)
    click.echo(f"Error ID: {error_context.get('error_id', 'unknown')}")
    click.echo(f"Exception: {exc.get('type', 'Unknown')}: {exc.get('message', '')[:100]}")
    click.echo(f"URL: {request.get('url', 'N/A')}")
    click.echo(f"Method: {request.get('method', 'N/A')}")
    click.echo(f"User: {request.get('user_id', 'N/A')}")
    click.echo(f"Breadcrumbs: {len(request.get('breadcrumbs', []))}")
    click.echo("")

    # Show breadcrumb summary
    breadcrumbs = request.get("breadcrumbs", [])
    if breadcrumbs:
        click.echo("I/O Summary:")
        by_type = {}
        for bc in breadcrumbs:
            t = bc.get("type", "UNKNOWN")
            by_type[t] = by_type.get(t, 0) + 1
        for t, count in sorted(by_type.items()):
            click.echo(f"  {t}: {count}")
        click.echo("")

    # Convert to replay format
    click.echo("Converting to replay format...")
    replay_session = convert_to_replay_format(error_context)

    # Create output directory
    error_id = error_context.get("error_id", str(uuid.uuid4()))
    if output:
        output_dir = Path(output)
    else:
        output_dir = Path(".autodebug") / "prod-errors" / error_id

    output_dir.mkdir(parents=True, exist_ok=True)

    # Write files
    manifest = {
        "run_id": error_id,
        "created_at": datetime.now(timezone.utc).isoformat(),
        "source": "production",
        "original_error": {
            "exception_type": exc.get("type"),
            "exception_message": exc.get("message"),
            "url": request.get("url"),
            "method": request.get("method"),
        },
        "env_fingerprint": error_context.get("environment", {}),
        "tool_versions": {
            "autodebug_cli": __version__,
        },
    }

    (output_dir / "manifest.json").write_text(
        json.dumps(manifest, indent=2), encoding="utf-8"
    )

    # Write events
    events = []
    for entry in replay_session.get("entries", []):
        events.append(json.dumps(entry, sort_keys=True))

    # Add exception event
    events.append(json.dumps({
        "type": "EXCEPTION",
        "exc_type": exc.get("type"),
        "message": exc.get("message"),
        "stack": exc.get("stack", []),
        "timestamp": error_context.get("timestamp"),
    }, sort_keys=True))

    (output_dir / "events.jsonl").write_text("\n".join(events), encoding="utf-8")

    # Write result
    result = {
        "outcome": "FAIL",
        "exception": exc,
    }
    (output_dir / "result.json").write_text(
        json.dumps(result, indent=2), encoding="utf-8"
    )

    click.echo(f"Replay bundle created: {output_dir}")
    click.echo("")

    # Run auto-debug if requested
    if auto_debug:
        click.echo("Running auto-debug on production error...")
        click.echo("")

        # Import and run auto-debug
        import os as os_module
        has_anthropic = bool(os_module.environ.get("ANTHROPIC_API_KEY"))
        has_openai = bool(os_module.environ.get("OPENAI_API_KEY"))

        if not has_anthropic and not has_openai:
            click.echo("Warning: No LLM API key found. Skipping auto-debug.", err=True)
            click.echo("Set ANTHROPIC_API_KEY or OPENAI_API_KEY to enable auto-debug.")
        else:
            from autodebug.autonomous.analyzer import analyze_failure, format_analysis_report
            from autodebug.autonomous.hypothesis import (
                CostTracker,
                format_hypothesis_report,
                generate_hypotheses_with_fallback,
            )

            # Analyze the failure
            exception_record = {
                "exc_type": exc.get("type"),
                "message": exc.get("message"),
                "stack": exc.get("stack", []),
            }

            analysis = analyze_failure(exception_record, None, manifest)
            click.echo(format_analysis_report(analysis))

            # Generate hypotheses
            click.echo("\nGenerating fix hypotheses...")
            cost_tracker = CostTracker()

            hypotheses = generate_hypotheses_with_fallback(
                analysis.context,
                num_hypotheses=3,
                cost_tracker=cost_tracker,
            )

            if hypotheses:
                click.echo(format_hypothesis_report(hypotheses))
                click.echo(f"\nLLM cost: {cost_tracker}")
            else:
                click.echo("No hypotheses generated.")

    click.echo("")
    click.echo("Next steps:")
    click.echo(f"  1. View the error: hikaflow replay debug {output_dir / 'events.jsonl'}")
    click.echo(f"  2. Run auto-debug: hikaflow replay auto {output_dir}")
    click.echo("  3. View in dashboard: Upload to your project")


@cli.command("list-prod-errors")
@click.option("--local", type=click.Path(exists=True), help="List errors from local storage directory.")
@click.option("--limit", default=20, help="Maximum number of errors to show.")
@click.option("--environment", "-e", help="Filter by environment.")
def list_prod_errors(local: str, limit: int, environment: str) -> None:
    """List production errors.

    Shows recent production errors either from local storage or the API.

    Examples:
        autodebug list-prod-errors
        autodebug list-prod-errors --local ./errors/
        autodebug list-prod-errors --environment staging --limit 50
    """
    if local:
        # List from local directory
        local_dir = Path(local)
        files = sorted(local_dir.glob("*.json*"), reverse=True)[:limit]

        if not files:
            click.echo(f"No errors found in {local}")
            return

        click.echo(f"Production Errors ({len(files)} found)")
        click.echo("=" * 70)

        for f in files:
            try:
                from autodebug_probe.prod_mode import load_prod_error
                error = load_prod_error(str(f))
                exc = error.get("exception", {})
                req = error.get("request", {})

                click.echo(f"\n{f.name}")
                click.echo(f"  ID: {error.get('error_id', 'unknown')[:20]}...")
                click.echo(f"  Exception: {exc.get('type', '?')}: {exc.get('message', '')[:50]}")
                click.echo(f"  URL: {req.get('url', 'N/A')[:50]}")
                click.echo(f"  Breadcrumbs: {len(req.get('breadcrumbs', []))}")
            except Exception as e:
                click.echo(f"\n{f.name}")
                click.echo(f"  Error reading: {e}")

    else:
        # List from API
        creds = load_credentials()
        api_base = get_api_base()
        token = get_valid_token()

        if not token:
            click.echo("Error: Not logged in. Run 'hikaflow login' first.", err=True)
            click.echo("Or use --local to list from a local directory.")
            raise SystemExit(1)

        try:
            params = {"limit": limit}
            if environment:
                params["environment"] = environment

            response = requests.get(
                f"{api_base.rstrip('/')}/v1/prod-errors",
                params=params,
                headers={"Authorization": f"Bearer {token}"},
                timeout=30,
            )
            response.raise_for_status()
            data = response.json()

            errors = data.get("errors", [])
            if not errors:
                click.echo("No production errors found.")
                return

            click.echo(f"Production Errors ({len(errors)} shown)")
            click.echo("=" * 70)

            for error in errors:
                exc = error.get("exception", {})
                click.echo(f"\n{error.get('error_id', 'unknown')[:36]}")
                click.echo(f"  Exception: {exc.get('type', '?')}: {exc.get('message', '')[:50]}")
                click.echo(f"  Environment: {error.get('environment', 'N/A')}")
                click.echo(f"  Time: {error.get('timestamp', 'N/A')}")

        except requests.RequestException as e:
            click.echo(f"Error fetching from API: {e}", err=True)
            raise SystemExit(1)


@cli.command("heal")
@click.argument("path", type=click.Path(exists=True))
@click.option("--dry-run/--apply", default=True, help="Preview fixes without applying (default: dry-run).")
@click.option("--validate/--no-validate", default=False, help="Run tests to validate fixes.")
@click.option("--min-confidence", default=0.5, help="Minimum confidence threshold for fixes.")
@click.option("--strategy", type=click.Choice(["template", "llm", "both"]),
              default="both", help="Fix strategy (template-only, llm-only, or both).")
@click.option("--category", "-c", multiple=True,
              type=click.Choice([
                  "async_wait", "concurrency", "order_dependency", "resource_leak",
                  "time_dependent", "randomness", "network", "floating_point", "platform"
              ]),
              help="Only fix patterns in these categories.")
@click.option("--output", "-o", type=click.Path(), help="Output report to file.")
def heal_cmd(
    path: str,
    dry_run: bool,
    validate: bool,
    min_confidence: float,
    strategy: str,
    category: tuple,
    output: str,
) -> None:
    """Self-heal flaky test patterns.

    Detects common flaky test patterns (hardcoded sleeps, race conditions,
    global state, etc.) and automatically applies fixes.

    Supports Python, TypeScript, JavaScript, Go, and Java.

    Examples:
        autodebug heal ./tests                          # Preview fixes
        autodebug heal ./tests --apply                  # Apply fixes
        autodebug heal ./tests/test_api.py --validate   # Validate fixes
        autodebug heal ./tests -c async_wait -c concurrency
        autodebug heal ./tests --strategy llm           # Use LLM for complex fixes
    """
    import os as os_module

    from autodebug.healing import (
        SUPPORTED_LANGUAGES,
        FlakyDetector,
        HealingStrategy,
        TestHealer,
    )
    from autodebug.healing.detector import analyze_flakiness_risk
    from autodebug.healing.healer import generate_fix_report
    from autodebug.healing.patterns import PatternCategory

    target_path = Path(path)

    # Map strategy string to enum
    strategy_map = {
        "template": HealingStrategy.TEMPLATE_ONLY,
        "llm": HealingStrategy.LLM_ONLY,
        "both": HealingStrategy.TEMPLATE_THEN_LLM,
    }
    healing_strategy = strategy_map[strategy]

    # Check for LLM API keys if using LLM strategy
    if strategy in ("llm", "both"):
        has_anthropic = bool(os_module.environ.get("ANTHROPIC_API_KEY"))
        has_openai = bool(os_module.environ.get("OPENAI_API_KEY"))

        if not has_anthropic and not has_openai:
            if strategy == "llm":
                click.echo("Error: ANTHROPIC_API_KEY or OPENAI_API_KEY required for LLM strategy", err=True)
                raise SystemExit(1)
            else:
                click.echo("Note: No LLM API key found. Will only use template-based fixes.")
                healing_strategy = HealingStrategy.TEMPLATE_ONLY

    # Map category strings to enums
    categories = None
    if category:
        category_map = {
            "async_wait": PatternCategory.ASYNC_WAIT,
            "concurrency": PatternCategory.CONCURRENCY,
            "order_dependency": PatternCategory.ORDER_DEPENDENCY,
            "resource_leak": PatternCategory.RESOURCE_LEAK,
            "time_dependent": PatternCategory.TIME_DEPENDENT,
            "randomness": PatternCategory.RANDOMNESS,
            "network": PatternCategory.NETWORK,
            "floating_point": PatternCategory.FLOATING_POINT,
            "platform": PatternCategory.PLATFORM,
        }
        categories = [category_map[c] for c in category]

    # First detect patterns
    click.echo(f"Scanning {path} for flaky patterns...")
    click.echo(f"Languages: {', '.join(SUPPORTED_LANGUAGES)}")
    click.echo("")

    detector = FlakyDetector(
        categories=categories,
        min_confidence=min_confidence,
    )

    if target_path.is_file():
        detection_result = detector.detect_in_file(target_path)
    else:
        detection_result = detector.detect_in_directory(target_path)

    # Deduplicate matches (same file+line+category can match multiple regexes)
    detection_result = detection_result.deduplicate()

    # Show detection summary
    click.echo(f"Scanned {detection_result.files_scanned} files")
    click.echo(f"Found {detection_result.total_matches} potential flaky patterns")

    if detection_result.scan_errors:
        click.echo(f"Errors: {len(detection_result.scan_errors)} files failed to scan")

    if not detection_result.matches:
        click.echo("")
        click.echo("No flaky patterns detected. Your tests look good!")
        return

    # Show risk analysis
    risk = analyze_flakiness_risk(detection_result)
    click.echo("")
    click.echo(f"Risk Level: {risk['risk_level'].upper()} (score: {risk['risk_score']}/100)")
    click.echo(f"High severity issues: {risk['high_severity_issues']}")
    click.echo("")

    # Show matches by category
    click.echo("Patterns by category:")
    for cat_name, count in risk["by_category"].items():
        click.echo(f"  {cat_name}: {count}")
    click.echo("")

    # Show recommendations
    if risk["recommendations"]:
        click.echo("Recommendations:")
        for rec in risk["recommendations"][:3]:
            click.echo(f"  - {rec[:80]}...")
        click.echo("")

    # Show detailed matches
    click.echo("-" * 60)
    click.echo("DETECTED PATTERNS")
    click.echo("-" * 60)

    for i, match in enumerate(detection_result.matches[:20], 1):
        click.echo(f"\n{i}. {match.pattern.name} (severity: {match.pattern.severity}/5)")
        click.echo(f"   File: {match.file_path}:{match.line_number}")
        click.echo(f"   Code: {match.matched_code[:60]}...")
        click.echo(f"   Confidence: {match.confidence:.0%}")

    if len(detection_result.matches) > 20:
        click.echo(f"\n... and {len(detection_result.matches) - 20} more")

    # Ask for confirmation if applying
    if not dry_run:
        click.echo("")
        if not click.confirm(f"Apply fixes to {len(detection_result.matches)} patterns?"):
            click.echo("Aborted.")
            return

    # Apply fixes
    click.echo("")
    click.echo("-" * 60)
    click.echo("APPLYING FIXES" if not dry_run else "PREVIEW FIXES (dry-run)")
    click.echo("-" * 60)

    healer = TestHealer(
        strategy=healing_strategy,
        validate_fixes=validate,
        dry_run=dry_run,
    )

    def progress(current: int, total: int, status: str) -> None:
        click.echo(f"\r[{current}/{total}] {status:<50}", nl=False)

    session = healer.heal_detection_result(detection_result)

    click.echo("")

    # Show results
    report = generate_fix_report(session)
    click.echo(report)

    # Summary
    summary = session.get_summary()
    click.echo("")
    click.echo("-" * 60)
    click.echo("SUMMARY")
    click.echo("-" * 60)
    click.echo(f"Patterns detected: {summary['total_matches']}")
    click.echo(f"Successfully fixed: {summary['successful_fixes']}")
    click.echo(f"Failed to fix: {summary['failed_fixes']}")
    click.echo(f"Success rate: {summary['success_rate']:.1f}%")

    if validate:
        click.echo(f"Validated fixes: {summary['validated_fixes']}")
        click.echo(f"Validation rate: {summary['validation_rate']:.1f}%")

    if summary['total_llm_cost'] > 0:
        click.echo(f"LLM cost: ${summary['total_llm_cost']:.4f}")

    # Save report if output specified
    if output:
        report_data = {
            "path": str(target_path),
            "dry_run": dry_run,
            "strategy": strategy,
            "detection": {
                "files_scanned": detection_result.files_scanned,
                "patterns_found": detection_result.total_matches,
                "risk_level": risk["risk_level"],
                "risk_score": risk["risk_score"],
                "by_category": risk["by_category"],
            },
            "healing": summary,
            "results": [
                {
                    "pattern": r.match.pattern.name,
                    "file": r.match.file_path,
                    "line": r.match.line_number,
                    "success": r.success,
                    "strategy": r.strategy_used.name,
                    "confidence": r.confidence,
                    "explanation": r.explanation,
                    "validated": r.validated,
                    "validation_passed": r.validation_passed,
                }
                for r in session.results
            ],
        }
        Path(output).write_text(json.dumps(report_data, indent=2), encoding="utf-8")
        click.echo(f"\nReport saved to {output}")

    # Next steps
    if dry_run and summary['successful_fixes'] > 0:
        click.echo("")
        click.echo("Next steps:")
        click.echo("  1. Review the fixes above")
        click.echo(f"  2. Apply fixes: hikaflow flaky heal {path} --apply")
        click.echo(f"  3. Run tests to verify: pytest {path}")


@cli.command("detect-flaky")
@click.argument("path", type=click.Path(exists=True))
@click.option("--min-confidence", default=0.5, help="Minimum confidence threshold.")
@click.option("--category", "-c", multiple=True,
              type=click.Choice([
                  "async_wait", "concurrency", "order_dependency", "resource_leak",
                  "time_dependent", "randomness", "network", "floating_point", "platform"
              ]),
              help="Only detect patterns in these categories.")
@click.option("--exclude-category", "-x", multiple=True,
              type=click.Choice([
                  "async_wait", "concurrency", "order_dependency", "resource_leak",
                  "time_dependent", "randomness", "network", "floating_point", "platform"
              ]),
              help="Exclude patterns from these categories.")
@click.option("--top", "-n", type=int, default=0, help="Show only top N patterns (0 = all).")
@click.option("--group-by-file", is_flag=True, help="Group output by file.")
@click.option("--output", "-o", type=click.Path(), help="Output results to JSON file.")
def detect_flaky_cmd(path: str, min_confidence: float, category: tuple,
                     exclude_category: tuple, top: int, group_by_file: bool, output: str) -> None:
    """Detect flaky test patterns without applying fixes.

    Scans test files for common patterns that cause test flakiness,
    such as hardcoded sleeps, race conditions, and global state mutations.

    Examples:
        autodebug detect-flaky ./tests
        autodebug detect-flaky ./tests/test_api.py
        autodebug detect-flaky ./tests -c async_wait -c order_dependency
        autodebug detect-flaky ./tests -x platform  # exclude platform patterns
        autodebug detect-flaky ./tests --top 10     # show only top 10
        autodebug detect-flaky ./tests --group-by-file
        autodebug detect-flaky ./tests -o report.json
    """
    from autodebug.healing import SUPPORTED_LANGUAGES, FlakyDetector
    from autodebug.healing.detector import analyze_flakiness_risk
    from autodebug.healing.patterns import PatternCategory

    target_path = Path(path)

    # Map category strings to enums
    category_map = {
        "async_wait": PatternCategory.ASYNC_WAIT,
        "concurrency": PatternCategory.CONCURRENCY,
        "order_dependency": PatternCategory.ORDER_DEPENDENCY,
        "resource_leak": PatternCategory.RESOURCE_LEAK,
        "time_dependent": PatternCategory.TIME_DEPENDENT,
        "randomness": PatternCategory.RANDOMNESS,
        "network": PatternCategory.NETWORK,
        "floating_point": PatternCategory.FLOATING_POINT,
        "platform": PatternCategory.PLATFORM,
    }

    categories = None
    if category:
        categories = [category_map[c] for c in category]

    # Build exclusion set
    excluded_categories = set()
    if exclude_category:
        excluded_categories = {category_map[c] for c in exclude_category}

    click.echo(f"Scanning {path} for flaky patterns...")
    click.echo(f"Languages: {', '.join(SUPPORTED_LANGUAGES)}")
    if excluded_categories:
        excluded_names = ", ".join(c.name for c in excluded_categories)
        click.echo(f"Excluding: {excluded_names}")
    click.echo("")

    detector = FlakyDetector(
        categories=categories,
        min_confidence=min_confidence,
    )

    if target_path.is_file():
        result = detector.detect_in_file(target_path)
    else:
        result = detector.detect_in_directory(target_path)

    # Deduplicate matches (same file+line+category can match multiple regexes)
    result = result.deduplicate()

    # Filter out excluded categories
    if excluded_categories:
        result.matches = [m for m in result.matches if m.pattern.category not in excluded_categories]
        result._total_matches = len(result.matches)

    # Show results
    click.echo(f"Scanned {result.files_scanned} files")
    click.echo(f"Found {result.total_matches} potential flaky patterns")

    if result.scan_errors:
        click.echo(f"Errors: {len(result.scan_errors)} files failed to scan")

    if not result.matches:
        click.echo("")
        click.echo("No flaky patterns detected!")
        return

    # Risk analysis
    risk = analyze_flakiness_risk(result)
    click.echo("")
    click.echo(f"Risk Level: {risk['risk_level'].upper()} (score: {risk['risk_score']}/100)")

    # Patterns by category
    click.echo("")
    click.echo("Patterns by category:")
    for cat_name, count in sorted(risk["by_category"].items(), key=lambda x: -x[1]):
        click.echo(f"  {cat_name}: {count}")

    # Detailed matches
    click.echo("")
    click.echo("-" * 60)
    click.echo("DETECTED PATTERNS")
    click.echo("-" * 60)

    # Apply --top limit
    matches_to_show = result.matches
    if top > 0:
        # Sort by severity * confidence (highest first)
        matches_to_show = sorted(
            result.matches,
            key=lambda m: m.pattern.severity * m.confidence,
            reverse=True
        )[:top]
        if len(result.matches) > top:
            click.echo(f"(Showing top {top} of {len(result.matches)} patterns)")

    if group_by_file:
        # Group matches by file
        from collections import defaultdict
        by_file = defaultdict(list)
        for match in matches_to_show:
            by_file[match.file_path].append(match)

        for file_path, file_matches in sorted(by_file.items()):
            click.echo(f"\n📄 {file_path}")
            for match in sorted(file_matches, key=lambda m: m.line_number):
                click.echo(f"   L{match.line_number}: [{match.pattern.category.name}] {match.pattern.name}")
                click.echo(f"          Severity: {match.pattern.severity}/5 | {match.matched_code[:50]}...")
    else:
        for i, match in enumerate(matches_to_show, 1):
            click.echo(f"\n{i}. [{match.pattern.category.name}] {match.pattern.name}")
            click.echo(f"   Severity: {match.pattern.severity}/5 | Confidence: {match.confidence:.0%}")
            click.echo(f"   File: {match.file_path}:{match.line_number}")
            click.echo(f"   Code: {match.matched_code[:70]}...")
            click.echo(f"   Why: {match.pattern.description}")

    # Recommendations
    if risk.get("recommendations"):
        click.echo("")
        click.echo("-" * 60)
        click.echo("RECOMMENDATIONS")
        click.echo("-" * 60)
        for rec in risk["recommendations"]:
            click.echo(f"  - {rec}")

    # Save to file
    if output:
        output_data = {
            "path": str(target_path),
            "files_scanned": result.files_scanned,
            "total_matches": result.total_matches,
            "risk_level": risk["risk_level"],
            "risk_score": risk["risk_score"],
            "by_category": risk["by_category"],
            "recommendations": risk["recommendations"],
            "matches": [
                {
                    "pattern_id": m.pattern.id,
                    "pattern_name": m.pattern.name,
                    "category": m.pattern.category.name,
                    "severity": m.pattern.severity,
                    "confidence": m.confidence,
                    "file": m.file_path,
                    "line": m.line_number,
                    "column": m.column,
                    "matched_code": m.matched_code,
                    "description": m.pattern.description,
                    "fix_explanation": m.pattern.fix_explanation,
                }
                for m in result.matches
            ],
        }
        Path(output).write_text(json.dumps(output_data, indent=2), encoding="utf-8")
        click.echo(f"\nResults saved to {output}")

    # Next steps
    click.echo("")
    click.echo("Next steps:")
    click.echo(f"  1. Fix automatically: hikaflow flaky heal {path}")
    click.echo(f"  2. Preview fixes: hikaflow flaky heal {path} --dry-run")


# =============================================================================
# Pattern Database Commands
# =============================================================================

@cli.group("patterns")
def patterns_group() -> None:
    """Manage the fix pattern database.

    The pattern database learns from proven fixes and suggests solutions
    for similar failures. Patterns are anonymized for privacy.

    Examples:
        autodebug patterns list
        autodebug patterns stats
        autodebug patterns match .autodebug/runs/abc/
        autodebug patterns export patterns.json
    """


@patterns_group.command("list")
@click.option("--limit", default=20, help="Maximum patterns to show.")
@click.option("--order", type=click.Choice(["success", "uses", "recent"]),
              default="success", help="Sort order.")
@click.option("--fix-type", help="Filter by fix type.")
def patterns_list(limit: int, order: str, fix_type: str) -> None:
    """List stored patterns.

    Examples:
        autodebug patterns list
        autodebug patterns list --limit 50 --order uses
        autodebug patterns list --fix-type add_await
    """
    from autodebug.patterns import get_pattern_store

    store = get_pattern_store()

    order_map = {
        "success": "success_count",
        "uses": "total_uses",
        "recent": "updated_at",
    }

    if fix_type:
        patterns = store.find_by_fix_type(fix_type, limit=limit)
    else:
        patterns = store.list_patterns(limit=limit, order_by=order_map[order])

    if not patterns:
        click.echo("No patterns stored yet.")
        click.echo("")
        click.echo("Patterns are created when fixes are validated.")
        click.echo("Run: hikaflow replay auto <run> --harness <harness>")
        return

    click.echo(f"Stored Patterns ({len(patterns)} shown)")
    click.echo("=" * 70)

    for p in patterns:
        success_total = p.success_count + p.failure_count
        rate = p.success_rate * 100 if success_total > 0 else 0

        click.echo(f"\n{p.fix_type.upper()}")
        click.echo(f"  ID: {p.id[:12]}...")
        click.echo(f"  Exception: {p.exception_signature.exception_type}")
        click.echo(f"  Success: {p.success_count}/{success_total} ({rate:.0f}%)")
        click.echo(f"  Uses: {p.total_uses}")
        click.echo(f"  Description: {p.fix_description[:60]}...")
        if p.tags:
            click.echo(f"  Tags: {', '.join(p.tags)}")


@patterns_group.command("stats")
def patterns_stats() -> None:
    """Show pattern database statistics.

    Examples:
        autodebug patterns stats
    """
    from autodebug.patterns import get_pattern_store

    store = get_pattern_store()
    stats = store.get_stats()

    click.echo("Pattern Database Statistics")
    click.echo("=" * 50)
    click.echo("")
    click.echo(f"Total patterns: {stats.get('total_patterns')}")
    click.echo(f"Total uses: {stats.get('total_uses')}")
    click.echo(f"Success rate: {stats.get('overall_success_rate'):.1%}")
    click.echo("")

    if stats.get("top_exception_types"):
        click.echo("Top Exception Types:")
        for item in stats.get("top_exception_types")[:5]:
            click.echo(f"  {item.get('type')}: {item.get('count')}")
        click.echo("")

    if stats.get("top_fix_types"):
        click.echo("Top Fix Types:")
        for item in stats.get("top_fix_types")[:5]:
            click.echo(f"  {item.get('type')}: {item.get('count')}")
        click.echo("")

    if stats.get("by_source"):
        click.echo("By Source:")
        for source, count in stats.get("by_source").items():
            click.echo(f"  {source}: {count}")


@patterns_group.command("match")
@click.argument("run_path", type=click.Path(exists=True))
@click.option("--min-similarity", default=0.5, help="Minimum similarity threshold.")
@click.option("--max-results", default=5, help="Maximum matches to show.")
def patterns_match(run_path: str, min_similarity: float, max_results: int) -> None:
    """Find matching patterns for a failure.

    Examples:
        autodebug patterns match .autodebug/runs/abc/
        autodebug patterns match ./run --min-similarity 0.7
    """
    from autodebug.patterns import find_similar_patterns, format_match_report

    run_dir = Path(run_path)

    # Find exception in the run
    exception = None

    for record in _read_jsonl_events(run_dir):
        if record.get("type") == "EXCEPTION":
            exception = record
            break

    result_path = run_dir / "result.json"
    if result_path.exists() and not exception:
        result_data = json.loads(result_path.read_text())
        if result_data.get("exception"):
            exception = result_data["exception"]

    if not exception:
        click.echo("Error: No exception found in run", err=True)
        raise SystemExit(1)

    exc_type = exception.get("exc_type") or exception.get("type", "Unknown")
    message = exception.get("message", "")

    click.echo(f"Finding patterns for: {exc_type}")
    click.echo(f"Message: {message[:60]}...")
    click.echo("")

    matches = find_similar_patterns(
        exception_data=exception,
        min_similarity=min_similarity,
        max_results=max_results,
    )

    if not matches:
        click.echo("No matching patterns found.")
        click.echo("")
        click.echo("This could mean:")
        click.echo("  1. No similar failures have been fixed before")
        click.echo("  2. The pattern database is empty")
        click.echo("  3. Similarity threshold is too high")
        return

    click.echo(format_match_report(matches))

    # Offer to apply the best match
    best = matches[0]
    if best.suggested_fix:
        click.echo("")
        click.echo("Suggested fix from best match:")
        click.echo("-" * 40)
        for line in best.suggested_fix.splitlines():
            click.echo(f"  {line}")


@patterns_group.command("add")
@click.argument("run_path", type=click.Path(exists=True))
@click.option("--fix-diff", "-d", type=click.Path(exists=True), help="Path to fix diff file.")
@click.option("--fix-description", "-m", required=True, help="Description of the fix.")
@click.option("--fix-type", "-t", help="Type of fix (auto-detected if not provided).")
@click.option("--tag", multiple=True, help="Tags for the pattern.")
def patterns_add(
    run_path: str,
    fix_diff: str,
    fix_description: str,
    fix_type: str,
    tag: tuple,
) -> None:
    """Add a pattern from a proven fix.

    Examples:
        autodebug patterns add .autodebug/runs/abc/ -d fix.patch -m "Add null check"
        autodebug patterns add ./run -d fix.diff -m "Add retry logic" -t flaky -t network
    """
    from autodebug.patterns import extract_pattern_from_fix, get_pattern_store

    run_dir = Path(run_path)

    # Find exception
    exception = None
    for record in _read_jsonl_events(run_dir):
        if record.get("type") == "EXCEPTION":
            exception = record
            break

    result_path = run_dir / "result.json"
    if result_path.exists() and not exception:
        result_data = json.loads(result_path.read_text())
        if result_data.get("exception"):
            exception = result_data["exception"]

    if not exception:
        click.echo("Error: No exception found in run", err=True)
        raise SystemExit(1)

    # Read diff
    if fix_diff:
        diff_content = Path(fix_diff).read_text()
    else:
        click.echo("Error: --fix-diff is required", err=True)
        raise SystemExit(1)

    # Extract pattern
    pattern = extract_pattern_from_fix(
        exception_data=exception,
        fix_diff=diff_content,
        fix_description=fix_description,
        fix_type=fix_type,
        tags=list(tag) if tag else None,
    )

    # Store pattern
    store = get_pattern_store()
    pattern_id = store.add_pattern(pattern)

    click.echo("Pattern added successfully!")
    click.echo(f"  ID: {pattern_id}")
    click.echo(f"  Fix Type: {pattern.fix_type}")
    click.echo(f"  Exception: {pattern.exception_signature.exception_type}")


@patterns_group.command("export")
@click.argument("output_path", type=click.Path())
def patterns_export(output_path: str) -> None:
    """Export patterns to a JSON file.

    Examples:
        autodebug patterns export patterns.json
        autodebug patterns export ~/backups/patterns.json
    """
    from autodebug.patterns import get_pattern_store

    store = get_pattern_store()
    count = store.export_patterns(Path(output_path))

    click.echo(f"Exported {count} patterns to {output_path}")


@patterns_group.command("import")
@click.argument("input_path", type=click.Path(exists=True))
@click.option("--source", default="imported", help="Source label for imported patterns.")
def patterns_import(input_path: str, source: str) -> None:
    """Import patterns from a JSON file.

    Examples:
        autodebug patterns import patterns.json
        autodebug patterns import community-patterns.json --source community
    """
    from autodebug.patterns import get_pattern_store

    store = get_pattern_store()
    imported, skipped = store.import_patterns(Path(input_path), source=source)

    click.echo(f"Imported {imported} patterns ({skipped} skipped)")


@patterns_group.command("delete")
@click.argument("pattern_id")
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation.")
def patterns_delete(pattern_id: str, yes: bool) -> None:
    """Delete a pattern.

    Examples:
        autodebug patterns delete abc123
        autodebug patterns delete abc123 -y
    """
    from autodebug.patterns import get_pattern_store

    store = get_pattern_store()
    pattern = store.get_pattern(pattern_id)

    if not pattern:
        click.echo(f"Pattern not found: {pattern_id}", err=True)
        raise SystemExit(1)

    if not yes:
        click.echo(f"Pattern: {pattern.fix_type}")
        click.echo(f"Description: {pattern.fix_description}")
        if not click.confirm("Delete this pattern?"):
            click.echo("Aborted.")
            return

    if store.delete_pattern(pattern_id):
        click.echo(f"Pattern deleted: {pattern_id}")
    else:
        click.echo("Failed to delete pattern", err=True)


@patterns_group.command("contribute")
@click.argument("run_path", type=click.Path(exists=True))
@click.option("--fix-diff", "-d", type=click.Path(exists=True), required=True,
              help="Path to fix diff file.")
@click.option("--fix-description", "-m", required=True, help="Description of the fix.")
def patterns_contribute(run_path: str, fix_diff: str, fix_description: str) -> None:
    """Contribute a pattern to the community database.

    Patterns are anonymized before upload. No code, variable names, or
    identifying information is shared - only structural patterns.

    Examples:
        autodebug patterns contribute .autodebug/runs/abc/ -d fix.patch -m "Add retry"
    """
    from autodebug.patterns import extract_pattern_from_fix, get_pattern_store

    run_dir = Path(run_path)

    # Find exception
    exception = None
    for record in _read_jsonl_events(run_dir):
        if record.get("type") == "EXCEPTION":
            exception = record
            break

    result_path = run_dir / "result.json"
    if result_path.exists() and not exception:
        result_data = json.loads(result_path.read_text())
        if result_data.get("exception"):
            exception = result_data["exception"]

    if not exception:
        click.echo("Error: No exception found in run", err=True)
        raise SystemExit(1)

    diff_content = Path(fix_diff).read_text()

    # Extract and anonymize pattern
    pattern = extract_pattern_from_fix(
        exception_data=exception,
        fix_diff=diff_content,
        fix_description=fix_description,
        tags=["community"],
    )
    pattern.source = "community"

    # Show what will be shared
    click.echo("Pattern to contribute (anonymized):")
    click.echo("-" * 50)
    click.echo(f"Exception Type: {pattern.exception_signature.exception_type}")
    click.echo(f"Message Pattern: {pattern.exception_signature.message_pattern}")
    click.echo(f"Fix Type: {pattern.fix_type}")
    click.echo(f"Description: {pattern.fix_description}")
    click.echo("")
    click.echo("Note: No actual code is shared, only structural patterns.")
    click.echo("")

    # Check for API credentials
    creds = load_credentials()
    api_base = get_api_base()
    token = get_valid_token()

    if not token:
        click.echo("Error: Not logged in. Run 'hikaflow login' first.", err=True)
        raise SystemExit(1)

    if not click.confirm("Contribute this pattern?"):
        click.echo("Aborted.")
        return

    # Upload to API
    try:
        response = requests.post(
            f"{api_base.rstrip('/')}/v1/patterns",
            json=pattern.to_dict(),
            headers={"Authorization": f"Bearer {token}"},
            timeout=30,
        )
        response.raise_for_status()
        result = response.json()

        click.echo("")
        click.echo("Pattern contributed successfully!")
        click.echo(f"Pattern ID: {result.get('pattern_id', pattern.id)}")
        click.echo("")
        click.echo("Thank you for helping improve the community database!")

    except requests.RequestException as e:
        click.echo(f"Error uploading pattern: {e}", err=True)
        click.echo("")
        click.echo("Pattern saved locally instead.")

        # Save locally as fallback
        store = get_pattern_store()
        store.add_pattern(pattern)
        click.echo(f"Local pattern ID: {pattern.id}")


# ============================================================================
# Session Replay CLI
# ============================================================================

@cli.group("session")
def session_group() -> None:
    """Session replay commands."""


@session_group.command("create")
@click.option("--project-id", required=True, help="Project UUID")
@click.option("--run-id", required=False, help="Optional run ID to derive trace_id")
@click.option("--trace-id", required=False, help="Optional trace ID to link sessions")
@click.option("--metadata", required=False, help="JSON metadata string")
def session_create(project_id: str, run_id: Optional[str], trace_id: Optional[str], metadata: Optional[str]) -> None:
    """Create a new session replay and return an initial upload URL."""
    session = _create_api_session()
    meta: Dict[str, Any] = {}
    if metadata:
        try:
            meta = json.loads(metadata)
        except json.JSONDecodeError as e:
            raise SystemExit(f"Invalid metadata JSON: {e}") from e

    if run_id and not trace_id:
        trace_id = _get_run_trace_id(run_id)

    payload = {"project_id": project_id, "trace_id": trace_id, "metadata": meta}
    resp = session.post(f"{_api_base()}/v1/sessions", json=payload, headers=_api_headers(), timeout=30)
    resp.raise_for_status()
    data = resp.json()
    click.echo(json.dumps(data, indent=2))


@session_group.command("chunk")
@click.option("--session-id", required=True, help="Session UUID")
@click.option("--stream", required=True, help="Stream type: dom, console, network, errors, perf")
@click.option("--chunk-id", required=True, help="Chunk ID to register")
@click.option("--file", "file_path", required=False, type=click.Path(exists=True), help="Optional file to upload")
def session_chunk(session_id: str, stream: str, chunk_id: str, file_path: Optional[str]) -> None:
    """Register a session chunk and optionally upload a file to the signed URL."""
    session = _create_api_session()
    payload = {"stream": stream, "chunk_id": chunk_id}
    resp = session.post(
        f"{_api_base()}/v1/sessions/{session_id}/chunks",
        json=payload,
        headers=_api_headers(),
        timeout=30,
    )
    resp.raise_for_status()
    data = resp.json()
    upload_url = data.get("upload_url")
    if file_path and upload_url:
        with open(file_path, "rb") as f:
            upload_resp = session.put(upload_url, data=f, headers={"Content-Type": "application/octet-stream"}, timeout=60)
            upload_resp.raise_for_status()
        click.echo("Uploaded chunk successfully.")
    click.echo(json.dumps(data, indent=2))


@session_group.command("finalize")
@click.option("--session-id", required=True, help="Session UUID")
def session_finalize(session_id: str) -> None:
    """Finalize a session replay and enqueue processing."""
    session = _create_api_session()
    resp = session.post(
        f"{_api_base()}/v1/sessions/{session_id}/finalize",
        headers=_api_headers(),
        timeout=30,
    )
    resp.raise_for_status()
    click.echo(json.dumps(resp.json(), indent=2))


@session_group.command("upload")
@click.option("--project-id", required=True, help="Project UUID")
@click.option("--file", "file_path", required=True, type=click.Path(exists=True), help="JSONL or .jsonl.zst file")
@click.option("--stream", default="dom", help="Stream type: dom, console, network, errors, perf")
@click.option("--run-id", required=False, help="Optional run ID to derive trace_id")
@click.option("--trace-id", required=False, help="Optional trace ID")
@click.option("--metadata", required=False, help="JSON metadata string")
def session_upload(project_id: str, file_path: str, stream: str, run_id: Optional[str], trace_id: Optional[str], metadata: Optional[str]) -> None:
    """Create a session, upload a chunk, and finalize."""
    session = _create_api_session()
    meta: Dict[str, Any] = {}
    if metadata:
        try:
            meta = json.loads(metadata)
        except json.JSONDecodeError as e:
            raise SystemExit(f"Invalid metadata JSON: {e}") from e

    if run_id and not trace_id:
        trace_id = _get_run_trace_id(run_id)

    create_resp = session.post(
        f"{_api_base()}/v1/sessions",
        json={"project_id": project_id, "trace_id": trace_id, "metadata": meta},
        headers=_api_headers(),
        timeout=30,
    )
    create_resp.raise_for_status()
    data = create_resp.json()
    session_id = data.get("session_id")

    if stream == "dom":
        upload_url = data.get("upload_url")
    else:
        chunk_id = str(uuid.uuid4())
        chunk_resp = session.post(
            f"{_api_base()}/v1/sessions/{session_id}/chunks",
            json={"stream": stream, "chunk_id": chunk_id},
            headers=_api_headers(),
            timeout=30,
        )
        chunk_resp.raise_for_status()
        upload_url = chunk_resp.json()["upload_url"]

    with open(file_path, "rb") as f:
        upload_resp = session.put(upload_url, data=f, headers={"Content-Type": "application/octet-stream"}, timeout=120)
        upload_resp.raise_for_status()

    finalize_resp = session.post(
        f"{_api_base()}/v1/sessions/{session_id}/finalize",
        headers=_api_headers(),
        timeout=30,
    )
    finalize_resp.raise_for_status()
    click.echo(json.dumps({"session_id": session_id, "status": "uploaded"}, indent=2))


@session_group.command("record")
@click.option("--url", required=True, help="URL to record")
@click.option("--project-id", required=True, help="Project UUID")
@click.option("--duration", default=60, type=int, help="Recording duration in seconds")
@click.option("--headed/--headless", default=False, help="Show browser UI")
@click.option("--chunk-interval", default=10, type=int, help="Seconds between chunk uploads")
def session_record(url: str, project_id: str, duration: int, headed: bool, chunk_interval: int) -> None:
    """Record a browser session with rrweb and upload to Hikaflow.

    Deprecated: use `hikaflow session record` (v2 CLI) instead.
    """
    click.echo("WARNING: v1 CLI is deprecated. Use `hikaflow session record` instead.", err=True)

    try:
        from playwright.sync_api import sync_playwright
    except ImportError:
        raise SystemExit(
            "Playwright not installed. Run: pip install 'autodebug[recording]' && playwright install chromium"
        ) from None

    import time as _time

    session = _create_api_session()
    api = _api_base()
    hdrs = _api_headers()

    # Create session
    create_resp = session.post(
        f"{api}/v1/sessions",
        json={"project_id": project_id, "metadata": {"source": "cli-record-v1", "url": url}},
        headers=hdrs,
        timeout=30,
    )
    create_resp.raise_for_status()
    session_data = create_resp.json()
    session_id = session_data["session_id"]
    click.echo(f"Session created: {session_id}")

    RRWEB_INIT_SCRIPT = """
    (function() {
        if (window.__hikaflow_rrweb_initialized) return;
        var s = document.createElement('script');
        s.src = 'https://cdn.jsdelivr.net/npm/rrweb@2.0.0-alpha.13/dist/rrweb-all.umd.cjs';
        s.onload = function() {
            window.__hikaflow_events = [];
            rrweb.record({
                emit: function(event) {
                    window.__hikaflow_events.push(event);
                }
            });
            window.__hikaflow_rrweb_initialized = true;
        };
        document.head.appendChild(s);
    })();
    """

    def _drain(pg):
        return pg.evaluate("() => { var e = window.__hikaflow_events || []; window.__hikaflow_events = []; return e; }")

    def _upload(sid, stream, events, cnum):
        if not events:
            return
        jsonl = "\n".join(json.dumps(e) for e in events).encode("utf-8")
        try:
            import zstandard as zstd
            compressed = zstd.ZstdCompressor().compress(jsonl)
        except ImportError:
            compressed = jsonl
        cid = str(uuid.uuid4())
        cr = session.post(f"{api}/v1/sessions/{sid}/chunks", json={"stream": stream, "chunk_id": cid}, headers=hdrs, timeout=30)
        cr.raise_for_status()
        session.put(cr.json()["upload_url"], data=compressed, headers={"Content-Type": "application/octet-stream"}, timeout=60).raise_for_status()
        click.echo(f"  Chunk {cnum}: {len(events)} events ({len(compressed)} bytes)")

    with sync_playwright() as p:
        browser = p.chromium.launch(headless=not headed)
        page = browser.new_page()
        page.on("load", lambda _: page.evaluate(RRWEB_INIT_SCRIPT))
        page.goto(url, wait_until="load")
        page.evaluate(RRWEB_INIT_SCRIPT)

        chunk_num = 0
        start = _time.time()
        while _time.time() - start < duration:
            page.wait_for_timeout(min(chunk_interval, duration - (_time.time() - start)) * 1000)
            chunk_num += 1
            _upload(session_id, "dom", _drain(page), chunk_num)

        remaining = _drain(page)
        if remaining:
            _upload(session_id, "dom", remaining, chunk_num + 1)
        browser.close()

    session.post(f"{api}/v1/sessions/{session_id}/finalize", headers=hdrs, timeout=30).raise_for_status()
    click.echo(json.dumps({"session_id": session_id, "status": "recorded"}, indent=2))


@session_group.command("open")
@click.option("--session-id", required=True, help="Session UUID")
@click.option("--local-dir", required=False, type=click.Path(exists=True), help="Open a local replay from a directory")
@click.option("--port", default=8000, type=int, help="Local server port for replay")
def session_open(session_id: str, local_dir: Optional[str], port: int) -> None:
    """Open a session replay in the web dashboard or locally."""
    import webbrowser
    if local_dir:
        directory = Path(local_dir)
        html_path = _prepare_local_replay(directory)
        click.echo(f"Prepared local replay at {html_path}")
        click.echo(f"Starting local server on http://localhost:{port}/replay.html")
        import subprocess
        server = subprocess.Popen(
            [sys.executable, "-m", "http.server", str(port)],
            cwd=str(directory),
        )
        webbrowser.open(f"http://localhost:{port}/replay.html")
        click.echo("Press Ctrl+C to stop the server.")
        try:
            server.wait()
        except KeyboardInterrupt:
            server.terminate()
        return

    web_base = os.environ.get("HIKAFLOW_WEB_URL") or os.environ.get("WEB_URL") or ""
    if not web_base:
        raise SystemExit("Set HIKAFLOW_WEB_URL to open session in browser.")
    url = f"{web_base.rstrip('/')}/dashboard/sessions/{session_id}"
    webbrowser.open(url)
    click.echo(f"Opened {url}")


@session_group.command("share")
@click.option("--session-id", required=True, help="Session UUID")
@click.option("--expires", default=7, help="Days until expiration (default: 7)")
@click.option("--open", "open_browser", is_flag=True, help="Open share link in browser.")
def session_share(session_id: str, expires: int, open_browser: bool) -> None:
    """Create a shareable link for a session replay."""
    session = _create_api_session()
    payload = {"expires_in_days": expires}
    resp = session.post(
        f"{_api_base()}/v1/sessions/{session_id}/share",
        json=payload,
        headers=_api_headers(),
        timeout=30,
    )
    resp.raise_for_status()
    data = resp.json()
    share_url = data.get("share_url")
    click.echo(json.dumps(data, indent=2))
    if share_url and open_browser:
        import webbrowser
        webbrowser.open(share_url)


@session_group.command("download")
@click.option("--session-id", required=True, help="Session UUID")
@click.option("--output", "-o", required=True, type=click.Path(), help="Output directory")
def session_download(session_id: str, output: str) -> None:
    """Download session chunks to a local directory."""
    session = _create_api_session()
    resp = session.get(f"{_api_base()}/v1/sessions/{session_id}/chunks", headers=_api_headers(), timeout=30)
    resp.raise_for_status()
    data = resp.json()
    chunks = data.get("chunks", [])
    out_dir = Path(output)
    out_dir.mkdir(parents=True, exist_ok=True)
    for chunk in chunks:
        url = chunk.get("download_url")
        if not url:
            continue
        stream = chunk.get("stream") or "unknown"
        chunk_id = chunk.get("chunk_id") or "chunk"
        path = out_dir / f"{stream}-{chunk_id}.jsonl.zst"
        download = session.get(url, timeout=60)
        download.raise_for_status()
        path.write_bytes(download.content)
    click.echo(f"Downloaded {len(chunks)} chunks to {out_dir}")


@cli.group("issues")
def issues_group() -> None:
    """Issue grouping commands."""


@issues_group.command("list")
def issues_list() -> None:
    """List issue groups."""
    session = _create_api_session()
    resp = session.get(f"{_api_base()}/v1/issue-groups", headers=_api_headers(), timeout=30)
    resp.raise_for_status()
    click.echo(json.dumps(resp.json(), indent=2))


@issues_group.command("group")
@click.argument("error_id")
def issues_group_error(error_id: str) -> None:
    """Create a group for an error and attach similar errors."""
    session = _create_api_session()
    resp = session.post(
        f"{_api_base()}/v1/prod-errors/{error_id}/group",
        headers=_api_headers(),
        timeout=30,
    )
    resp.raise_for_status()
    click.echo(json.dumps(resp.json(), indent=2))


@issues_group.command("delete")
@click.argument("group_id")
def issues_delete_group(group_id: str) -> None:
    """Delete an issue group."""
    session = _create_api_session()
    resp = session.delete(
        f"{_api_base()}/v1/issue-groups/{group_id}",
        headers=_api_headers(),
        timeout=30,
    )
    resp.raise_for_status()
    click.echo(json.dumps(resp.json(), indent=2))


@issues_group.command("merge")
@click.argument("group_id")
@click.argument("target_group_id")
def issues_merge_group(group_id: str, target_group_id: str) -> None:
    """Merge one issue group into another."""
    session = _create_api_session()
    resp = session.post(
        f"{_api_base()}/v1/issue-groups/{group_id}/merge",
        params={"target_group_id": target_group_id},
        headers=_api_headers(),
        timeout=30,
    )
    resp.raise_for_status()
    click.echo(json.dumps(resp.json(), indent=2))
