"""
cli/main.py — RPy command-line interface.

Usage:
    rpy transpile <src> <out> [--typed] [--fast] [--no-runtime] [--verbose]
    rpy validate <src>                - validate only, no output
    rpy setup [dir]                   - scaffold a new RPy project
    rpy live <src> <out> [flags]      - live file monitoring & Roblox sync
    
    (Aliases: build=transpile, check=validate, init=setup, watch=live)
"""

from __future__ import annotations

import argparse
import asyncio
import json
from dataclasses import replace
import os
import shutil
import subprocess
import sys
import time
from pathlib import Path
from typing import Optional, Sequence
from enum import Enum

try:
    import tomllib
except ImportError:
    tomllib = None

from transpiler.frontend.parser import parse_source, parse_file
from transpiler.transformer import transform
from transpiler.infrastructure.flags import CompilerFlags
from transpiler.backend.generator import generate, GenerateResult
from transpiler.infrastructure.errors import TranspileError
from transpiler.infrastructure.dependency_graph import DependencyGraph
from transpiler.infrastructure.project_context import ProjectContext
from transpiler.middleend.analyzer import SemanticAnalyzer
from transpiler.infrastructure.package_manager import PackageManager


# ---------------------------------------------------------------------------
# Version
# ---------------------------------------------------------------------------
from transpiler.version import __version__



# ---------------------------------------------------------------------------
# rpy.json helpers
# ---------------------------------------------------------------------------

def _load_rpy_config(project_dir: Path) -> dict:
    """Load rpy.toml or rpy.json from *project_dir*, returning an empty dict if not found."""
    toml_path = project_dir / "rpy.toml"
    if toml_path.exists():
        if tomllib is None:
            print("Warning: rpy.toml found but 'tomllib' is missing (requires Python 3.11+).", file=sys.stderr)
            return {}
        try:
            return tomllib.loads(toml_path.read_text(encoding="utf-8"))
        except Exception as e:
            print(f"Warning: rpy.toml is malformed ({e}). Using defaults.", file=sys.stderr)
            return {}

    cfg_path = project_dir / "rpy.json"
    if cfg_path.exists():
        try:
            return json.loads(cfg_path.read_text(encoding="utf-8"))
        except json.JSONDecodeError as e:
            print(f"Warning: rpy.json is malformed ({e}). Using defaults.", file=sys.stderr)
    return {}


def _merge_flags(flags: CompilerFlags, config: dict) -> CompilerFlags:
    """Merge flags from rpy.json into the provided CompilerFlags object (CLI wins)."""
    cfg_flags = config.get("flags", {})
    # Map of flag name to its attribute in CompilerFlags
    flag_map = {
        "typed": "typed",
        "fast": "fast",
        "no_runtime": "no_runtime",
        "shared_runtime": "shared_runtime",
        "source_refs": "source_refs",
        "compile_time": "compile_time",
        "debug": "debug",
        "explain": "explain",
        "verify_cfg": "verify_cfg",
        "opt": "opt"
    }
    
    for cfg_key, attr in flag_map.items():
        val = cfg_flags.get(cfg_key)
        if val is not None:
            # CLI wins if it was explicitly provided (argparse defaults might complicate this, 
            # but for bools if it's True in CLI we keep it. For ints we check if CLI value is default)
            setattr(flags, attr, val)
            
    return flags


def _get_project_folders(config: dict) -> dict[str, str]:
    """Return folder mapping from config, falling back to native Roblox services."""
    raw = config.get("folders", {})
    mapping = {
        "workspace": "workspace",
        "replicatedstorage": "replicatedstorage",
        "serverscriptservice": "serverscriptservice",
        "starterplayerscripts": "starterplayerscripts",
        "lighting": "lighting",
        "startergui": "startergui"
    }
    mapping.update(raw)
    return mapping


# ---------------------------------------------------------------------------
# Project Setup Helpers
# ---------------------------------------------------------------------------
# (Project configuration generation logic)


# ---------------------------------------------------------------------------
# Syncing logic placeholder
# ---------------------------------------------------------------------------
# (Sync hooks moved to external dev server)


# (Structure warnings removed)


# ---------------------------------------------------------------------------
# Core transpile function
def _get_script_type(path: Path) -> str:
    """Determine script type from filename suffixes."""
    name = path.name.lower()
    if name.endswith(".server.py"):
        return "server"
    if name.endswith(".client.py"):
        return "client"
    return "module"


def _validate_placement(path: Path, src_root: Path, folders: dict[str, str]) -> Optional[str]:
    """
    Validate if a script is placed in a logically correct folder.
    Returns an error message if invalid, otherwise None.
    """
    name = path.name.lower()
    rel_path = str(path.relative_to(src_root)).lower()
    
    # Example: .server.py should be in a 'server' folder (mapped or literal)
    if ".server.py" in name:
        if "client" in rel_path or "starterplayer" in rel_path:
            return f"Placement Warning: Server script '{path.name}' is located in a client-side folder."
    
    if ".client.py" in name:
        if "server" in rel_path or "serverscriptservice" in rel_path:
            return f"Placement Warning: Client script '{path.name}' is located in a server-side folder."
            
    return None


def transpile_file(
    src_path: Path,
    flags: CompilerFlags,
    verbose: bool = False,
    project: Optional[ProjectContext] = None,
    module_name: str = "main"
) -> GenerateResult:
    """Parse -> transform -> generate a single .py file into Luau."""
    # Create a fresh copy of flags to avoid mutating shared state (Phase 14 fix)
    file_flags = flags
    if flags.script_type == "module":
        detected_type = _get_script_type(src_path)
        if detected_type != "module":
            from dataclasses import replace
            file_flags = replace(flags, script_type=detected_type)

    tree = parse_file(src_path)
    result = transform(
        tree, 
        filename=str(src_path.absolute()), 
        flags=file_flags,
        project=project,
        module_name=module_name
    )

    gen_result = generate(result, file_flags)
    return gen_result


def transpile_and_write(
    src_path: Path,
    out_path: Path,
    flags: CompilerFlags,
    verbose: bool = False,
    project: Optional[ProjectContext] = None,
    module_name: str = "main"
) -> None:
    """Transpile a single file and write the result with optional source map."""
    gen_result = transpile_file(src_path, flags, verbose, project, module_name)
    out_path.parent.mkdir(parents=True, exist_ok=True)
    out_path.write_text(gen_result.code, encoding="utf-8")
    
    # Write source map
    if gen_result.line_map:
        map_path = out_path.with_suffix(out_path.suffix + ".map")
        map_data = {
            "source": str(src_path.absolute()),
            "mapping": gen_result.line_map
        }
        map_path.write_text(json.dumps(map_data), encoding="utf-8")

    if verbose:
        helpers = ", ".join(sorted(gen_result.runtime_helpers)) or "(none)"
        print(f"  [OK] {src_path} -> {out_path}  [runtime: {helpers}]")


# ---------------------------------------------------------------------------
# Incremental & Watcher
# ---------------------------------------------------------------------------

def run_incremental_build(src: Path):
    """Wrapper to run single-pass incremental build."""
    from cli.watcher import IncrementalBuilder
    builder = IncrementalBuilder(str(src))
    builder.build_incremental()

def cmd_watch(args):
    """
    RPy Watcher entry point.
    """
    from cli.watcher import start_watcher
    start_watcher(args.path)

def cmd_install(args: argparse.Namespace) -> int:
    """Entry point for the install command."""
    from transpiler.package_manager import PackageManager
    pm = PackageManager(args.dir)
    pm.install()
    return 0


# ---------------------------------------------------------------------------
# Commands
# ---------------------------------------------------------------------------

def cmd_build(args: argparse.Namespace) -> int:
    """Build command: transpile .py files to .lua."""
    src = Path(args.src).resolve()
    out = Path(args.out).resolve()
    flags = CompilerFlags(
        typed=args.typed,
        fast=args.fast,
        no_runtime=args.no_runtime,
        shared_runtime=args.shared_runtime,
        source_refs=args.source_refs,
        compile_time=args.compile_time,
        debug=args.debug,
        explain=args.explain,
        verify_cfg=args.verify_cfg,
        opt=args.opt,
    )

    # Load project config
    project_dir = src.parent if src.is_file() else src
    config = _load_rpy_config(project_dir)
    flags = _merge_flags(flags, config)

    if src.is_file():
        if out.suffix == "":
            name = src.name
            if name.endswith(".server.py"):
                new_name = name[:-10] + ".server.lua"
            elif name.endswith(".client.py"):
                new_name = name[:-10] + ".client.lua"
            elif name.endswith(".module.py"):
                new_name = name[:-10] + ".lua"
            else:
                new_name = src.with_suffix(".lua").name
            out = out / new_name
        try:
            transpile_and_write(src, out, flags, verbose=args.verbose)
        except TranspileError as e:
            source = src.read_text(encoding="utf-8") if src.exists() else None
            print(f"Error: {e.format_with_context(source)}", file=sys.stderr)
            return 1
        print(f"Built 1 file -> {out}")
        return 0

    if src.is_dir():
        if getattr(args, "incremental", False):
             try:
                 run_incremental_build(src)
                 return 0
             except Exception as e:
                 print(f"Incremental build failed: {e}", file=sys.stderr)
                 return 1
        
        project = ProjectContext()
        graph = DependencyGraph(str(src))
        graph.scan_project()
        import concurrent.futures
        import threading
        import multiprocessing

        # We still call get_build_order() to let it validate the DAG cycle detection.
        try:
            build_order = graph.get_build_order()
        except TranspileError as e:
            print(f"Error: {e}", file=sys.stderr)
            return 1

        if not build_order:
            print(f"No .py files found in {src}")
            return 1

        in_degree = {n: len(deps) for n, deps in graph.graph.items()}
        ready_queue = [n for n in graph.graph if in_degree[n] == 0]
        
        completed_nodes = 0
        total_nodes = len(graph.graph)
        errors = 0

        # Output formatting lock for deterministic logs
        print_lock = threading.Lock()

        def compile_file(py_file: Path) -> bool:
            try:
                # Increment handled in main loop to avoid double counting and scoping issues
                # Calculate module name for the project context
                rel = py_file.relative_to(src)
                module_name = ".".join(rel.with_suffix("").parts)
                if module_name.endswith(".__init__"): 
                    module_name = module_name[:-9]
                
                # Use same naming convention for output files
                name = py_file.name
                if name.endswith(".server.py"):
                    new_name = name[:-10] + ".server.lua"
                elif name.endswith(".client.py"):
                    new_name = name[:-10] + ".client.lua"
                elif name.endswith(".module.py"):
                    new_name = name[:-10] + ".lua"
                else:
                    new_name = py_file.with_suffix(".lua").name
                
                lua_file = out / rel.parent / new_name

                # The print output inside transpile_and_write is captured/suppressed optionally,
                # but for simplicity we rely on transpile_file and handle writes manually here,
                # OR we wrap transpile_and_write with the lock if verbose.
                
                # For Phase 1 parallelism, we compile then write.
                from transpiler.parser import parse_file
                from transpiler.transformer import transform
                from transpiler.generator import generate
                
                file_flags = flags
                if flags.script_type == "module":
                    detected_type = _get_script_type(py_file)
                    if detected_type != "module":
                        from dataclasses import replace
                        file_flags = replace(flags, script_type=detected_type)

                tree = parse_file(py_file)
                result = transform(
                    tree, 
                    filename=str(py_file.absolute()), 
                    flags=file_flags,
                    project=project,
                    module_name=module_name
                )
                gen_result = generate(result, file_flags, module_name=module_name)
                
                lua_file.parent.mkdir(parents=True, exist_ok=True)
                lua_file.write_text(gen_result.code, encoding="utf-8")
                
                # Write source map
                if gen_result.line_map:
                    map_path = lua_file.with_suffix(lua_file.suffix + ".map")
                    map_data = {
                        "source": str(py_file.absolute()),
                        "mapping": gen_result.line_map
                    }
                    map_path.write_text(json.dumps(map_data), encoding="utf-8")
                
                if args.verbose:
                    helpers = ", ".join(sorted(gen_result.runtime_helpers)) or "(none)"
                    with print_lock:
                        print(f"  [OK] {py_file} -> {lua_file}  [runtime: {helpers}]")
                return True
            except TranspileError as e:
                source = py_file.read_text(encoding="utf-8") if py_file.exists() else None
                with print_lock:
                    print(f"  ✗ {py_file}:\n{e.format_with_context(source)}\n", file=sys.stderr)
                return False
            except Exception as e:
                with print_lock:
                    print(f"  ✗ {py_file}: internal error: {e}", file=sys.stderr)
                    if flags.debug:
                        import traceback
                        traceback.print_exc()
                return False

        num_workers = args.workers if getattr(args, "workers", 0) > 0 else (multiprocessing.cpu_count() or 4)
        
        with concurrent.futures.ThreadPoolExecutor(max_workers=num_workers) as executor:
            future_to_node = {}
            for node in ready_queue:
                future_to_node[executor.submit(compile_file, node)] = node # type: ignore[arg-type]
            ready_queue.clear()
            
            while future_to_node:
                done, _ = concurrent.futures.wait(
                    future_to_node.keys(), 
                    return_when=concurrent.futures.FIRST_COMPLETED
                )
                
                for fut in done:
                    node = future_to_node.pop(fut)
                    completed_nodes += 1 # type: ignore
                    try:
                        success = fut.result()
                        if not success:
                            errors += 1
                        else:
                            # Update dependents
                            for dependent in graph.reverse_graph.get(node, []):
                                in_degree[dependent] -= 1
                                if in_degree[dependent] == 0:
                                    future_to_node[executor.submit(compile_file, dependent)] = dependent
                    except Exception:
                        errors += 1

        ok = total_nodes - errors
        print(f"\nBuilt {ok}/{total_nodes} files -> {out} (workers: {num_workers})")
        if getattr(args, "expose_out", False):
            print(f"Output directory available at: {out.resolve()}")
        if errors:
            print(f"  {errors} file(s) had errors.", file=sys.stderr)
        return 1 if errors else 0

    if src.is_dir():
        if getattr(args, "incremental", False):
             try:
                 run_incremental_build(src, out)
                 return 0
             except Exception as e:
                 print(f"Incremental build failed: {e}", file=sys.stderr)
                 return 1

    print(f"Error: {src} does not exist.", file=sys.stderr)
    return 1


def cmd_validate(args: argparse.Namespace) -> int:
    """Validate command: dry-run transpilation to check AST and CFG syntax errors without writing output."""
    src = Path(args.src)
    flags = CompilerFlags(
        typed=args.typed,
        fast=args.fast,
        no_runtime=getattr(args, "no_runtime", False),
        shared_runtime=getattr(args, "shared_runtime", False),
        source_refs=getattr(args, "source_refs", False),
        compile_time=args.compile_time,
        debug=args.debug,
        verify_cfg=getattr(args, "verify_cfg", False)
    )

    # Load project config to merge flags
    project_dir = src.parent if src.is_file() else src
    config = _load_rpy_config(project_dir)
    flags = _merge_flags(flags, config)

    files: list[Path] = []
    if src.is_file():
        files = [src]
    elif src.is_dir():
        files = [
            f for f in src.rglob("*.py")
            if f.name != "__init__.py" and "__pycache__" not in str(f)
        ]
    else:
        print(f"Error: {src} does not exist.", file=sys.stderr)
        return 1

    errors = 0
    for py_file in files:
        try:
            transpile_file(py_file, flags, verbose=args.verbose)
            if args.verbose:
                print(f"  ✓ {py_file}")
        except TranspileError as e:
            source = py_file.read_text(encoding="utf-8") if py_file.exists() else None
            print(f"  ✗ {py_file}:\n{e.format_with_context(source)}", file=sys.stderr)
            errors += 1
        except Exception as e:
            print(f"  ✗ {py_file}: internal error: {e}", file=sys.stderr)
            errors += 1

    total = len(files)
    ok = total - errors
    if errors:
        print(f"\nCheck: {ok}/{total} files OK, {errors} error(s).")
    return 0


def cmd_init(args: argparse.Namespace) -> int:
    """
    Init command: scaffold a new RPy project.

    Creates:
        workspace/
        replicatedstorage/
        serverscriptservice/
        starterplayerscripts/
        .rpy/out/
        rpy.toml          — project configuration
    """
    project_dir = Path(args.dir or ".")
    project_name = project_dir.resolve().name or "RPyProject"

    # Determine folder mapping from existing config (if any)
    config = _load_rpy_config(project_dir)
    folders = _get_project_folders(config)

    # Create root service structure
    out_dir = project_dir / ".rpy" / "out"
    (project_dir / "src").mkdir(parents=True, exist_ok=True) # Recommended source folder
    for folder_key, folder_name in folders.items():
        if folder_key not in ("lighting", "startergui"): # Skip these by default to reduce clutter
            (project_dir / folder_name).mkdir(parents=True, exist_ok=True)
            (out_dir / folder_name).mkdir(parents=True, exist_ok=True)

    print(f"  Created 'src' and native service folders")
    print(f"  Created hidden .rpy/out/ artifacts directory")

    # rpy.toml
    default_config = f"""version = "2.0"
src = "."
out = ".rpy/out"
exclude = ["__pycache__", "*.pyc", "tools", ".rpy"]

[flags]
typed = false
fast = false
no_runtime = false
source_refs = false
compile_time = false

[folders]
workspace = "workspace"
replicatedstorage = "replicatedstorage"
serverscriptservice = "serverscriptservice"
starterplayerscripts = "starterplayerscripts"
lighting = "lighting"
startergui = "startergui"
"""
    config_path = project_dir / "rpy.toml"
    if not config_path.exists():
        config_path.write_text(default_config, encoding="utf-8")
        print(f"  Created {config_path}")
    else:
        print(f"  {config_path} already exists — skipping.")

    # Example scripts in each folder
    _write_example_scripts(project_dir, folders)

    # Copy runtime into each out subfolder
    runtime_src = Path(__file__).parent.parent / "runtime" / "python_runtime.lua"
    if runtime_src.exists():
        for folder_key, folder_name in folders.items():
            if folder_key not in ("lighting", "startergui"):
                runtime_dst = out_dir / folder_name / "python_runtime.lua"
                if not runtime_dst.exists():
                    shutil.copy2(runtime_src, runtime_dst)
        print(f"  Copied python_runtime.lua into .rpy/out/ subfolders")

    print(f"\n[OK] RPy project '{project_name}' initialized in {project_dir.resolve()}")
    print("  Folder structure:")
    print("    serverscriptservice/  (server scripts)")
    print("    starterplayerscripts/ (client scripts)")
    print("    replicatedstorage/    (shared modules)")
    print("    workspace/            (world logic)")
    print()
    print("  Next steps:")
    print("    rpy transpile . .rpy/out/        - transpile all scripts")
    print("    rpy live . .rpy/out/             - start dev server & sync to Studio")
    return 0


def _write_example_scripts(project_dir: Path, mapping: dict[str, str]) -> None:
    """Write starter scripts into each root subfolder."""
    examples = {
        "serverscriptservice": (
            '"""Server-side RPy script."""\n'
            "from roblox import game\n\n\n"
            "def on_player_added(player):\n"
            '    print(f"Player joined: {player.Name}")\n\n\n'
            'players = game.GetService("Players")\n'
            "players.PlayerAdded.connect(on_player_added)\n"
        ),
        "starterplayerscripts": (
            '"""Client-side RPy script."""\n'
            "from roblox import game\n\n\n"
            'players = game.GetService("Players")\n'
            "local_player = players.LocalPlayer\n"
            'print(f"Hello, {local_player.Name}!")\n'
        ),
        "replicatedstorage": (
            '"""Shared utility module."""\n\n\n'
            "def clamp(value, min_val, max_val):\n"
            '    """Clamp value between min and max."""\n'
            "    if value < min_val:\n"
            "        return min_val\n"
            "    if value > max_val:\n"
            "        return max_val\n"
            "    return value\n"
        ),
    }
    for folder_key, folder_name in mapping.items():
        script = examples.get(folder_key)
        if script is None:
            continue
        out_file = project_dir / folder_name / ("init.server.py" if folder_key == "serverscriptservice" else "init.client.py" if folder_key == "starterplayerscripts" else "utils.py")
        if not out_file.exists():
            out_file.write_text(script, encoding="utf-8")
            print(f"  Created {out_file}")


def cmd_watch(args: argparse.Namespace) -> int:
    """Watch command: rebuild on file changes, with optional --sync hook."""
    src = Path(args.src)
    out = Path(args.out)
    flags = CompilerFlags(
        typed=args.typed,
        fast=args.fast,
        no_runtime=args.no_runtime,
        shared_runtime=args.shared_runtime,
        source_refs=args.source_refs,
        compile_time=args.compile_time,
        debug=args.debug,
    )
    interval = args.interval
    # Load config once
    project_dir = src if src.is_dir() else src.parent
    config = _load_rpy_config(project_dir)
    flags = _merge_flags(flags, config)

    if not src.exists():
        print(f"Error: {src} does not exist.", file=sys.stderr)
        return 1

    print(f"Watching {src} -> {out} (interval: {interval}s)")
    print("Press Ctrl+C to stop.\n")

    if getattr(args, "incremental", False):
        try:
            run_watcher(src, interval)
            return 0
        except KeyboardInterrupt:
            return 0
        except Exception as e:
            print(f"Watcher failed: {e}", file=sys.stderr)
            return 1

    last_mtimes: dict[Path, float] = {}

    def get_files() -> list[Path]:
        if src.is_file():
            return [src]
        return [
            f for f in src.rglob("*.py")
            if f.name != "__init__.py" and "__pycache__" not in str(f)
        ]

    def build_changed() -> int:
        """Rebuild all changed files. Returns count of newly transpiled files."""
        nonlocal last_mtimes
        files = get_files()
        rebuilt = 0
        for py_file in files:
            try:
                mtime = py_file.stat().st_mtime
            except OSError:
                continue

            if py_file not in last_mtimes or last_mtimes[py_file] < mtime:
                last_mtimes[py_file] = mtime
                # Resolve output path (respects mapped subfolders)
                if src.is_file():
                    lua_path = out if out.suffix == ".lua" else out / py_file.with_suffix(".lua").name
                else:
                    rel = py_file.relative_to(src)
                    lua_path = out / rel.with_suffix(".lua")
                try:
                    transpile_and_write(py_file, lua_path, flags, verbose=True)
                    rebuilt += 1
                except TranspileError as e:
                    source = py_file.read_text(encoding="utf-8") if py_file.exists() else None
                    print(f"  ✗ {py_file}:\n{e.format_with_context(source)}", file=sys.stderr)
                except Exception as e:
                    print(f"  ✗ {py_file}: internal error: {e}", file=sys.stderr)
        return rebuilt

    """Alias for live syncing."""
    return cmd_live(args)



def cmd_check(args: argparse.Namespace) -> int:
    """Type-check Python source files using mypy before transpilation.

    This uses the user's existing Python source (not the generated Luau) to
    validate type correctness.  mypy must be installed:  pip install mypy
    """
    import subprocess
    import shutil

    src = getattr(args, "src", ".")
    src_path = Path(src).resolve()
    strict = getattr(args, "strict", False)
    no_error_summary = getattr(args, "no_error_summary", False)

    # Prefer dmypy for incremental checking if available
    checker = "dmypy" if shutil.which("dmypy") else "mypy"

    if not shutil.which(checker) and not shutil.which("mypy"):
        print(
            "\033[91mError:\033[0m 'mypy' is not installed.\n"
            "  Install it with:  pip install mypy\n"
            "  Then re-run:      rpy check",
            file=sys.stderr,
        )
        return 1

    # Build mypy command
    cmd: list[str] = []
    if checker == "dmypy":
        # dmypy run -- <src> ... 
        cmd = ["dmypy", "run", "--", str(src_path)]
    else:
        cmd = ["mypy", str(src_path)]

    if strict:
        cmd.append("--strict")

    # Always ignore missing stubs for Roblox-specific modules
    cmd += ["--ignore-missing-imports"]

    if no_error_summary:
        cmd += ["--no-error-summary"]

    print(f"  Running {checker} on \033[94m{src_path}\033[0m ...\n")

    try:
        result = subprocess.run(cmd, capture_output=False, text=True)
        if result.returncode == 0:
            print("\n\033[92m✓ No type errors found.\033[0m")
        elif result.returncode == 1:
            print(f"\n\033[93m⚠ Type errors found (exit {result.returncode}).\033[0m")
        else:
            print(f"\n\033[91m✗ mypy exited with code {result.returncode}.\033[0m")
        return result.returncode
    except FileNotFoundError:
        print(
            f"\033[91mError:\033[0m Could not find '{checker}'.  "
            "Make sure mypy is installed:  pip install mypy",
            file=sys.stderr,
        )
        return 1


def cmd_lsp(args: argparse.Namespace) -> int:
    """Starts the RPy Language Server."""
    # Ensure the project root is in sys.path so 'server' and 'transpiler' are findable
    import sys
    import os
    project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    if project_root not in sys.path:
        sys.path.insert(0, project_root)
        
    try:
        from server.lsp_server import start_lsp_server
        # host and port can be passed if the server supports them (currently stdio only)
        start_lsp_server(host=args.host, port=args.port)
    except ImportError as e:
        print(f"\033[91mError:\033[0m Could not find LSP server module: {e}", file=sys.stderr)
        return 1
    return 0


def cmd_live(args: argparse.Namespace) -> int:
    """Live command: monitor files and sync to Dev Server."""
    src = Path(args.src)
    out = Path(args.out)
    flags = CompilerFlags(
        typed=args.typed,
        fast=args.fast,
        no_runtime=args.no_runtime,
        shared_runtime=args.shared_runtime,
        source_refs=args.source_refs,
        compile_time=args.compile_time,
        debug=args.debug,
    )
    
    # Load config
    project_dir = src if src.is_dir() else src.parent
    config = _load_rpy_config(project_dir)
    flags = _merge_flags(flags, config)

    if not src.exists() and args.src == "src":
        # Fallback to current directory if default 'src' doesn't exist
        src = Path(".")
    
    if not src.exists():
        print(f"Error: Source directory '{src}' does not exist.", file=sys.stderr)
        return 1

    # Use asyncio to run the coordinator
    from sync.coordinator import start_live_sync
    try:
        asyncio.run(start_live_sync(src, out, flags))
    except KeyboardInterrupt:
        print("\nLive sync stopped.")
    except OSError as e:
        if e.errno == 10048: # Address already in use
            print(f"\033[91mError: Port 8000 is already in use.\033[0m", file=sys.stderr)
            print(f"  A previous 'rpy live' process might still be running.", file=sys.stderr)
            print(f"  Please stop it or kill the process using port 8000.", file=sys.stderr)
        else:
            print(f"\033[91mError starting live server:\033[0m {e}", file=sys.stderr)
        return 1
    return 0


def _install_studio_plugin() -> bool:
    """Helper to install the RPy Studio Companion plugin to the local Roblox plugins folder."""
    import platform
    import shutil
    import os
    
    system = platform.system()
    plugin_src = Path(__file__).parent.parent / "plugin" / "init.lua"
    
    if not plugin_src.exists():
        print(f"\033[91mError:\033[0m Plugin source not found at {plugin_src}", file=sys.stderr)
        return False
        
    if system == "Windows":
        appdata = os.getenv("LOCALAPPDATA")
        if not appdata:
            print("\033[91mError:\033[0m LOCALAPPDATA environment variable not found.", file=sys.stderr)
            return False
        plugin_dir = Path(appdata) / "Roblox" / "Plugins"
    elif system == "Darwin":
        plugin_dir = Path.home() / "Library" / "Application Support" / "Roblox" / "Plugins"
    else:
        print(f"Plugin installation is not supported on {system} automatically.", file=sys.stderr)
        return False
        
    try:
        plugin_dir.mkdir(parents=True, exist_ok=True)
        plugin_dst = plugin_dir / "RPyCompanion.rbxm"
        
        # Wrap .lua code into a Roblox XML model (.rbxmx format but with .rbxm extension)
        lua_code = plugin_src.read_text(encoding="utf-8")
        
        # Simple XML template for a Roblox Script
        xml_content = f"""<roblox xmlns:xmime="http://www.w3.org/2005/05/xmlmime" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://www.roblox.com/roblox.xsd" version="4">
	<External>null</External>
	<External>nil</External>
	<Item class="Script" referent="RBX0">
		<Properties>
			<BinaryString name="AttributesSerialize"></BinaryString>
			<bool name="Disabled">false</bool>
			<Content name="LinkedSource"><null></null></Content>
			<string name="Name">RPyCompanion</string>
			<token name="RunContext">0</token>
			<string name="ScriptGuid">{{00000000-0000-0000-0000-000000000000}}</string>
			<ProtectedString name="Source"><![CDATA[{lua_code}]]></ProtectedString>
			<int64 name="SourceAssetId">-1</int64>
			<BinaryString name="Tags"></BinaryString>
		</Properties>
	</Item>
</roblox>"""
        plugin_dst.write_text(xml_content, encoding="utf-8")
        print(f"  [OK] Installed Studio Companion as binary model to {plugin_dst}")
        return True
    except Exception as e:
        print(f"\033[91mError:\033[0m Failed to install plugin: {e}", file=sys.stderr)
        return False


def cmd_install(args: argparse.Namespace) -> int:
    """Install command: download dependencies AND/OR install the Studio plugin."""
    project_dir = Path(args.dir).resolve()
    
    # Check for rpy.json/toml
    config = _load_rpy_config(project_dir)
    
    if not config:
        # If no config is found in THIS directory, we assume the user wants to install the plugin globally
        print("No RPy project configuration found in this directory.")
        print("Deploying RPy Studio Companion...")
        return 0 if _install_studio_plugin() else 1

    # If config exists, try to install dependencies
    print(f"Installing dependencies for {project_dir.name}...")
    pm = PackageManager(str(project_dir))
    try:
        pm.install()
        # Also offer to install the plugin if it's not present or as part of the setup
        print("\nDeploying RPy Studio Companion...")
        _install_studio_plugin()
        return 0
    except Exception as e:
        print(f"\033[91mError during installation:\033[0m {e}", file=sys.stderr)
        return 1


def cmd_expose_out(args: argparse.Namespace) -> int:
    """Print the absolute path to the .rpy/out folder."""
    project_dir = Path(args.dir or ".").resolve()
    out_dir = project_dir / ".rpy" / "out"
    print(str(out_dir))
    return 0

def cmd_open_out(args: argparse.Namespace) -> int:
    """Open the .rpy/out folder in the system file explorer."""
    project_dir = Path(args.dir or ".").resolve()
    out_dir = project_dir / ".rpy" / "out"
    if not out_dir.exists():
        print(f"Error: {out_dir} does not exist. Run 'rpy build' first.", file=sys.stderr)
        return 1
        
    print(f"Opening {out_dir}...")
    import platform
    import subprocess
    system = platform.system()
    try:
        if system == "Windows":
            os.startfile(out_dir)
        elif system == "Darwin":
            subprocess.run(["open", str(out_dir)], check=True)
        else:
            subprocess.run(["xdg-open", str(out_dir)], check=True)
        return 0
    except Exception as e:
        print(f"Error opening folder: {e}", file=sys.stderr)
        return 1

def _print_ir_node(node, indent="", seen=None):
    if seen is None:
        seen = set()
        # The original instruction snippet seems to be for a different function
        # (e.g., one iterating over a symbol table's definitions).
        # Applying it directly here would break the logic of _print_ir_node.
        # If there's a _print_symbol_table function, the change should go there.
        # For _print_ir_node, we proceed with its original logic.
        
    if isinstance(node, Enum):
        print(f"{indent}{getattr(node, 'name', str(node))}")
        return
        
    node_id = id(node)
    if node_id in seen:
        print(f"{indent}<recursive ref to {type(node).__name__}>")
        return
        
    if isinstance(node, list):
        for item in node:
            _print_ir_node(item, indent, seen)
        return
        
    if not hasattr(node, "__dict__"):
        print(f"{indent}{repr(node)}")
        return
        
    seen.add(node_id)
    name = type(node).__name__
    print(f"{indent}{name}:")
    for k, v in node.__dict__.items():
        if k.startswith("_") or k in ("lineno", "col_offset"):
            continue
        if isinstance(v, list) and v and hasattr(v[0], "__dict__"):
            print(f"{indent}  {k}:")
            for item in v:
                _print_ir_node(item, indent + "    ", seen)
        elif hasattr(v, "__dict__"):
            print(f"{indent}  {k}:")
            _print_ir_node(v, indent + "    ", seen)
        else:
            print(f"{indent}  {k} = {repr(v)}")


def cmd_dump_ir(args: argparse.Namespace) -> int:
    """Dump the intermediate representation of a file at a specific stage."""
    src = Path(args.src)
    if not src.exists():
        print(f"Error: {src} does not exist.", file=sys.stderr)
        return 1

    try:
        from transpiler.parser import parse_file
        from transpiler.ir_transformer import PythonToIRTransformer
        from transpiler.ir_optimizer import optimize_ir
        
        tree = parse_file(src)
        transformer = PythonToIRTransformer()
        ir_module = transformer.transform(tree)
        
        if args.stage in ("ssa", "optimized"):
            # We intercept optimize_ir. For now if it's just 'optimized', we run full.
            # If 'ssa', we run partial? ir_optimizer doesn't support partial return easily.
            # We just run full optimization and dump it.
            optimize_ir(ir_module)
            
        _print_ir_node(ir_module)
        return 0
    except Exception as e:
        print(f"Error dumping IR: {e}", file=sys.stderr)
        return 1


def cmd_dump_cfg(args: argparse.Namespace) -> int:
    """Dump the control flow graph of a file at a specific stage."""
    src = Path(args.src)
    if not src.exists():
        print(f"Error: {src} does not exist.", file=sys.stderr)
        return 1

    try:
        from transpiler.parser import parse_file
        from transpiler.ir_transformer import PythonToIRTransformer
        from transpiler.cfg_builder import build_cfg
        from transpiler import ssa, purity, loop_detector
        
        tree = parse_file(src)
        transformer = PythonToIRTransformer()
        ir_module = transformer.transform(tree)
        cfg = build_cfg(ir_module)
        
        if getattr(args, "stage", "optimized") == "ast":
             print(cfg)
             return 0

        # Run up to optimized
        dom_analysis = ssa.DominatorAnalysis(cfg)
        dom_analysis.run()
        ssa.SSAConstructor(cfg, dom_analysis).run()
        
        # Purity
        p_analyzer = purity.PurityAnalyzer()
        # [Simplified purity loop skipped here for brevity, we just test simple scripts]
        
        verifier = ssa.SSAVerifier(cfg)
        changed = True
        while changed:
            changed = False
            if ssa.SSAConstantPropagation(cfg).run(): changed = True
            if ssa.SSACopyPropagation(cfg).run(): changed = True
            if ssa.SSACSE(cfg).run(): changed = True
            if ssa.SSADCE(cfg).run(): changed = True
            if ssa.PhiSimplification(cfg).run(): changed = True

        dom_analysis.run()
        loops = loop_detector.detect_loops(cfg, dom_analysis)
        ssa.SSALICM(cfg, loops).run()
        ssa.SSADCE(cfg).run()
        
        ssa.SSAExpressionInliner(cfg).run()
        
        print(f"CFG Entry: {cfg.entry_block.id}")
        for block_id, block in cfg.blocks.items():
             print(f"\nBlock {block_id}:")
             print(f"  Predecessors: {[p.id for p in block.predecessors]}")
             print(f"  Successors: {[s.id for s in block.successors]}")
             print("  Statements:")
             for stmt in block.statements:
                 print(f"    - {type(stmt).__name__}")
                 # simplistic print for important fields
                 target_node = getattr(stmt, "target", None)
                 if target_node:
                     # Safely extract a name or representation
                     if hasattr(target_node, "original_name"):
                         tgt = target_node.original_name
                     elif hasattr(target_node, "name"):
                         tgt = target_node.name
                     else:
                         tgt = f"<{type(target_node).__name__}>"
                     print(f"      target: {tgt}")
                 if hasattr(stmt, "value"):
                      val = type(stmt.value).__name__
                      print(f"      value: {val}")
        return 0
    except Exception as e:
        import traceback
        traceback.print_exc()
        print(f"Error dumping CFG: {e}", file=sys.stderr)
        return 1


def cmd_graph(args: argparse.Namespace) -> int:
    """Graph command: visualize project dependencies."""
    src = Path(args.src).resolve()
    if not src.exists():
        print(f"Error: {src} does not exist.", file=sys.stderr)
        return 1

    graph = DependencyGraph(str(src))
    graph.scan_project(ignore_roots=getattr(args, "all", False))

    mermaid_code = graph.to_mermaid()

    if args.output:
        out_path = Path(args.output)
        out_path.write_text(mermaid_code, encoding="utf-8")
        print(f"Graph written to {out_path}")
    else:
        print(mermaid_code)
    return 0


def cmd_remap(args: argparse.Namespace) -> int:
    """Remap a Luau stack trace back to Python using available .map files."""
    import re
    input_text = args.trace
    if not input_text:
        # Try reading from stdin if possible, but for CLI we'll just use the arg for now
        print("Error: No stack trace provided.", file=sys.stderr)
        return 1

    # Look for patterns like "module_name:line_number" or "[string \"...\"]:line"
    # Luau stack trace element examples:
    # 1. "ReplicatedStorage.RPy.test:5"
    # 2. "[string \"-- Generated by RPy...\"]:12"
    
    lines = input_text.splitlines()
    out_lines = []
    
    # Quick cache for loaded maps
    loaded_maps = {} # path -> map_data

    def find_and_remap(file_id: str, luau_line: int) -> str:
        # Search .rpy/out or current dir for .lua.map files
        # This is a bit heuristic.
        possible_maps = []
        if os.path.exists(".rpy/out"):
            for root, _, files in os.walk(".rpy/out"):
                for f in files:
                    if f.endswith(".lua.map"):
                        possible_maps.append(Path(root) / f)
        
        for map_path in possible_maps:
            if map_path not in loaded_maps:
                try:
                    loaded_maps[map_path] = json.loads(map_path.read_text(encoding="utf-8"))
                except: continue
            
            data = loaded_maps[map_path]
            # Simple check: does the map_path seem to match the file_id?
            if file_id in str(map_path) or file_id.replace(".", "/") in str(map_path):
                py_line = data["mapping"].get(str(luau_line)) or data["mapping"].get(luau_line)
                if py_line:
                    return f"{data['source']}:{py_line} (from {file_id}:{luau_line})"
        
        return f"{file_id}:{luau_line}"

    for line in lines:
        match = re.search(r"([\w\.]+):(\d+)", line)
        if match:
            file_id = match.group(1)
            luau_line = int(match.group(2))
            remapped = find_and_remap(file_id, luau_line)
            out_lines.append(line.replace(f"{file_id}:{luau_line}", remapped))
        else:
            out_lines.append(line)
            
    print("\n--- Remapped Stack Trace ---")
    for l in out_lines:
        print(l)
    return 0


def cmd_debug(args: argparse.Namespace) -> int:
    """Debug command: map Python breakpoints to Luau lines."""
    src_file = Path(args.src).resolve()
    if not src_file.exists():
        print(f"Error: {src_file} does not exist.", file=sys.stderr)
        return 1

    # Search for matching .lua.map file in .rpy/out
    project_dir = src_file.parent
    while project_dir != project_dir.parent and not (project_dir / "rpy.toml").exists() and not (project_dir / "rpy.json").exists():
        project_dir = project_dir.parent
    
    out_dir = project_dir / ".rpy" / "out"
    if not out_dir.exists():
        out_dir = project_dir # Fallback
        
    map_file = None
    for root, _, files in os.walk(out_dir):
        for f in files:
            if f.endswith(".lua.map"):
                try:
                    data = json.loads((Path(root) / f).read_text(encoding="utf-8"))
                    if data.get("source") == str(src_file):
                        map_file = Path(root) / f
                        break
                except: continue
        if map_file: break
    
    if not map_file:
        print(f"Error: No source map found for {src_file}. Did you build the project?", file=sys.stderr)
        return 1
        
    # Ensure map_file is not None for type checkers
    assert map_file is not None
    data = json.loads(map_file.read_text(encoding="utf-8"))
    mapping = data.get("mapping", {})
    
    if args.line:
        # Python -> Luau
        target_line = args.line
        luau_lines = [int(l) for l, p in mapping.items() if int(p) == target_line]
        if luau_lines:
            luau_lines.sort()
            print(f"Python line {target_line} maps to Luau line(s): {', '.join(map(str, luau_lines))}")
            print(f"File: {map_file.with_suffix('')}")
        else:
            # Try to find the nearest previous mapping
            sorted_py_lines = sorted([int(p) for p in mapping.values()])
            prev_line = None
            for p in sorted_py_lines:
                if p <= target_line:
                    prev_line = p
                else:
                    break
            
            if prev_line:
                luau_lines = [int(l) for l, p in mapping.items() if int(p) == prev_line]
                luau_lines.sort()
                print(f"No exact match for Python line {target_line}. Nearest previous mapping (Python {prev_line}):")
                print(f"  Luau line(s): {', '.join(map(str, luau_lines))}")
            else:
                print(f"No Luau mapping found for Python line {target_line}")
    elif args.luau_line:
        # Luau -> Python
        target_line = str(args.luau_line)
        py_line = mapping.get(target_line)
        if py_line:
            print(f"Luau line {target_line} maps to Python line: {py_line}")
            print(f"Source: {src_file}")
        else:
            print(f"No Python mapping found for Luau line {target_line}")
    else:
        print(f"Source Map for {src_file}:")
        print(f"  Luau Output: {map_file.with_suffix('')}")
        print(f"  Mappings: {len(mapping)} lines")
        
    return 0

def cmd_profile(args: argparse.Namespace) -> int:
    """Profile command: update or view profiling data in .rpy_profile."""
    project_dir = Path(args.dir or ".").resolve()
    profile_path = project_dir / ".rpy_profile"

    if args.reset:
        if profile_path.exists():
            profile_path.unlink()
            print(f"  [OK] Reset profiling data at {profile_path}")
        else:
            print(f"  No profiling data found at {profile_path}")
        return 0

    if args.update:
        try:
            stats = json.loads(args.update)
            # Ensure basic structure
            if "functions" not in stats:
                stats = {"functions": stats}
            
            # Load existing or start fresh
            current = {}
            if profile_path.exists():
                try:
                    current = json.loads(profile_path.read_text(encoding="utf-8"))
                except: pass
            
            # Update
            if "functions" not in current: current["functions"] = {}
            for fn, data in stats["functions"].items():
                current["functions"][fn] = data
            
            profile_path.write_text(json.dumps(current, indent=4), encoding="utf-8")
            print(f"  [OK] Updated profiling data at {profile_path}")
            return 0
        except json.JSONDecodeError:
            print(f"Error: Invalid JSON provided to --update", file=sys.stderr)
            return 1
        except Exception as e:
            print(f"Error updating profile: {e}", file=sys.stderr)
            return 1

    if not profile_path.exists():
        print(f"No profiling data found at {profile_path}")
        print("Tip: Use 'rpy profile --update <json>' to seed data from Roblox.")
        return 0

    try:
        data = json.loads(profile_path.read_text(encoding="utf-8"))
        funcs = data.get("functions", {})
        if not funcs:
            print(f"Profiling data at {profile_path} is empty.")
            return 0
            
        print(f"\nProfiling Data ({profile_path}):")
        print(f"{'Function':<40} | {'Avg (ms)':<10} | {'Calls':<8}")
        print("-" * 65)
        for name, stats in sorted(funcs.items(), key=lambda x: x[1].get("avg_ms", 0), reverse=True):
            avg = stats.get("avg_ms", 0.0)
            calls = stats.get("calls", 0)
            print(f"{name:<40} | {avg:<10.4f} | {calls:<8}")
        print()
        return 0
    except Exception as e:
        print(f"Error reading profiling data: {e}", file=sys.stderr)
        return 1

def cmd_doctor(args: argparse.Namespace) -> int:
    """Doctor command: perform environment and project health checks."""
    project_dir = Path(args.dir or ".").resolve()
    print(f"\nRPy Doctor — Environment Health Check")
    print("-" * 40)

    # 1. System Checks
    print(f"Checking system...")
    py_ver = sys.version_info
    py_status = "✓" if py_ver >= (3, 11) else "⚠"
    print(f"  {py_status} Python {py_ver.major}.{py_ver.minor}.{py_ver.micro}")
    if py_ver < (3, 11):
        print("    (Tip: Python 3.11+ is recommended for native 'tomllib' support)")

    import shutil
    mypy_path = shutil.which("mypy")
    mypy_status = "✓" if mypy_path else "⚠"
    print(f"  {mypy_status} mypy: {mypy_path or 'Not found'}")
    if not mypy_path:
        print("    (Tip: Install 'mypy' for 'rpy check' functionality)")

    # 2. Project Checks
    print(f"\nChecking project at {project_dir}...")
    config_path = project_dir / "rpy.toml"
    if not config_path.exists():
        config_path = project_dir / "rpy.json"
    
    config_status = "✓" if config_path.exists() else "✗"
    print(f"  {config_status} Configuration: {config_path.name if config_path.exists() else 'Missing (rpy.toml or rpy.json)'}")

    # Out directory check
    out_dir = project_dir / ".rpy" / "out"
    if out_dir.exists():
        can_write = os.access(out_dir, os.W_OK)
        write_status = "✓" if can_write else "✗"
        print(f"  {write_status} Build Output: {out_dir} (Writable: {can_write})")
    else:
        print(f"  ⚠ Build Output: {out_dir} (Missing — run 'rpy build' to create it)")

    # Roblox source folders
    config = _load_rpy_config(project_dir)
    folders = _get_project_folders(config)
    found_folders = []
    for f in folders.values():
        if (project_dir / f).exists():
            found_folders.append(f)
    folder_status = "✓" if found_folders else "⚠"
    print(f"  {folder_status} Project Structure: found {len(found_folders)} native service folders")

    if not config_path.exists():
        print("\n[!] Conclusion: This does not appear to be an RPy project directory.")
        print("    Run 'rpy setup' to initialize a new project here.")
        return 1
    
    print("\n[OK] Everything looks good! Your environment is ready for RPy.")
    return 0

def cmd_clean(args: argparse.Namespace) -> int:
    """Clean command: clear compilation cache and/or build artifacts."""
    project_dir = Path(args.dir or ".").resolve()
    removed = []

    def safe_remove(path: Path, label: str):
        # Safety check: ensure we don't delete random directories
        safe_names = [".rpy_cache", "out"]
        if path.name not in safe_names:
            print(f"Error: Refusing to delete potentially unsafe path '{path}'", file=sys.stderr)
            return

        # Ensure it's inside the project root
        try:
            path.relative_to(project_dir)
        except ValueError:
            print(f"Error: Refusing to delete path outside project root '{path}'", file=sys.stderr)
            return

        if not path.exists():
            rel_path = path.relative_to(project_dir) if path.is_relative_to(project_dir) else path
            print(f"[-] Not found: {rel_path}")
            return

        if getattr(args, "dry_run", False):
            rel_path = path.relative_to(project_dir) if path.is_relative_to(project_dir) else path
            print(f"Would remove {label}: {rel_path}")
            removed.append(path)
            return

        try:
            import shutil
            import stat
            
            def handle_remove_read_only(func, path, exc):
                try:
                    import os
                    os.chmod(path, stat.S_IWRITE)
                    func(path)
                except Exception:
                    pass

            shutil.rmtree(path, onerror=handle_remove_read_only)
            rel_path = path.relative_to(project_dir) if path.is_relative_to(project_dir) else path
            print(f"[OK] Removed {label}: {rel_path}")
            removed.append(path)
        except Exception as e:
            print(f"Failed to remove {path}: {e}", file=sys.stderr)

    cache_dir = project_dir / ".rpy_cache"
    out_dir = project_dir / ".rpy" / "out"

    clean_cache = getattr(args, "cache", False)
    clean_out = getattr(args, "out", False)
    clean_all = getattr(args, "all", False)
    
    if not (clean_cache or clean_out or clean_all):
        clean_cache = True

    if clean_all:
        clean_cache = True
        clean_out = True

    if clean_cache:
        safe_remove(cache_dir, "cache")
    if clean_out:
        safe_remove(out_dir, "build output")

    if getattr(args, "dry_run", False) and not removed:
        print("[OK] Nothing would be cleaned")
    elif not removed:
        print("[OK] Nothing to clean")

    return 0

def cmd_lsp(args: argparse.Namespace) -> int:
    try:
        from server import lsp_server
        sys.exit(lsp_server.main())
    except ImportError:
        print("\033[91mError:\033[0m Could not import language server module.")
        return 1



# ---------------------------------------------------------------------------
# Argument parser
# ---------------------------------------------------------------------------

def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="rpy",
        description="RPy — Python to Roblox Luau transpiler",
    )
    parser.add_argument("--version", action="version", version=f"rpy {__version__}")
    subparsers = parser.add_subparsers(dest="command", help="Available commands")

    # Common compiler flags helper
    def add_common_flags(p):
        p.add_argument("--typed", action="store_true", help="Emit Luau type annotations")
        p.add_argument("--fast", action="store_true", help="Skip py_bool() shims for speed")
        p.add_argument("--no-runtime", action="store_true", dest="no_runtime", help="Skip prepending RPy runtime")
        p.add_argument("--shared-runtime", action="store_true", help="Use require() for runtime")
        p.add_argument("--source-refs", action="store_true", help="Emit Python source line comments")
        p.add_argument("--compile-time", action="store_true", help="Enable build-time code execution")
        p.add_argument("--opt", type=int, choices=[0, 1, 2], default=2, help="Optimization level (0-2)")
        p.add_argument("--debug", action="store_true", help="Enable diagnostic logging")
        p.add_argument("--explain", action="store_true", help="Explain optimization decisions")
        p.add_argument("--verify-cfg", action="store_true", help="Run internal CFG structural audit")
        p.add_argument("--debug-compiler", action="store_true", help="Show internal compiler debug messages (noisy)")
        p.add_argument("--verbose", "-v", action="store_true")

    # ---- build / transpile ----
    build_p = subparsers.add_parser("build", aliases=["transpile"], help="Build the project into Luau")
    build_p.add_argument("src", nargs="?", default=".", help="Source file or directory (default: .)")
    build_p.add_argument("out", nargs="?", default=".rpy/out", help="Output directory (default: .rpy/out)")
    add_common_flags(build_p)
    build_p.add_argument("--workers", type=int, default=os.cpu_count(), help="Number of concurrent workers")
    build_p.add_argument("--expose-out", action="store_true", help="Print absolute path to .rpy/out")
    build_p.add_argument("--incremental", action="store_true", help="Only rebuild changed files (AAA Scale)")

    # ---- validate ----
    validate_p = subparsers.add_parser("validate", help="Check AST/CFG syntax without outputting files")
    validate_p.add_argument("src", nargs="?", default=".", help="Source file or directory")
    add_common_flags(validate_p)

    # ---- check (mypy) ----
    check_p = subparsers.add_parser("check", help="Type-check Python sources using mypy before transpiling")
    check_p.add_argument("src", nargs="?", default=".", help="Source file or directory")
    check_p.add_argument("--strict", action="store_true", help="Enable mypy --strict mode")
    check_p.add_argument("--no-error-summary", action="store_true", dest="no_error_summary", help="Suppress mypy count summary")

    # ---- dump-ir ----
    dump_ir_p = subparsers.add_parser("dump-ir", help="Dump Intermediate Representation")
    dump_ir_p.add_argument("src", help="Source file to dump IR for")
    dump_ir_p.add_argument("--stage", choices=["ast", "ssa", "optimized"], default="optimized", help="Stage to view")
    add_common_flags(dump_ir_p)
    dump_ir_p.set_defaults(func=cmd_dump_ir)

    # ---- dump-cfg ----
    dump_cfg_p = subparsers.add_parser("dump-cfg", help="Dump Control Flow Graph")
    dump_cfg_p.add_argument("src", help="Source file to dump CFG for")
    dump_cfg_p.add_argument("--stage", choices=["ast", "ssa", "optimized"], default="optimized", help="Stage to view")
    add_common_flags(dump_cfg_p)
    dump_cfg_p.set_defaults(func=cmd_dump_cfg)

    # ---- setup (init) ----
    setup_p = subparsers.add_parser("setup", aliases=["init"], help="Scaffold a new RPy project")
    setup_p.add_argument("dir", nargs="?", default=".", help="Project directory (default: .)")

    # ---- live (watch) ----
    live_p = subparsers.add_parser("live", aliases=["watch"], help="Live monitoring and Roblox sync")
    live_p.add_argument("src", nargs="?", default="src", help="Source file or directory (default: src)")
    live_p.add_argument("out", nargs="?", default=".rpy/out", help="Output folder")
    add_common_flags(live_p)
    live_p.add_argument("--interval", type=float, default=1.0, help="Poll interval in seconds")
    live_p.add_argument("--show-out", action="store_true", help="Write transpiled files even in live mode")
    live_p.add_argument("--backup-studio", action="store_true", help="Backup Studio scripts before overwrite")
    live_p.add_argument("--incremental", action="store_true", default=True, help="Enable AAA incremental builder")


    # ---- remap ----
    remap_p = subparsers.add_parser("remap", help="Translate Luau stack trace to Python")
    remap_p.add_argument("trace", help="The Luau stack trace string to remap")
    remap_p.set_defaults(func=cmd_remap)
    
    debug_p = subparsers.add_parser("debug", help="Map breakpoints between Python and Luau")
    debug_p.add_argument("src", help="Python source file")

    # ---- install ----
    install_p = subparsers.add_parser("install", help="Install dependencies from rpy.toml")
    install_p.add_argument("dir", nargs="?", default=".", help="Project directory")
    install_p.set_defaults(func=cmd_install)

    # ---- profile ----
    profile_p = subparsers.add_parser("profile", help="Manage profiling data for automatic optimization")
    profile_p.add_argument("dir", nargs="?", default=".", help="Project directory")
    profile_p.add_argument("--update", help="JSON string of profiling stats to merge")
    profile_p.add_argument("--reset", action="store_true", help="Clear all profiling data")
    profile_p.set_defaults(func=cmd_profile)

    # ---- doctor ----
    doctor_p = subparsers.add_parser("doctor", help="Environment and project health check")
    doctor_p.add_argument("dir", nargs="?", default=".", help="Project directory")

    # ---- clean ----
    clean_p = subparsers.add_parser("clean", help="Clear compilation cache and artifacts")
    clean_p.add_argument("dir", nargs="?", default=".", help="Project directory")
    clean_p.add_argument("--cache", action="store_true", help="Remove the .rpy_cache directory (default)")
    clean_p.add_argument("--out", action="store_true", help="Remove the .rpy/out directory")
    clean_p.add_argument("--all", action="store_true", help="Remove both cache and output directories")
    clean_p.add_argument("--dry-run", action="store_true", help="Show what would be removed without actually deleting anything")
    clean_p.set_defaults(func=cmd_clean)

    # ---- lsp ----
    lsp_p = subparsers.add_parser("lsp", help="Start the Language Server for IDE support")
    lsp_p.add_argument("--stdio", action="store_true", help="Talk to the VS Code client over STDIO")
    lsp_p.set_defaults(func=cmd_lsp)

    return parser


# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------

def main(argv: Sequence[str] | None = None) -> int:
    parser = build_parser()
    args = parser.parse_args(argv)

    from transpiler.infrastructure import logger
    if hasattr(args, "debug_compiler") and args.debug_compiler:
        logger.set_debug_compiler(True)
    if hasattr(args, "explain") and args.explain:
        logger.set_explain_mode(True)

    if args.command is None:
        parser.print_help()
        return 0

    commands = {
        "transpile": cmd_build,
        "build": cmd_build,
        "validate": cmd_validate,
        "debug": cmd_debug,
        "check": cmd_check,
        "setup": cmd_init,
        "init": cmd_init,
        "live": cmd_live,
        "watch": cmd_live,
        "lsp": cmd_lsp,
        "install": cmd_install,
        "expose-out": cmd_expose_out,
        "open-out": cmd_open_out,
        "dump-ir": cmd_dump_ir,
        "dump-cfg": cmd_dump_cfg,
        "graph": cmd_graph,
        "profile": cmd_profile,
        "doctor": cmd_doctor,
        "clean": cmd_clean,
    }

    handler = commands.get(args.command)
    if handler is None:
        parser.print_help()
        return 1

    try:
        return handler(args)
    except TranspileError as e:
        # Most transpile errors are reported within handlers for file-specific context.
        # This catch-all handles any that escape or are global.
        print(f"\033[91mError:\033[0m {e}", file=sys.stderr)
        return 1
    except KeyboardInterrupt:
        print("\nStopped by user.")
        return 130
    except Exception as e:
        # Use try-except to avoid encoding errors on windows when printing to stderr
        try:
            print(f"\033[91mInternal Compiler Error:\033[0m {e}", file=sys.stderr)
        except UnicodeEncodeError:
            print(f"\033[91mInternal Compiler Error:\033[0m {repr(e)}", file=sys.stderr)
        print("  This is likely a bug in RPy. Please report it with the stack trace.", file=sys.stderr)
        if os.environ.get("RPY_DEBUG") or (hasattr(args, "debug") and args.debug):
            import traceback
            traceback.print_exc()
        return 2


if __name__ == "__main__":
    sys.exit(main())

