"""
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."""
    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")

    # ── 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":
        _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_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]Syncing here may upload many unrelated files and degrade performance.[/dim]"
        )

    # ── Check 2: home directory itself ────────────────────────
    elif resolved == Path.home().resolve():
        warning = (
            "[yellow]⚠ You're in your home directory.[/yellow]\n"
            "    [dim]Syncing here may upload many unrelated files and degrade performance.[/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]Syncing here may upload many unrelated files and degrade performance.[/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]Syncing here may upload many unrelated files and degrade performance.[/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 with project sync? (y/N):[/bold] "
        ).strip().lower()
    except (EOFError, KeyboardInterrupt):
        return False

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

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


# ── 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)


# ── Auto-sync (detects edits between messages) ──────────────────

async def _sync_local_changes(client, output_dir: Path) -> int:
    """
    Detect locally modified, new, and deleted files — sync them to the server.

    1. **Modified files**: compares hash of every tracked file to detect edits.
    2. **New files**: full recursive walk of the project tree for files
       matching the extension allowlist that aren't tracked yet (catches
       brand-new subdirectories created by user or agent).
    3. **Deleted files**: tracked files that no longer exist on disk.

    Returns the total number of sync operations (uploads + deletes).
    """
    from sigmanticai.display import (
        get_tracked_files, update_file_hash, remove_file_hash, _hash_content
    )

    tracked = get_tracked_files()
    synced = 0

    # ── 1. Detect modifications and deletions in tracked files ──
    for rel_path, old_hash in list(tracked.items()):
        full_path = (output_dir / rel_path).resolve()

        if not full_path.is_file():
            # File was deleted locally → notify server
            await client.send_delete(rel_path)
            remove_file_hash(rel_path)
            synced += 1
            continue

        try:
            fsize = full_path.stat().st_size
            if fsize > _SYNC_MAX_FILE_BYTES:
                continue
        except OSError:
            continue
        try:
            content = full_path.read_text()
        except Exception:
            continue

        new_hash = _hash_content(content)
        if new_hash != old_hash:
            await client.send_file(rel_path, content)
            update_file_hash(rel_path, content)
            synced += 1

    # ── 2. Detect new files anywhere in the project tree ───────
    # Full recursive walk (same shape as _initial_project_sync) so that
    # brand-new subdirectories — whether created by the agent or the
    # user — are discovered automatically.
    new_files = 0
    files_scanned = 0
    for root, dirs, filenames in os.walk(output_dir):
        dirs[:] = sorted(
            d for d in dirs
            if not d.startswith(".") and d not in _SKIP_DIRS
        )
        for fname in sorted(filenames):
            files_scanned += 1
            if files_scanned > _MAX_SYNC_FILES:
                break  # safety valve

            fpath = Path(root) / fname

            if (
                fpath.suffix.lower() not in _PROJECT_EXTENSIONS
                and fname not in _PROJECT_NAMES
            ):
                continue

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

            if rel in tracked:
                continue  # already handled in pass 1

            try:
                fsize = fpath.stat().st_size
                if fsize > _SYNC_MAX_FILE_BYTES or fsize == 0:
                    continue
            except OSError:
                continue

            try:
                content = fpath.read_text(errors="replace")
            except Exception:
                continue

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

        if files_scanned > _MAX_SYNC_FILES:
            break

    synced += new_files

    return synced


# ── /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]")


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:
        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)

    # ── Upload existing project files so the cloud agent can see them ──
    await _initial_project_sync(client, output_dir)

    # 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())

    try:
        while True:
            # Check for pending events first
            while not event_queue.empty():
                event = event_queue.get_nowait()
                display_event(event, output_dir)
                if event.get("type") in ("done", "failed"):
                    pass  # Don't exit — let user continue conversation

            # Read user input (non-blocking with asyncio)
            try:
                user_input = await asyncio.get_event_loop().run_in_executor(
                    None, lambda: console.input("\n[bold blue]sigmanticai>[/bold blue] ")
                )
            except (EOFError, KeyboardInterrupt):
                break

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

            if user_input.lower() in ("exit", "quit", "q"):
                break

            # ── Slash commands (hidden power-user features) ──────
            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() in ("/help", "/h", "/?"):
                console.print(
                    "\n[bold]Commands:[/bold]\n"
                    "  [cyan]exit | quit | q[/cyan]   End session\n"
                    "  [cyan]/upload <path>[/cyan]     Push a specific file to the server\n"
                    "  [cyan]/help[/cyan]              Show this help\n"
                )
                continue

            # Auto-sync locally modified files before sending the message
            await _sync_local_changes(client, output_dir)

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

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

                    if event.get("type") == "input_needed":
                        break  # Back to input prompt
                    if event.get("type") in ("done", "failed"):
                        break  # Job finished, back to prompt
                except asyncio.TimeoutError:
                    console.print("[yellow]Waiting for response...[/yellow]")

    except KeyboardInterrupt:
        console.print("\n[yellow]Interrupted. 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:
        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)

    # Upload existing project files so the cloud agent can see them
    await _initial_project_sync(client, output_dir)

    # 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
    from sigmanticai.display import _stop_spinner
    async for event in client.events():
        display_event(event, output_dir)

    _stop_spinner()
    await client.close()
    console.print(f"[green]Output saved to: {output_dir}[/green]")


if __name__ == "__main__":
    main()

