"""
SigmanticAI CLI — thin client that connects to the backend via WebSocket.

Commands:
  sigmanticai                  → Interactive REPL
  sigmanticai login            → Browser-based login
  sigmanticai logout           → Clear stored credentials
  sigmanticai whoami           → Show current user
  sigmanticai status           → Show plan, quota, recent jobs
  sigmanticai jobs             → List recent generation jobs
  sigmanticai generate "..."   → One-shot generation (non-interactive)
"""

from __future__ import annotations

import argparse
import asyncio
import os
import sys
from pathlib import Path

from rich.console import Console

console = Console()


def main():
    """Entry point for the `sigmanticai` command."""
    if sys.platform == "win32" and hasattr(sys.stdout, "reconfigure"):
        try:
            sys.stdout.reconfigure(encoding="utf-8", errors="replace")
        except Exception:
            pass
    from sigmanticai import __version__

    parser = argparse.ArgumentParser(
        prog="sigmanticai",
        description="SigmanticAI — AI-powered hardware verification",
    )
    parser.add_argument(
        "-V", "--version", action="version", version=f"sigmanticai {__version__}"
    )
    subparsers = parser.add_subparsers(dest="command")

    # ── login ─────────────────────────────────────────────────
    login_parser = subparsers.add_parser("login", help="Sign in via browser (or --email/--password for headless)")
    login_parser.add_argument("--email", type=str, help="Email for headless login (no browser)")
    login_parser.add_argument("--password", type=str, help="Password for headless login")
    login_parser.add_argument("--google", action="store_true", help="Login via email OTP (for Google-auth accounts in headless mode)")

    # ── logout ────────────────────────────────────────────────
    subparsers.add_parser("logout", help="Sign out and clear credentials")

    # ── whoami ────────────────────────────────────────────────
    subparsers.add_parser("whoami", help="Show current logged-in user")

    # ── status ────────────────────────────────────────────────
    subparsers.add_parser("status", help="Show plan, quota, and account info")

    # ── jobs ──────────────────────────────────────────────────
    subparsers.add_parser("jobs", help="List recent generation jobs")

    # ── generate ──────────────────────────────────────────────
    gen_parser = subparsers.add_parser("generate", help="One-shot generation")
    gen_parser.add_argument("prompt", help="Design description")
    gen_parser.add_argument(
        "--rtl", type=str, help="Path to existing RTL file(s)"
    )
    gen_parser.add_argument(
        "-o", "--output", type=str, default=".", help="Output directory"
    )

    args = parser.parse_args()

    if args.command == "login":
        if args.google:
            _cmd_login_otp(email=args.email)
        else:
            _cmd_login(email=args.email, password=args.password)
    elif args.command == "logout":
        _cmd_logout()
    elif args.command == "whoami":
        _cmd_whoami()
    elif args.command == "status":
        asyncio.run(_cmd_status())
    elif args.command == "jobs":
        asyncio.run(_cmd_jobs())
    elif args.command == "generate":
        asyncio.run(_cmd_generate(args.prompt, args.rtl, args.output))
    else:
        # No subcommand → interactive REPL
        asyncio.run(_cmd_interactive())


# ── Commands ──────────────────────────────────────────────────

def _cmd_login(email: str | None = None, password: str | None = None):
    """Browser-based login, or headless with --email/--password."""
    if email and password:
        # Headless login via Supabase REST API (no browser, no SDK)
        from sigmanticai.auth import headless_login
        creds = headless_login(email, password)
    elif email or password:
        console.print("[red]Both --email and --password are required for headless login.[/red]")
        sys.exit(1)
    else:
        from sigmanticai.auth import browser_login
        creds = browser_login()

    if creds:
        console.print("[green]✓ Logged in successfully![/green]")
    else:
        console.print("[red]✗ Login failed or timed out.[/red]")
        sys.exit(1)


def _cmd_login_otp(email: str | None = None):
    """Login via email OTP — for Google-auth accounts in headless/SSH environments."""
    from sigmanticai.auth import AuthClient

    if not email:
        email = input("Enter the email you used with Google sign-in: ").strip()
        if not email:
            console.print("[red]Email is required.[/red]")
            sys.exit(1)

    auth = AuthClient()

    console.print(f"[cyan]Sending a verification code to {email}...[/cyan]")
    try:
        auth.send_otp(email)
    except Exception as e:
        console.print(f"[red]Failed to send code: {e}[/red]")
        sys.exit(1)

    console.print("[green]Check your email for a 6-digit verification code.[/green]")
    code = input("Enter the code: ").strip()
    if not code:
        console.print("[red]Code is required.[/red]")
        sys.exit(1)

    try:
        creds = auth.verify_otp(email, code)
    except Exception as e:
        console.print(f"[red]Verification failed: {e}[/red]")
        sys.exit(1)

    if creds:
        console.print("[green]✓ Logged in successfully![/green]")
    else:
        console.print("[red]✗ Login failed.[/red]")
        sys.exit(1)


def _cmd_logout():
    """Clear stored credentials."""
    from sigmanticai.config import clear_credentials

    clear_credentials()
    console.print("[yellow]Logged out.[/yellow]")


def _cmd_whoami():
    """Show current user — verified via gateway API."""
    from sigmanticai.auth import get_access_token, AuthClient

    token = get_access_token()
    if not token:
        console.print("[yellow]Not logged in. Run: sigmanticai login[/yellow]")
        return

    try:
        auth = AuthClient()
        user = auth.verify_token(token)
        name = user.get("name") or user.get("email", "unknown")
        console.print(f"[green]✓ Logged in as:[/green] {name} ({user.get('email', '')})")
    except Exception:
        console.print("[yellow]Session expired or API unreachable. Run: sigmanticai login[/yellow]")


async def _cmd_status():
    """Show plan, quota, and account info — all via gateway API."""
    from sigmanticai.auth import get_access_token, AuthClient

    token = get_access_token()
    if not token:
        console.print("[yellow]Not logged in. Run: sigmanticai login[/yellow]")
        return

    try:
        auth = AuthClient()
        user = auth.verify_token(token)
        quota = auth.check_quota(token)

        from rich.table import Table

        table = Table(title="SigmanticAI Account", border_style="cyan")
        table.add_column("Field", style="bold")
        table.add_column("Value")
        table.add_row("Email", user.get("email", "unknown"))
        table.add_row("Name", user.get("name", ""))
        table.add_row("Plan", quota.get("plan", "free"))
        table.add_row("Generations Used", f"{quota['used']}/{quota['max']}")
        table.add_row("Remaining", str(quota.get("remaining", 0)))
        console.print(table)

    except Exception as e:
        console.print(f"[red]Could not fetch status: {e}[/red]")
        console.print("[dim]Is the API running? Check SIGMANTICAI_API_URL[/dim]")


async def _cmd_jobs():
    """List recent generation jobs — via gateway API.

    NOTE: Job listing requires a /v1/auth/jobs endpoint on the gateway.
    If not available, this will show an appropriate message.
    """
    from sigmanticai.auth import get_access_token

    token = get_access_token()
    if not token:
        console.print("[yellow]Not logged in. Run: sigmanticai login[/yellow]")
        return

    try:
        from sigmanticai.auth import _api_request

        jobs_data = _api_request("GET", "/v1/auth/jobs", token=token)
        jobs = jobs_data.get("jobs", [])

        if not jobs:
            console.print("[dim]No generation jobs found.[/dim]")
            return

        from rich.table import Table

        table = Table(title="Recent Jobs (last 10)", border_style="cyan")
        table.add_column("Job ID", style="dim")
        table.add_column("Prompt")
        table.add_column("Type")
        table.add_column("Status")
        table.add_column("Files")
        table.add_column("Duration")
        table.add_column("Created")

        for job in jobs:
            prompt = job.get("prompt", "")
            if len(prompt) > 50:
                prompt = prompt[:50] + "..."
            job_status = job.get("status", "unknown")
            status_color = "green" if job_status == "complete" else "red" if job_status == "failed" else "yellow"
            duration = job.get("duration_seconds")
            duration_str = f"{duration}s" if duration else "-"
            files = job.get("files_count")
            files_str = str(files) if files else "-"
            created = job.get("created_at", "")[:16].replace("T", " ")

            table.add_row(
                job.get("job_id", "")[:12],
                prompt,
                job.get("type", "-"),
                f"[{status_color}]{job_status}[/{status_color}]",
                files_str,
                duration_str,
                created,
            )

        console.print(table)

    except Exception as e:
        console.print(f"[red]Could not fetch jobs: {e}[/red]")
        console.print("[dim]Is the API running? Check SIGMANTICAI_API_URL[/dim]")


# ── File-sync constants ──────────────────────────────────────────

# Per-file size limit for both initial sync and auto-sync (2 MB).
_SYNC_MAX_FILE_BYTES = 2_000_000

# Hard ceiling for any single upload via WebSocket (8 MB).
# The WebSocket max_size is 10 MB; leave headroom for JSON envelope.
_UPLOAD_HARD_MAX_BYTES = 8_000_000

# Total project size cap for initial sync (50 MB).
_INITIAL_SYNC_MAX_TOTAL = 50_000_000

# File extensions included in the project sync.
_PROJECT_EXTENSIONS = {
    # HDL source
    ".sv", ".svh", ".svi", ".v", ".vh", ".vp", ".vhd", ".vhdl",
    # Build / project files
    ".f", ".tcl", ".do", ".cfg", ".yml", ".yaml", ".json", ".toml",
    # Scripts
    ".py", ".sh", ".pl",
    # C/C++ (DPI, Verilator models)
    ".c", ".cpp", ".cc", ".h", ".hpp",
    # Constraints
    ".sdc", ".xdc", ".ucf",
    # Documentation & config
    ".txt", ".md", ".rst", ".csv",
}
_PROJECT_NAMES = {"Makefile", "makefile", "GNUmakefile", "CMakeLists.txt"}

# Directories always skipped during sync.
_SKIP_DIRS = {
    ".git", ".svn", ".hg", ".DS_Store",
    "node_modules", "__pycache__", ".cache", ".mypy_cache", ".pytest_cache",
    ".venv", "venv", "env",
    # EDA build artifacts
    "work", "csrc", "DVEfiles", "AN.DB", "INCA_libs",
    ".Xil", "vivado.runs", "vivado.cache",
    "simv.daidir", "simv.vdb",
}


# ── Pre-flight safety checks for initial sync ───────────────────

# Directories that are clearly NOT hardware projects.  If CWD matches
# one of these, skip the sync entirely to avoid uploading gigabytes of
# unrelated files.
_DANGEROUS_DIR_NAMES = {
    "downloads", "desktop", "documents", "pictures", "music", "videos",
    "movies", "library", "applications", "program files", "appdata",
    "tmp", "temp", "var", "usr", "bin", "sbin", "etc", "opt",
}

# Maximum number of top-level entries (files + dirs) before we consider
# the directory "too broad" to be a real project.
_MAX_TOP_LEVEL_ENTRIES = 200

# Maximum total file count (across all subdirs) for the sync walk.
# Prevents us from scanning millions of files in a bloated directory.
_MAX_SYNC_FILES = 2000


def _is_safe_project_dir(output_dir: Path) -> bool:
    """
    Fast pre-flight check: is this directory likely a real project?

    For directories that look suspicious (home folders, Downloads, Desktop,
    /tmp, etc.) we warn the user and ask for confirmation rather than
    silently skipping.  If the user confirms, we proceed with the sync
    (still subject to the normal file/size caps).
    """
    resolved = output_dir.resolve()
    dir_name = resolved.name.lower()

    warning: str | None = None

    # ── Check 1: known dangerous directory names ──────────────
    if dir_name in _DANGEROUS_DIR_NAMES:
        warning = (
            f"[yellow]⚠ '{resolved.name}' doesn't look like a project directory.[/yellow]\n"
            "    [dim]Scanning here may include many unrelated files and degrade context quality.[/dim]"
        )

    # ── Check 2: home directory itself ────────────────────────
    elif resolved == Path.home().resolve():
        warning = (
            "[yellow]⚠ You're in your home directory.[/yellow]\n"
            "    [dim]Scanning here may include many unrelated files and degrade context quality.[/dim]"
        )

    # ── Check 3: filesystem root ──────────────────────────────
    elif resolved == resolved.anchor or str(resolved) in ("/", "C:\\"):
        warning = (
            "[yellow]⚠ You're at the filesystem root.[/yellow]\n"
            "    [dim]Scanning here may include many unrelated files and degrade context quality.[/dim]"
        )

    # ── Check 4: too many top-level entries ───────────────────
    else:
        try:
            top_count = sum(1 for _ in output_dir.iterdir())
            if top_count > _MAX_TOP_LEVEL_ENTRIES:
                warning = (
                    f"[yellow]⚠ Directory has {top_count} top-level items — "
                    f"that's unusual for a project.[/yellow]\n"
                    "    [dim]Scanning here may include many unrelated files and degrade context quality.[/dim]"
                )
        except PermissionError:
            return False

    if warning is None:
        return True

    # Show warning and ask user for confirmation
    console.print(f"\n  {warning}")
    try:
        answer = console.input(
            "  [bold]Continue anyway? (y/N):[/bold] "
        ).strip().lower()
    except (EOFError, KeyboardInterrupt):
        return False

    if answer in ("y", "yes"):
        return True

    console.print(
        "  [dim]Tip: cd into your project folder before running sigmanticai.[/dim]"
    )
    return False


def _should_skip_dir(d: str) -> bool:
    """Return True if directory should be skipped during project tree walk."""
    return d.startswith(".") or d in _SKIP_DIRS


def _get_project_tree(output_dir: Path, max_entries: int = 500) -> str:
    """Build a compact directory tree of the user's project for system prompt injection."""
    if not _is_safe_project_dir(output_dir):
        return ""

    lines: list[str] = []
    count = 0

    for root, dirs, filenames in os.walk(output_dir):
        dirs[:] = sorted(d for d in dirs if not _should_skip_dir(d))
        rel_root = os.path.relpath(root, output_dir)
        depth = 0 if rel_root == "." else rel_root.count(os.sep) + 1
        indent = "  " * depth

        if rel_root != ".":
            lines.append(f"{indent}{os.path.basename(root)}/")
            count += 1

        for fname in sorted(filenames):
            fpath = Path(root) / fname
            if (
                fpath.suffix.lower() not in _PROJECT_EXTENSIONS
                and fname not in _PROJECT_NAMES
            ):
                continue
            try:
                size = fpath.stat().st_size
            except OSError:
                continue
            if size > _SYNC_MAX_FILE_BYTES or size == 0:
                continue
            size_str = _fmt_file_size(size)
            lines.append(f"{indent}  {fname} ({size_str})")
            count += 1
            if count >= max_entries:
                lines.append(f"\n  ... (truncated at {max_entries} entries)")
                return "\n".join(lines)

    return "\n".join(lines) if lines else ""


def _fmt_file_size(size: int) -> str:
    for unit in ("B", "KB", "MB"):
        if size < 1024:
            return f"{size:.0f}{unit}" if unit == "B" else f"{size:.1f}{unit}"
        size /= 1024
    return f"{size:.1f}GB"


# ── Initial project sync ────────────────────────────────────────

async def _initial_project_sync(client, output_dir: Path) -> int:
    """
    Upload the user's project directory to the server at session start.

    Scans CWD for relevant source files and pushes them over the WebSocket
    so the cloud agent has full access to the existing codebase.  After this
    all tool calls (read_file, grep_search, verilator, etc.) run against
    these files on the server — no further network round-trips needed.

    Returns the number of files uploaded.
    """
    # ── Pre-flight: make sure this looks like a real project ──
    if not _is_safe_project_dir(output_dir):
        return 0

    from sigmanticai.display import update_file_hash

    files_to_upload: list[tuple[str, str, int]] = []  # (rel_path, content, size)
    total_size = 0
    total_files_scanned = 0
    hit_cap = False

    for root, dirs, filenames in os.walk(output_dir):
        # Prune hidden directories and known-bad directories
        dirs[:] = sorted(
            d for d in dirs
            if not d.startswith(".") and d not in _SKIP_DIRS
        )

        for fname in sorted(filenames):
            total_files_scanned += 1

            # Safety valve: if we've scanned thousands of files and
            # still going, this isn't a normal project — bail out.
            if total_files_scanned > _MAX_SYNC_FILES:
                hit_cap = True
                break

            fpath = Path(root) / fname

            # Must match extension allowlist or exact filename
            if (
                fpath.suffix.lower() not in _PROJECT_EXTENSIONS
                and fname not in _PROJECT_NAMES
            ):
                continue

            # Per-file size check
            try:
                fsize = fpath.stat().st_size
            except OSError:
                continue
            if fsize > _SYNC_MAX_FILE_BYTES or fsize == 0:
                continue

            # Total project cap
            if total_size + fsize > _INITIAL_SYNC_MAX_TOTAL:
                hit_cap = True
                break

            # Read as text (skip binary)
            try:
                content = fpath.read_text(errors="replace")
            except Exception:
                continue

            try:
                rel = str(fpath.relative_to(output_dir))
            except ValueError:
                continue

            files_to_upload.append((rel, content, fsize))
            total_size += fsize

        if hit_cap:
            break

    if not files_to_upload:
        return 0

    # Brief status line — user doesn't need to do anything
    total_mb = total_size / 1_000_000
    console.print(
        f"  [dim]Syncing project ({len(files_to_upload)} files, "
        f"{total_mb:.1f} MB)...[/dim]",
        end="",
    )

    for rel_path, content, _ in files_to_upload:
        await client.send_file(rel_path, content)
        update_file_hash(rel_path, content)

    console.print("  [dim]done ✓[/dim]")

    if hit_cap:
        console.print(
            f"  [yellow]⚠ Project is large — only synced {len(files_to_upload)} files "
            f"({total_mb:.1f} MB).[/yellow]\n"
            f"  [dim]Some files may not be available on the server. "
            f"Use /upload <path> to push specific files manually.[/dim]"
        )

    return len(files_to_upload)


# ── /upload escape-hatch (power users) ──────────────────────────

async def _handle_upload_command(client, output_dir: Path, arg: str) -> None:
    """Handle ``/upload <path>`` — push specific files to the server."""
    from sigmanticai.display import update_file_hash

    if not arg:
        console.print("[yellow]Usage: /upload <file-or-glob>[/yellow]")
        return

    target = Path(arg)
    if not target.is_absolute():
        target = output_dir / target

    if "*" in arg or "?" in arg:
        files = sorted(output_dir.glob(arg))
    elif target.is_dir():
        files = [f for f in sorted(target.rglob("*")) if f.is_file()]
    elif target.is_file():
        files = [target]
    else:
        console.print(f"[red]File not found:[/red] {arg}")
        return

    if not files:
        console.print(f"[yellow]No files matched:[/yellow] {arg}")
        return

    uploaded = 0
    for fpath in files:
        fpath = fpath.resolve()
        try:
            rel = fpath.relative_to(output_dir.resolve())
        except ValueError:
            continue
        try:
            fsize = fpath.stat().st_size
        except OSError:
            continue
        if fsize > _UPLOAD_HARD_MAX_BYTES or fsize == 0:
            continue
        try:
            content = fpath.read_text()
        except (UnicodeDecodeError, Exception):
            continue

        await client.send_file(str(rel), content)
        update_file_hash(str(rel), content)
        uploaded += 1

    if uploaded:
        console.print(f"  [dim]{uploaded} file(s) uploaded[/dim]")


def _read_multiline_input(first_line: str) -> str:
    """Collect remaining lines from a multi-line paste after the first line.

    When a user pastes multi-line text, the terminal buffers all lines at once.
    After reading the first line with input(), we check if more data is
    immediately available on stdin (within a tiny window) and collect it.
    """
    lines = [first_line]
    try:
        if sys.platform == "win32":
            import msvcrt
            while msvcrt.kbhit():
                line = sys.stdin.readline()
                if not line:
                    break
                lines.append(line.rstrip('\n'))
        else:
            import select
            while True:
                ready, _, _ = select.select([sys.stdin], [], [], 0.05)
                if not ready:
                    break
                line = sys.stdin.readline()
                if not line:
                    break
                lines.append(line.rstrip('\n'))
    except (OSError, ValueError):
        pass
    return '\n'.join(lines)


def _read_input_with_multiline() -> str:
    """Read user input with multi-line paste support."""
    first_line = console.input("\n[bold blue]sigmanticai>[/bold blue] ")
    return _read_multiline_input(first_line)


def _handle_tool_command(raw_input: str):
    """Interactive EDA tool selector — mirrors verifagent's /tool command."""
    from rich.table import Table
    from sigmanticai.tools_config import (
        ToolConfig, TOOL_CATEGORIES, TOOL_REGISTRY,
        get_vendor_tools, VENDOR_PRESETS,
    )

    _vc = {"synopsys": "magenta", "cadence": "green", "siemens": "cyan",
           "open-source": "yellow", "amd-xilinx": "red"}

    def _show_config(cfg: ToolConfig, title: str = "Your EDA Toolchain"):
        tbl = Table(title=title, show_header=True, header_style="bold cyan",
                    border_style="blue", pad_edge=True, expand=False)
        tbl.add_column("Category", style="bold white", min_width=12)
        tbl.add_column("Tool", min_width=26)
        tbl.add_column("Vendor", min_width=14)
        for cat in TOOL_CATEGORIES:
            t = cfg.get_tool(cat)
            vc = _vc.get(t.vendor, "white")
            nm = t.display_name if t.display_name != "None" else "[dim]-- not set --[/dim]"
            vn = f"[{vc}]{t.vendor}[/{vc}]" if t.vendor != "none" else "[dim]--[/dim]"
            tbl.add_row(cat, nm, vn)
        console.print()
        console.print(tbl)

    config = ToolConfig.from_file()
    parts = raw_input.split()

    # /tool preset <vendor>
    if len(parts) >= 3 and parts[1].lower() == "preset":
        vendor = parts[2].lower()
        if vendor in VENDOR_PRESETS:
            config = ToolConfig.preset(vendor)
            config.save()
            _show_config(config, f"{vendor.title()} Preset Applied")
            console.print("[green]  Saved to .verifagent/tools.json[/green]")
        else:
            console.print(f"[red]  Unknown preset '{vendor}'. Options: {', '.join(VENDOR_PRESETS)}[/red]")
        return

    # /tool reset
    if len(parts) >= 2 and parts[1].lower() == "reset":
        config = ToolConfig()
        config.save()
        _show_config(config, "Reset to Defaults")
        console.print("[green]  Saved to .verifagent/tools.json[/green]")
        return

    _show_config(config)
    console.print()

    _cat_labels = {
        "lint": "Lint / Static Analysis",
        "simulator": "Simulation (UVM + RTL)",
        "coverage": "Coverage Analysis",
        "formal": "Formal Verification",
        "cdc": "Clock Domain Crossing",
        "debug": "Debug / Waveform Viewer",
    }

    vendors = [
        ("synopsys", "Synopsys", "magenta"),
        ("cadence", "Cadence", "green"),
        ("siemens", "Siemens", "cyan"),
        ("amd-xilinx", "AMD / Xilinx (Vivado)", "red"),
        ("open-source", "Open Source (Verilator)", "yellow"),
    ]

    console.print("[bold]EDA Tool Selector[/bold]")
    console.print("[dim]Select a provider, pick tools by number, repeat. Type r to reset, q to finish.[/dim]")
    console.print()

    while True:
        console.print("[bold]Available EDA Providers:[/bold]")
        for i, (vid, vname, vc) in enumerate(vendors, 1):
            tool_count = len(get_vendor_tools(vid))
            console.print(f"  [{vc}]{i}[/{vc}]. {vname} [dim]({tool_count} tools)[/dim]")
        console.print(f"  [red]r[/red]. Reset all tools to defaults (Verilator)")
        console.print()

        try:
            choice = input(f"  Provider [1-{len(vendors)}], r to reset, or q to finish: ").strip().lower()
        except (EOFError, KeyboardInterrupt):
            console.print("\n[yellow]  Cancelled.[/yellow]")
            break

        if choice in ("q", "quit", "exit", "done", ""):
            break

        if choice in ("r", "reset"):
            config = ToolConfig()
            config.save()
            console.print()
            _show_config(config, "Reset to Defaults (Verilator)")
            console.print("[green]  Saved to .verifagent/tools.json[/green]")
            console.print()
            continue

        if not choice.isdigit() or int(choice) < 1 or int(choice) > len(vendors):
            console.print("[red]  Invalid choice.[/red]")
            console.print()
            continue

        vendor_id, vendor_name, vendor_color = vendors[int(choice) - 1]
        vendor_tools = get_vendor_tools(vendor_id)
        if not vendor_tools:
            console.print(f"[yellow]  No tools registered for {vendor_name}.[/yellow]")
            console.print()
            continue

        console.print()
        tbl = Table(title=f"{vendor_name} Tools",
                    show_header=True, header_style=f"bold {vendor_color}",
                    border_style=vendor_color, expand=False)
        tbl.add_column("#", style="bold", min_width=3, justify="right")
        tbl.add_column("Tool", min_width=26)
        tbl.add_column("Used For", min_width=22)
        tbl.add_column("Active", justify="center", min_width=6)
        for i, t in enumerate(vendor_tools, 1):
            cat_label = _cat_labels.get(t.category, t.category)
            is_active = getattr(config, t.category, "") == t.id
            active = "[bold green]YES[/bold green]" if is_active else ""
            tbl.add_row(str(i), t.display_name, cat_label, active)
        console.print(tbl)
        console.print()

        try:
            sel = input("  Select tools (e.g. 1,2,3 or 'all' or Enter to skip): ").strip().lower()
        except (EOFError, KeyboardInterrupt):
            console.print("\n[yellow]  Cancelled.[/yellow]")
            break

        if not sel:
            console.print("[dim]  Skipped.[/dim]")
            console.print()
            continue

        if sel == "all":
            indices = list(range(len(vendor_tools)))
        else:
            indices = []
            for part in sel.replace(" ", "").split(","):
                if part.isdigit():
                    idx = int(part) - 1
                    if 0 <= idx < len(vendor_tools):
                        indices.append(idx)

        if not indices:
            console.print("[yellow]  No valid selections.[/yellow]")
            console.print()
            continue

        cats_seen: dict[str, list] = {}
        for idx in indices:
            t = vendor_tools[idx]
            cats_seen.setdefault(t.category, []).append(t)
        conflicts = {cat: tools for cat, tools in cats_seen.items() if len(tools) > 1}
        if conflicts:
            for cat, dupes in conflicts.items():
                cat_label = _cat_labels.get(cat, cat)
                names = ", ".join(t.display_name for t in dupes)
                console.print(f"  [red]Conflict:[/red] You selected {len(dupes)} tools for "
                              f"[bold]{cat_label}[/bold]: {names}")
                console.print(f"  [yellow]Only one tool per category is allowed. Pick one:[/yellow]")
                for i, t in enumerate(dupes, 1):
                    console.print(f"    {i}. {t.display_name}")
                try:
                    pick = input(f"    Choice [1-{len(dupes)}]: ").strip()
                except (EOFError, KeyboardInterrupt):
                    pick = "1"
                pick_idx = int(pick) - 1 if pick.isdigit() and 1 <= int(pick) <= len(dupes) else 0
                keep_id = dupes[pick_idx].id
                indices = [idx for idx in indices if vendor_tools[idx].category != cat
                           or vendor_tools[idx].id == keep_id]
                console.print(f"    [green]Keeping {dupes[pick_idx].display_name}[/green]")

        selected_tools = []
        for idx in indices:
            t = vendor_tools[idx]
            setattr(config, t.category, t.id)
            selected_tools.append(t)
            cat_label = _cat_labels.get(t.category, t.category)
            console.print(f"  [green]+[/green] {t.display_name} [dim]-> {cat_label}[/dim]")

        console.print()
        version_examples = {
            "synopsys": "T-2022.06, 2024.03",
            "cadence": "23.09, 24.03",
            "siemens": "2024.1, 2023.4",
            "amd-xilinx": "2024.1, 2025.1",
            "open-source": "5.024, 5.028",
        }
        hint = version_examples.get(vendor_id, "2024.1")
        for t in selected_tools:
            try:
                ver = input(f"  {t.display_name} version? (e.g. {hint}, or Enter to skip): ").strip()
            except (EOFError, KeyboardInterrupt):
                ver = ""
            if ver:
                config.set_version(t.id, ver)
                console.print(f"    [dim]{t.display_name} -> v{ver}[/dim]")

        config.save()
        console.print("[green]  Saved to .verifagent/tools.json[/green]")
        console.print()

    # Execution mode prompt for commercial tools
    config.execution_mode = "local"
    config.save()

    _show_config(config, "Final EDA Toolchain")
    console.print("[bold]  Execution: LOCAL (your machine)[/bold]")
    console.print("[green]  Configuration saved to .verifagent/tools.json[/green]")
    console.print("[dim]  Your agents will now use these tools for all compilation, simulation, and analysis.[/dim]")


async def _cmd_interactive():
    """Interactive REPL — the main mode."""
    from sigmanticai.auth import get_access_token, browser_login
    from sigmanticai.client import SigmanticClient
    from sigmanticai.display import display_welcome, display_event, _stop_spinner, _start_spinner

    # Ensure logged in
    token = get_access_token()
    if not token:
        console.print("[yellow]Not logged in. Opening browser...[/yellow]")
        creds = browser_login()
        if not creds:
            console.print("[red]Login required to use SigmanticAI.[/red]")
            return
        token = creds["access_token"]

    # Connect to backend
    client = SigmanticClient(access_token=token)
    try:
        session_info = await client.connect()
    except ConnectionError as e:
        err_msg = str(e)
        if "Quota exceeded" in err_msg or "suspended" in err_msg:
            # Clean quota/ban message — no extra noise
            console.print(f"[red]{err_msg}[/red]")
        elif "Invalid" in err_msg or "unconfirmed" in err_msg:
            # Auth rejection
            console.print(f"[red]{err_msg}[/red]")
            console.print("[dim]Try: sigmanticai logout && sigmanticai[/dim]")
        else:
            # Actual connectivity issue
            console.print(f"[red]Connection failed: {e}[/red]")
            console.print("[dim]Is the backend running? Check SIGMANTICAI_API_URL[/dim]")
        return

    display_welcome(session_info)
    # Use CWD as the output base — generated files land in the user's project
    output_dir = Path(os.getcwd())
    output_dir.mkdir(parents=True, exist_ok=True)

    # Remap /app/ paths from the cloud worker to the local output directory
    from sigmanticai.tool_executor import set_local_base
    set_local_base(str(output_dir) + "/")

    # ── Detect client OS for correct command generation ──
    import platform as _platform
    _os_name = _platform.system()  # "Darwin", "Windows", "Linux"
    _os_version = _platform.version()
    _shell = os.environ.get("SHELL", os.environ.get("COMSPEC", "unknown"))
    os_info = f"{_os_name} ({_platform.machine()}) | shell: {os.path.basename(_shell)}"

    # ── Send project directory tree + tool config + cache (replaces file sync) ──
    project_tree = _get_project_tree(output_dir)
    tool_config_json = ""
    cache_json = ""
    try:
        from sigmanticai.tools_config import ToolConfig
        import json as _json
        from dataclasses import asdict as _asdict
        _cfg = ToolConfig.from_file()
        tool_config_json = _json.dumps(_asdict(_cfg))
    except Exception:
        pass
    try:
        import json as _json
        cache_dir = output_dir / ".sigmanticaicache"
        if cache_dir.is_dir():
            cache_files = {}
            for f in cache_dir.iterdir():
                if f.is_file() and f.suffix == ".json":
                    content = f.read_text()
                    cache_files[f.name] = content
            if cache_files:
                cache_json = _json.dumps(cache_files)
    except Exception:
        pass
    # ── Scan for checkpoint files (resume after credits exhausted) ──
    checkpoints_json = ""
    _resume_prompt = ""  # If set, auto-send this as first message to resume
    try:
        import json as _json
        _ckpt_files = {}
        _ckpt_meta = []  # list of parsed checkpoint dicts + rel_path
        # Scan both <cwd>/output/<project>/ and <cwd>/<project>/ for checkpoints,
        # since the worker may write to either location depending on how the
        # output directory is structured.
        _scan_dirs = []
        _output_subdir = output_dir / "output"
        if _output_subdir.is_dir():
            _scan_dirs.append(_output_subdir)
        _scan_dirs.append(output_dir)  # also scan top-level project dirs
        for _scan_root in _scan_dirs:
            for _proj in _scan_root.iterdir():
                if not _proj.is_dir() or _proj.name.startswith("."):
                    continue
                for _ckpt_name in (".verifagent_checkpoint.json", ".verifagent_rtl_checkpoint.json"):
                    _ckpt = _proj / _ckpt_name
                    if _ckpt.is_file():
                        _rel = str(_ckpt.relative_to(output_dir))
                        if _rel in _ckpt_files:
                            continue  # already found via output/ scan
                        _content = _ckpt.read_text()
                        _ckpt_files[_rel] = _content
                        try:
                            _data = _json.loads(_content)
                            _data["_project_name"] = _proj.name
                            _data["_rel_path"] = _rel
                            _ckpt_meta.append(_data)
                        except Exception:
                            pass
        if _ckpt_files and _ckpt_meta:
            # Show resume prompt to user for each checkpoint
            for _cm in _ckpt_meta:
                _source = _cm.get("source", "unknown")
                _pm_label = "RTL generation" if _source == "rtl" else "Verification generation"
                _ndone = len(_cm.get("completed_specialists", []))
                console.print()
                console.print(
                    f"  [yellow bold]Found checkpoint:[/yellow bold] [white]{_cm['_project_name']}[/white] "
                    f"({_pm_label}, {_ndone} specialist(s) completed)"
                )
            console.print()
            try:
                _choice = input("  Resume from checkpoint? [Y/n] ").strip().lower()
            except (EOFError, KeyboardInterrupt):
                _choice = "n"
            if _choice in ("", "y", "yes"):
                checkpoints_json = _json.dumps(_ckpt_files)
                _cm = _ckpt_meta[-1]
                _source = _cm.get("source", "rtl")
                _ckpt_prompt = _cm.get("prompt", "")
                _done = _cm.get("completed_specialists", [])
                _summaries = _cm.get("specialist_summaries", {})
                # Build a concise progress summary
                _progress_lines = []
                for _sp_name in _done:
                    _sp_sum = _summaries.get(_sp_name, "")
                    if _sp_sum:
                        _progress_lines.append(f"- {_sp_name}: {_sp_sum[:200]}")
                    else:
                        _progress_lines.append(f"- {_sp_name}: completed")
                _progress = "\n".join(_progress_lines) if _progress_lines else "No specialists completed yet."
                _pm_label = "RTL generation" if _source == "rtl" else "Verification generation"
                _resume_prompt = (
                    f"Continue the {_pm_label} that was interrupted due to credits running out.\n\n"
                    f"Original request: {_ckpt_prompt}\n\n"
                    f"Progress so far ({len(_done)} specialist(s) completed):\n{_progress}\n\n"
                    f"A checkpoint file exists in the project directory. "
                    f"The pipeline will automatically skip completed specialists and continue from where it left off."
                )
                console.print(f"  [green]Resuming {_cm['_project_name']}...[/green]")
            else:
                # User declined — delete checkpoint files so they don't re-appear
                for _rp_key in _ckpt_files:
                    try:
                        (output_dir / _rp_key).unlink()
                    except Exception:
                        pass
                console.print("  [dim]Checkpoints cleared.[/dim]")
        elif _output_subdir.is_dir() and any(p.is_dir() for p in _output_subdir.iterdir()):
            # Output directory has files from a previous run but no checkpoint.
            # Warn the user — existing artifacts may confuse specialists (e.g. Tariq
            # diagnosing stale compile errors from a prior killed run).
            console.print()
            console.print(
                "  [yellow bold]⚠ Output directory has files from a previous run.[/yellow bold] "
                "No checkpoint found — starting fresh. Existing files may affect results."
            )
            console.print()
    except Exception:
        pass

    if project_tree:
        await client.send_project_context(project_tree, tool_config_json, cache_json, os_info, checkpoints_json)
        tree_lines = project_tree.count("\n") + 1
        cache_info = f", cache {len(cache_json)//1024}KB" if cache_json else ""
        console.print(f"  [dim]Sent project tree ({tree_lines} entries{cache_info}) to server[/dim]")

    # Start event listener in background
    event_queue: asyncio.Queue = asyncio.Queue()

    async def _listen():
        try:
            async for event in client.events():
                await event_queue.put(event)
        except Exception as e:
            await event_queue.put({"type": "error", "message": str(e)})

    listener = asyncio.create_task(_listen())

    import time as _time
    _last_interrupt_time = 0.0
    _DOUBLE_CTRL_C_WINDOW = 1.5
    _session_exit = False

    # ── Auto-resume: queue the original prompt so it's sent as first message ──
    _pending_resume = _resume_prompt  # Will be consumed on first loop iteration

    try:
        while True:
            if _session_exit:
                break

            # Check for pending events first
            while not event_queue.empty():
                event = event_queue.get_nowait()
                display_event(event, output_dir)
                if event.get("type") == "credits_exhausted":
                    _session_exit = True
                    break

            try:
                # ── If we have a pending resume, send it instead of waiting for input ──
                if _pending_resume:
                    user_input = _pending_resume
                    _pending_resume = ""
                else:
                    # Read user input (non-blocking with asyncio, supports multi-line paste)
                    try:
                        user_input = await asyncio.get_event_loop().run_in_executor(
                            None, lambda: _read_input_with_multiline()
                        )
                    except EOFError:
                        break

                    user_input = user_input.strip()
                    if not user_input:
                        continue

                    if user_input.lower() in ("exit", "quit", "q"):
                        console.print("[yellow]Goodbye![/yellow]")
                        break

                    # ── Slash commands ──────
                    if user_input.startswith("/upload ") or user_input == "/upload":
                        arg = user_input[len("/upload "):].strip() if " " in user_input else ""
                        await _handle_upload_command(client, output_dir, arg)
                        continue

                    if user_input.lower().startswith("/tool"):
                        _handle_tool_command(user_input)
                        try:
                            from sigmanticai.tools_config import ToolConfig
                            import json as _json
                            from dataclasses import asdict as _asdict
                            _cfg = ToolConfig.from_file()
                            _tc_json = _json.dumps(_asdict(_cfg))
                            await client.send_project_context("", _tc_json, "", "")
                            console.print("  [dim]Updated tool config sent to server[/dim]")
                        except Exception:
                            pass
                        continue

                    if user_input.lower() in ("/help", "/h", "/?"):
                        console.print(
                            "\n[bold]Commands:[/bold]\n"
                            "  [cyan]exit | quit | q[/cyan]   End session\n"
                            "  [cyan]/tool[/cyan]              Configure EDA tools\n"
                            "  [cyan]/tool reset[/cyan]        Reset tools to defaults\n"
                            "  [cyan]/upload <path>[/cyan]     Push a specific file to the server\n"
                            "  [cyan]/help[/cyan]              Show this help\n"
                        )
                        continue

                # Send message to backend
                await client.send_message(user_input)
                _start_spinner("Thinking")

                # Wait for and display events until input_needed or done
                try:
                    while True:
                        try:
                            event = await asyncio.wait_for(event_queue.get(), timeout=1800)
                            display_event(event, output_dir)

                            if event.get("type") == "tool_request":
                                from sigmanticai.tool_executor import execute_tool
                                req_id = event.get("request_id", "")
                                tool_name = event.get("tool", "")
                                args_json = event.get("args", "{}")
                                result = execute_tool(tool_name, args_json)
                                await client.send_tool_response(req_id, result)
                                continue

                            if event.get("type") == "command_request":
                                from sigmanticai.local_exec import execute_local_command
                                req_id = event.get("request_id", "")
                                cmd = event.get("command", "")
                                cmd_cwd = str(output_dir / event.get("cwd", "."))
                                result = await execute_local_command(cmd, cwd=cmd_cwd)
                                await client.send_command_response(
                                    req_id,
                                    result["stdout"],
                                    result["stderr"],
                                    result["exit_code"],
                                )
                                continue

                            if event.get("type") == "input_needed":
                                break
                            if event.get("type") in ("done", "failed"):
                                break
                            if event.get("type") == "credits_exhausted":
                                _session_exit = True
                                break
                        except asyncio.TimeoutError:
                            console.print("[yellow]Waiting for response...[/yellow]")
                except (KeyboardInterrupt, asyncio.CancelledError):
                    _stop_spinner()
                    try:
                        await client.send_cancel()
                    except Exception:
                        pass
                    # Drain stale events until we see input_needed (server ack) or timeout
                    _drain_deadline = _time.time() + 5.0
                    while _time.time() < _drain_deadline:
                        try:
                            _evt = await asyncio.wait_for(event_queue.get(), timeout=0.5)
                            if _evt.get("type") == "input_needed":
                                break
                            if _evt.get("type") == "text":
                                _content = _evt.get("content", "")
                                if "interrupted" in _content.lower():
                                    display_event(_evt, output_dir)
                        except (asyncio.TimeoutError, asyncio.CancelledError, Exception):
                            break
                    # Flush anything left over
                    while not event_queue.empty():
                        try:
                            event_queue.get_nowait()
                        except Exception:
                            break
                    console.print("\n[yellow]Interrupted. What would you like to do next?[/yellow]")
                    _last_interrupt_time = _time.time()
                    continue

            except (KeyboardInterrupt, asyncio.CancelledError):
                now = _time.time()
                if now - _last_interrupt_time < _DOUBLE_CTRL_C_WINDOW:
                    console.print("\n[yellow]Goodbye![/yellow]")
                    break
                else:
                    _last_interrupt_time = now
                    console.print("\n[dim]Press Ctrl+C again to exit[/dim]")
                    continue

            except Exception as e:
                console.print(f"[red]Error: {e}[/red]")

    except (KeyboardInterrupt, asyncio.CancelledError):
        console.print("\n[yellow]Goodbye![/yellow]")
    finally:
        _stop_spinner()
        listener.cancel()
        await client.close()
        console.print("[dim]Disconnected.[/dim]")


async def _cmd_generate(prompt: str, rtl_path: str | None, output: str):
    """One-shot generation mode."""
    from sigmanticai.auth import get_access_token, browser_login
    from sigmanticai.client import SigmanticClient
    from sigmanticai.display import display_event

    # Ensure logged in
    token = get_access_token()
    if not token:
        console.print("[yellow]Not logged in. Opening browser...[/yellow]")
        creds = browser_login()
        if not creds:
            console.print("[red]Login required.[/red]")
            return
        token = creds["access_token"]

    client = SigmanticClient(access_token=token)
    try:
        session_info = await client.connect()
    except ConnectionError as e:
        err_msg = str(e)
        if "Quota exceeded" in err_msg or "suspended" in err_msg:
            console.print(f"[red]{err_msg}[/red]")
        else:
            console.print(f"[red]Connection failed: {e}[/red]")
        return

    # Use provided path or CWD — files land in user's project directory
    output_dir = Path(output).resolve() if output != "." else Path(os.getcwd())
    output_dir.mkdir(parents=True, exist_ok=True)

    # Remap /app/ paths from the cloud worker to the local output directory
    from sigmanticai.tool_executor import set_local_base
    set_local_base(str(output_dir) + "/")

    # Send project context (tree, tool config, cache, OS info) so the worker
    # has full context for tool-aware generation
    import platform as _platform
    _os_name = _platform.system()
    _shell = os.environ.get("SHELL", os.environ.get("COMSPEC", "unknown"))
    _os_info = f"{_os_name} ({_platform.machine()}) | shell: {os.path.basename(_shell)}"
    _tree = _get_project_tree(output_dir)
    _tc_json = ""
    _cache_json = ""
    try:
        from sigmanticai.tools_config import ToolConfig
        import json as _json
        from dataclasses import asdict as _asdict
        _tc_json = _json.dumps(_asdict(ToolConfig.from_file()))
    except Exception:
        pass
    try:
        import json as _json
        _cache_dir = output_dir / ".sigmanticaicache"
        if _cache_dir.is_dir():
            _cache_files = {}
            for f in _cache_dir.iterdir():
                if f.is_file() and f.suffix == ".json":
                    _cache_files[f.name] = f.read_text()
            if _cache_files:
                _cache_json = _json.dumps(_cache_files)
    except Exception:
        pass
    # Scan for checkpoint files (resume after credits exhausted)
    _ckpts_json = ""
    try:
        import json as _json
        _ckpt_files = {}
        _scan_roots = []
        _out_sub = output_dir / "output"
        if _out_sub.is_dir():
            _scan_roots.append(_out_sub)
        _scan_roots.append(output_dir)
        for _sr in _scan_roots:
            for _proj in _sr.iterdir():
                if not _proj.is_dir() or _proj.name.startswith("."):
                    continue
                for _cn in (".verifagent_checkpoint.json", ".verifagent_rtl_checkpoint.json"):
                    _ck = _proj / _cn
                    if _ck.is_file():
                        _rk = str(_ck.relative_to(output_dir))
                        if _rk not in _ckpt_files:
                            _ckpt_files[_rk] = _ck.read_text()
        if _ckpt_files:
            _ckpts_json = _json.dumps(_ckpt_files)
    except Exception:
        pass

    if _tree:
        await client.send_project_context(_tree, _tc_json, _cache_json, _os_info, _ckpts_json)

    # Upload RTL files if provided (may overlap with project sync — that's OK)
    if rtl_path:
        rtl_file = Path(rtl_path)
        if rtl_file.is_file():
            content = rtl_file.read_text()
            await client.send_file(rtl_file.name, content)
        elif rtl_file.is_dir():
            for f in rtl_file.glob("*.sv"):
                await client.send_file(f.name, f.read_text())

    # Send the prompt
    await client.send_message(prompt)

    # Stream all events until done, handling remote tool/command execution
    from sigmanticai.display import _stop_spinner
    try:
        async for event in client.events():
            display_event(event, output_dir)

            if event.get("type") == "tool_request":
                from sigmanticai.tool_executor import execute_tool
                req_id = event.get("request_id", "")
                tool_name = event.get("tool", "")
                args_json = event.get("args", "{}")
                result = execute_tool(tool_name, args_json)
                await client.send_tool_response(req_id, result)

            elif event.get("type") == "command_request":
                from sigmanticai.local_exec import execute_local_command
                req_id = event.get("request_id", "")
                cmd = event.get("command", "")
                cmd_cwd = str(output_dir / event.get("cwd", "."))
                result = await execute_local_command(cmd, cwd=cmd_cwd)
                await client.send_command_response(
                    req_id, result["stdout"], result["stderr"], result["exit_code"],
                )
    except KeyboardInterrupt:
        console.print("\n[yellow]Interrupted — closing connection.[/yellow]")
    finally:
        _stop_spinner()
        await client.close()
    console.print(f"[green]Output saved to: {output_dir}[/green]")


if __name__ == "__main__":
    main()

