import click
import subprocess
import shutil
import platform
import os
import sys
import json
import urllib.request
import urllib.error
from pathlib import Path
from .config_parser import CppProjectConfig

# Unicode fallback for Windows terminals with limited encoding
def _supports_unicode():
    """Check if terminal supports Unicode output."""
    if sys.platform == 'win32':
        try:
            # Test if we can encode Unicode box characters
            '╔═╗'.encode(sys.stdout.encoding or 'utf-8')
            return True
        except (UnicodeEncodeError, LookupError):
            return False
    return True

# ASCII fallbacks for box-drawing characters
_UNICODE = _supports_unicode()
_BOX_TOP = "╔══════════════════════════════════════════════════════════════╗" if _UNICODE else "+--------------------------------------------------------------+"
_BOX_BOTTOM = "╚══════════════════════════════════════════════════════════════╝" if _UNICODE else "+--------------------------------------------------------------+"
_BOX_SIDE = "║" if _UNICODE else "|"
_SEC_TOP = "┌─────────────────────────────────────────────────────────────┐" if _UNICODE else "+-------------------------------------------------------------+"
_SEC_BOTTOM = "└─────────────────────────────────────────────────────────────┘" if _UNICODE else "+-------------------------------------------------------------+"
_SEC_SIDE = "│" if _UNICODE else "|"
_HLINE = "═" if _UNICODE else "-"
_CHECK = ""  # Removed check marks per user request
_CROSS = "✗" if _UNICODE else "[X]"
_BULLET = "•" if _UNICODE else "*"
_ARROW = "→" if _UNICODE else "->"
_TRIANGLE = "▶" if _UNICODE else ">"
_DIAMOND = "◆" if _UNICODE else "*"

def _safe_echo(text, **kwargs):
    """Echo text with Unicode fallback for unsupported terminals."""
    try:
        click.secho(text, **kwargs)
    except UnicodeEncodeError:
        # Fallback: replace Unicode with ASCII
        ascii_text = text
        replacements = [
            ('╔', '+'), ('╗', '+'), ('╚', '+'), ('╝', '+'),
            ('┌', '+'), ('┐', '+'), ('└', '+'), ('┘', '+'),
            ('═', '-'), ('─', '-'), ('║', '|'), ('│', '|'),
            ('✗', '[X]'), ('•', '*'),
            ('→', '->'), ('▶', '>'), ('◆', '*')
        ]
        for uni, ascii_char in replacements:
            ascii_text = ascii_text.replace(uni, ascii_char)
        click.secho(ascii_text, **kwargs)

def _render_readme_with_colors(readme_text):
    """Render README.md with color highlighting in terminal."""
    lines = readme_text.split('\n')

    for line in lines:
        stripped = line.strip()

        # Headers
        if stripped.startswith('# '):
            click.echo()
            click.secho("=" * 70, fg='cyan', bold=True)
            click.secho(stripped[2:], fg='cyan', bold=True)
            click.secho("=" * 70, fg='cyan', bold=True)
        elif stripped.startswith('## '):
            click.echo()
            click.secho("+" + "-" * 68 + "+", fg='blue', bold=True)
            click.secho("| " + stripped[3:], fg='blue', bold=True)
            click.secho("+" + "-" * 68 + "+", fg='blue')
        elif stripped.startswith('### '):
            click.echo()
            click.secho("  " + stripped[4:], fg='green', bold=True)
            click.secho("  " + "-" * len(stripped[4:]), fg='green')
        elif stripped.startswith('#### '):
            click.secho("    " + stripped[5:], fg='yellow', bold=True)
        # Code blocks
        elif stripped.startswith('```'):
            lang = stripped[3:] if len(stripped) > 3 else ''
            if lang:
                click.secho("    [" + lang + "]", fg='magenta')
            else:
                click.echo()
        # Bullet points
        elif stripped.startswith('- '):
            click.echo("  * ", nl=False)
            click.echo(stripped[2:])
        elif stripped.startswith('* '):
            click.echo("  * ", nl=False)
            click.echo(stripped[2:])
        # Numbered lists
        elif len(stripped) > 2 and stripped[0].isdigit() and stripped[1] == '.':
            click.secho("  " + stripped[:2] + " ", fg='yellow', nl=False)
            click.echo(stripped[3:])
        # Code lines (indented with 4+ spaces in markdown code blocks)
        elif line.startswith('    ') or line.startswith('\t'):
            click.secho("    " + stripped, fg='bright_black')
        # Bold text **text**
        elif '**' in stripped:
            # Simple bold rendering
            parts = stripped.split('**')
            for i, part in enumerate(parts):
                if i % 2 == 1:  # Bold parts
                    click.secho(part, fg='white', bold=True, nl=False)
                else:
                    click.echo(part, nl=False)
            click.echo()
        # Horizontal rules
        elif stripped.startswith('---'):
            click.secho("-" * 70, fg='bright_black')
        # Empty lines
        elif not stripped:
            click.echo()
        # Regular text
        else:
            click.echo("  " + stripped)


@click.group(invoke_without_command=True)
@click.option('--doc', 'show_doc', is_flag=True, help='Show documentation (README) with color highlighting')
@click.pass_context
def cli(ctx, show_doc):
    """IncludeCPP - C++ Performance in Python, Zero Hassle"""
    if show_doc:
        click.echo("=" * 70)
        click.secho("IncludeCPP Documentation", fg='cyan', bold=True)
        click.echo("=" * 70)
        click.echo()

        try:
            click.echo("  Fetching documentation from PyPI...", nl=False)
            req = urllib.request.Request(
                "https://pypi.org/pypi/IncludeCPP/json",
                headers={"User-Agent": "IncludeCPP-CLI"}
            )
            with urllib.request.urlopen(req, timeout=15) as response:
                raw_data = response.read().decode('utf-8')

            data = json.loads(raw_data)
            description = data.get('info', {}).get('description', '')
            click.secho(" OK", fg='green')
            click.echo()

            if description:
                # v2.8: Add critical namespace warning at top
                click.secho("+" + "=" * 68 + "+", fg='red', bold=True)
                click.secho("|  IMPORTANT: All C++ code MUST be in 'namespace includecpp { }'     |", fg='red', bold=True)
                click.secho("|  This is REQUIRED for IncludeCPP to work!                         |", fg='red', bold=True)
                click.secho("+" + "=" * 68 + "+", fg='red', bold=True)
                click.echo()
                _render_readme_with_colors(description)
            else:
                click.secho("No documentation available.", fg='yellow')

        except Exception as e:
            click.secho(" FAILED", fg='red')
            click.echo()
            click.secho("Could not fetch documentation: " + str(e), fg='red', err=True)

        click.echo("=" * 70)
        ctx.exit(0)

    # If no subcommand is given, show help
    if ctx.invoked_subcommand is None:
        click.echo(ctx.get_help())

def _setup_global_command():
    system = platform.system()

    if system == "Windows":
        bin_dir = Path(os.environ.get('LOCALAPPDATA', os.path.expanduser('~\\AppData\\Local'))) / 'IncludeCPP' / 'bin'
        bin_dir.mkdir(parents=True, exist_ok=True)

        script_path = bin_dir / 'includecpp.bat'
        with open(script_path, 'w') as f:
            f.write('@echo off\n')
            f.write(f'"{sys.executable}" -m includecpp %*\n')

        current_path = os.environ.get('PATH', '')
        bin_dir_str = str(bin_dir)

        if bin_dir_str not in current_path:
            try:
                import winreg
                key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, 'Environment', 0, winreg.KEY_ALL_ACCESS)
                try:
                    user_path, _ = winreg.QueryValueEx(key, 'PATH')
                except FileNotFoundError:
                    user_path = ''

                if bin_dir_str not in user_path:
                    new_path = f"{user_path};{bin_dir_str}" if user_path else bin_dir_str
                    winreg.SetValueEx(key, 'PATH', 0, winreg.REG_EXPAND_SZ, new_path)
                    click.secho(f"Added {bin_dir} to user PATH", fg='green')
                    click.echo("Restart your terminal for the change to take effect")
                    winreg.CloseKey(key)
                    return True
            except Exception as e:
                click.secho(f"Warning: Could not update PATH automatically: {e}", fg='yellow')
                click.echo(f"Please manually add {bin_dir} to your PATH")
                return False

        click.secho(f"Global command 'includecpp' is ready at {script_path}", fg='green')
        return True

    elif system in ["Linux", "Darwin"]:
        bin_dir = Path.home() / '.local' / 'bin'
        bin_dir.mkdir(parents=True, exist_ok=True)

        script_path = bin_dir / 'includecpp'
        with open(script_path, 'w') as f:
            f.write('#!/usr/bin/env bash\n')
            f.write(f'exec "{sys.executable}" -m includecpp "$@"\n')

        script_path.chmod(0o755)

        current_path = os.environ.get('PATH', '')
        bin_dir_str = str(bin_dir)

        if bin_dir_str in current_path:
            click.secho(f"Global command 'includecpp' is ready at {script_path}", fg='green')
        else:
            click.secho(f"Created wrapper script at {script_path}", fg='green')
            click.echo(f"Add {bin_dir} to your PATH by adding this to ~/.bashrc or ~/.zshrc:")
            click.echo(f'  export PATH="$PATH:{bin_dir}"')

        return True

    return False

@cli.command()
def init():
    config_file = Path.cwd() / "cpp.proj"
    if config_file.exists():
        click.echo("cpp.proj already exists")
        return

    CppProjectConfig.create_default('cpp.proj')
    click.echo("Created cpp.proj configuration")
    click.echo("Created include/ directory")
    click.echo("Created plugins/ directory")

    marker_file = Path.home() / '.includecpp_global_setup'
    if not marker_file.exists():
        click.echo()
        if _setup_global_command():
            marker_file.touch()
            click.echo()
            click.echo("You can now use 'includecpp <command>' instead of 'python -m includecpp <command>'")

def _show_build_info_report(build_dir: Path, compiler: str):
    """Show detailed analysis report of the last build."""
    import json
    from datetime import datetime

    registry_path = build_dir / "module_registry.json"

    # Check if build exists
    if not build_dir.exists() or not registry_path.exists():
        click.echo("")
        _safe_echo(_BOX_TOP, fg='yellow')
        _safe_echo(f"{_BOX_SIDE} NO BUILD FOUND", fg='yellow')
        _safe_echo(_BOX_BOTTOM, fg='yellow')
        click.echo("")
        click.echo("No previous build found. Run 'includecpp rebuild' first.")
        click.echo("")
        return

    # Load registry
    try:
        with open(registry_path, 'r') as f:
            registry = json.load(f)
    except Exception as e:
        click.secho(f"Error reading registry: {e}", fg='red', err=True)
        return

    modules = registry.get('modules', {})
    if not modules:
        click.echo("No modules found in registry.")
        return

    # Collect statistics
    total_modules = len(modules)
    total_functions = 0
    total_classes = 0
    total_structs = 0
    total_methods = 0
    total_bindings = 0

    for mod_info in modules.values():
        funcs = mod_info.get('functions', [])
        classes = mod_info.get('classes', [])
        structs = mod_info.get('structs', [])

        total_functions += len(funcs)
        total_classes += len(classes)
        total_structs += len(structs)

        for cls in classes:
            if isinstance(cls, dict):
                total_methods += len(cls.get('methods', []))

        # Count bindings (functions + methods + class constructors)
        total_bindings += len(funcs)
        for cls in classes:
            if isinstance(cls, dict):
                total_bindings += len(cls.get('methods', [])) + 1  # +1 for constructor

    # Check for .pyd/.so files
    pyd_files = list(build_dir.glob("*.pyd")) + list(build_dir.glob("*.so"))

    # Check for .pyi stub files
    pyi_files = list(build_dir.glob("*.pyi"))

    # Get last build time from registry file modification time
    last_build_time = datetime.fromtimestamp(registry_path.stat().st_mtime)
    time_ago = datetime.now() - last_build_time
    if time_ago.days > 0:
        time_str = f"{time_ago.days} day(s) ago"
    elif time_ago.seconds >= 3600:
        time_str = f"{time_ago.seconds // 3600} hour(s) ago"
    elif time_ago.seconds >= 60:
        time_str = f"{time_ago.seconds // 60} minute(s) ago"
    else:
        time_str = f"{time_ago.seconds} second(s) ago"

    # Print report header
    click.echo("")
    _safe_echo(_BOX_TOP, fg='cyan')
    _safe_echo(f"{_BOX_SIDE} IncludeCPP Build Analysis Report", fg='cyan')
    _safe_echo(_BOX_BOTTOM, fg='cyan')
    click.echo("")

    # Build Info Section
    _safe_echo(_SEC_TOP, fg='blue')
    _safe_echo(f"{_SEC_SIDE} Build Information", fg='blue')
    _safe_echo(_SEC_BOTTOM, fg='blue')
    click.echo(f"  Build Directory: {build_dir}")
    click.echo(f"  Compiler:        {compiler}")
    click.echo(f"  Last Build:      {last_build_time.strftime('%Y-%m-%d %H:%M:%S')} ({time_str})")
    click.echo("")

    # Statistics Section
    _safe_echo(_SEC_TOP, fg='green')
    _safe_echo(f"{_SEC_SIDE} Build Statistics", fg='green')
    _safe_echo(_SEC_BOTTOM, fg='green')
    click.echo(f"  Modules:         {total_modules}")
    click.echo(f"  Functions:       {total_functions}")
    click.echo(f"  Classes:         {total_classes}")
    click.echo(f"  Structs:         {total_structs}")
    click.echo(f"  Methods:         {total_methods}")
    click.echo(f"  Total Bindings:  {total_bindings}")
    click.echo("")

    # Compiled Modules Section
    _safe_echo(_SEC_TOP, fg='magenta')
    _safe_echo(f"{_SEC_SIDE} Compiled Modules", fg='magenta')
    _safe_echo(_SEC_BOTTOM, fg='magenta')

    for mod_name, mod_info in modules.items():
        funcs = mod_info.get('functions', [])
        classes = mod_info.get('classes', [])
        structs = mod_info.get('structs', [])

        # Check if .pyd exists
        pyd_path = build_dir / f"{mod_name}.pyd"
        so_path = build_dir / f"{mod_name}.so"
        has_binary = pyd_path.exists() or so_path.exists()

        status_icon = click.style(_CHECK, fg='green') if has_binary else click.style(_CROSS, fg='red')
        _safe_echo(f"  {status_icon} {mod_name}")

        # Show source files
        sources = mod_info.get('sources', [])
        if sources:
            click.echo(f"      Sources: {', '.join(sources)}")

        # Show dependencies
        deps = mod_info.get('depends', [])
        if deps:
            click.echo(f"      Depends: {', '.join(deps)}")

        # Summary line
        summary_parts = []
        if funcs:
            summary_parts.append(f"{len(funcs)} function(s)")
        if classes:
            summary_parts.append(f"{len(classes)} class(es)")
        if structs:
            summary_parts.append(f"{len(structs)} struct(s)")

        if summary_parts:
            click.echo(f"      Exports: {', '.join(summary_parts)}")
        click.echo("")

    # Bindings Details Section
    _safe_echo(_SEC_TOP, fg='yellow')
    _safe_echo(f"{_SEC_SIDE} Bindings Details", fg='yellow')
    _safe_echo(_SEC_BOTTOM, fg='yellow')

    for mod_name, mod_info in modules.items():
        click.secho(f"  {mod_name}:", fg='cyan')

        # Functions
        funcs = mod_info.get('functions', [])
        if funcs:
            click.echo("    Functions:")
            for func in funcs:
                if isinstance(func, dict):
                    name = func.get('name', 'unknown')
                    return_type = func.get('return_type', 'void')
                    params = func.get('params', [])
                    param_str = ', '.join(params) if params else ''
                    _safe_echo(f"      {_ARROW} {return_type} {name}({param_str})")
                else:
                    _safe_echo(f"      {_ARROW} {func}")

        # Classes
        classes = mod_info.get('classes', [])
        if classes:
            click.echo("    Classes:")
            for cls in classes:
                if isinstance(cls, dict):
                    cls_name = cls.get('name', 'unknown')
                    methods = cls.get('methods', [])
                    _safe_echo(f"      {_TRIANGLE} {cls_name}", fg='green')
                    for method in methods:
                        if isinstance(method, dict):
                            m_name = method.get('name', 'unknown')
                            m_return = method.get('return_type', 'void')
                            m_params = method.get('params', [])
                            param_str = ', '.join(m_params) if m_params else ''
                            _safe_echo(f"          .{m_name}({param_str}) {_ARROW} {m_return}")
                        else:
                            click.echo(f"          .{method}()")
                else:
                    _safe_echo(f"      {_TRIANGLE} {cls}")

        # Structs
        structs = mod_info.get('structs', [])
        if structs:
            click.echo("    Structs:")
            for struct in structs:
                if isinstance(struct, dict):
                    s_name = struct.get('name', 'unknown')
                    fields = struct.get('fields', [])
                    _safe_echo(f"      {_DIAMOND} {s_name}", fg='yellow')
                    for field in fields:
                        if isinstance(field, dict):
                            f_name = field.get('name', 'unknown')
                            f_type = field.get('type', 'unknown')
                            click.echo(f"          .{f_name}: {f_type}")
                        else:
                            click.echo(f"          .{field}")
                else:
                    _safe_echo(f"      {_DIAMOND} {struct}")

        click.echo("")

    # Output Files Section
    _safe_echo(_SEC_TOP, fg='white')
    _safe_echo(f"{_SEC_SIDE} Output Files", fg='white')
    _safe_echo(_SEC_BOTTOM, fg='white')

    click.echo("  Binary Modules (.pyd/.so):")
    if pyd_files:
        for pyd in pyd_files:
            size_kb = pyd.stat().st_size / 1024
            _safe_echo(f"    {_CHECK} {pyd.name}", fg='green', nl=False)
            click.echo(f" ({size_kb:.1f} KB)")
    else:
        click.secho("    (none found)", fg='yellow')

    click.echo("")
    click.echo("  Type Stubs (.pyi):")
    if pyi_files:
        for pyi in pyi_files:
            _safe_echo(f"    {_CHECK} {pyi.name}", fg='green')
    else:
        click.secho("    (none found)", fg='yellow')

    click.echo("")

    # Usage Example
    _safe_echo(_SEC_TOP, fg='cyan')
    _safe_echo(f"{_SEC_SIDE} Usage Example", fg='cyan')
    _safe_echo(_SEC_BOTTOM, fg='cyan')

    if modules:
        first_mod = list(modules.keys())[0]
        first_info = modules[first_mod]
        first_func = None
        if first_info.get('functions'):
            f = first_info['functions'][0]
            first_func = f.get('name', f) if isinstance(f, dict) else f

        click.echo(f"  from includecpp import CppApi")
        click.echo(f"  api = CppApi()")
        click.echo(f"  {first_mod} = api.include(\"{first_mod}\")")
        if first_func:
            click.echo(f"  result = {first_mod}.{first_func}(...)")

    click.echo("")
    _safe_echo(_HLINE * 64, fg='cyan')
    click.echo("")


def _check_for_updates_silent():
    """Check PyPI for a newer version of IncludeCPP. Returns (current, latest) or None."""
    try:
        from .. import __version__ as current_version
        import urllib.request
        import json

        req = urllib.request.Request(
            "https://pypi.org/pypi/IncludeCPP/json",
            headers={"User-Agent": "IncludeCPP-CLI"}
        )
        with urllib.request.urlopen(req, timeout=3) as response:
            data = json.loads(response.read().decode('utf-8'))
            latest_version = data.get('info', {}).get('version', '')

            if latest_version and latest_version != current_version:
                # Compare versions
                def parse_version(v):
                    try:
                        return tuple(int(x) for x in v.split('.'))
                    except:
                        return (0, 0, 0)

                if parse_version(latest_version) > parse_version(current_version):
                    return (current_version, latest_version)
    except:
        pass  # Silently fail - don't interrupt user's workflow
    return None


def _parse_cp_sources(cp_path):
    """Parse SOURCE() and HEADER() paths from an existing .cp file.

    Returns:
        Tuple of (source_files, header_files) as lists of Path objects
    """
    import re
    from pathlib import Path

    source_files = []
    header_files = []

    try:
        content = cp_path.read_text()
        project_root = cp_path.parent.parent  # plugins/ -> project root

        # Find SOURCE(...) declarations
        source_match = re.search(r'SOURCE\s*\(\s*([^)]+)\s*\)', content)
        if source_match:
            sources_str = source_match.group(1)
            # Handle multiple files: SOURCE(file1.cpp file2.cpp) or SOURCE(file1.cpp, file2.cpp)
            sources = re.split(r'[,\s]+', sources_str.strip())
            for src in sources:
                src = src.strip()
                if src:
                    p = Path(src)
                    if not p.is_absolute():
                        p = project_root / src
                    if p.exists():
                        source_files.append(p)

        # Find HEADER(...) declarations
        header_match = re.search(r'HEADER\s*\(\s*([^)]+)\s*\)', content)
        if header_match:
            headers_str = header_match.group(1)
            headers = re.split(r'[,\s]+', headers_str.strip())
            for hdr in headers:
                hdr = hdr.strip()
                if hdr:
                    p = Path(hdr)
                    if not p.is_absolute():
                        p = project_root / hdr
                    if p.exists():
                        header_files.append(p)

    except Exception as e:
        pass  # Return empty lists on error

    return (source_files, header_files)


@cli.command()
@click.option('--clean', is_flag=True, help='Force clean rebuild (ignore incremental)')
@click.option('--keep', is_flag=True, help='Keep existing plugin_gen.exe (skip generator rebuild)')
@click.option('--verbose', is_flag=True, help='Verbose output')
@click.option('--no-incremental', is_flag=True, help='Disable incremental builds')
@click.option('--incremental', is_flag=True, help='Explicitly enable incremental builds (default: auto)')
@click.option('--parallel/--no-parallel', default=True, help='Enable/disable parallel compilation (default: enabled)')
@click.option('--jobs', '-j', type=int, default=4, help='Max parallel jobs (default: 4)')
@click.option('--modules', '-m', multiple=True, help='Specific modules to rebuild')
@click.option('--this', 'build_paths', multiple=True, type=click.Path(exists=True),
              help='Build only from specific paths (can specify multiple)')
@click.option('--fast', 'fast_mode', is_flag=True,
              help='Fast incremental rebuild mode (use with module name, e.g., rebuild --fast gamekit)')
@click.option('--all', 'fast_all', is_flag=True, help='With --fast: rebuild all plugins incrementally')
@click.option('--info', is_flag=True, help='Show detailed analysis report of the last build')
@click.argument('module_args', nargs=-1)
def rebuild(clean, keep, verbose, no_incremental, incremental, parallel, jobs, modules,
            build_paths, fast_mode, fast_all, info, module_args):
    """Rebuild C++ modules with automatic generator updates."""
    from ..core.build_manager import BuildManager
    from ..core.error_formatter import BuildErrorFormatter, BuildSuccessFormatter
    import time
    import json
    from datetime import datetime

    # Combine -m modules with positional arguments
    # Allows both: includecpp rebuild -m gamekit AND includecpp rebuild gamekit
    all_modules = list(modules) + list(module_args)
    modules = tuple(all_modules) if all_modules else modules

    # v2.4.13: Check for updates (non-blocking)
    update_info = _check_for_updates_silent()
    if update_info:
        current_ver, latest_ver = update_info
        click.echo()
        click.secho("[INFO] ", fg='bright_black', nl=False)
        click.echo("A new version of IncludeCPP is available: ", nl=False)
        click.secho(f"{current_ver}", fg='yellow', nl=False)
        click.echo(" -> ", nl=False)
        click.secho(f"{latest_ver}", fg='green')
        click.secho("[INFO] ", fg='bright_black', nl=False)
        click.echo("To update, run: ", nl=False)
        click.secho("includecpp update", fg='cyan')
        click.echo()

    project_root = Path.cwd()
    config = CppProjectConfig()

    compiler = detect_compiler()
    build_dir = config.get_build_dir(compiler)

    # Handle --info flag: Show detailed build analysis report
    if info:
        if verbose:
            click.echo(f"DEBUG: project_root = {project_root}")
            click.echo(f"DEBUG: compiler = {compiler}")
            click.echo(f"DEBUG: build_dir = {build_dir}")
            click.echo(f"DEBUG: build_dir.exists() = {build_dir.exists()}")
            registry_path = build_dir / "module_registry.json"
            click.echo(f"DEBUG: registry_path = {registry_path}")
            click.echo(f"DEBUG: registry_path.exists() = {registry_path.exists()}")
            if build_dir.exists():
                click.echo(f"DEBUG: Files in build_dir:")
                for f in build_dir.iterdir():
                    click.echo(f"  - {f.name}")
        _show_build_info_report(build_dir, compiler)
        return

    # v2.5.4: By default, always delete the generator to ensure it's up-to-date
    # Use --keep to skip generator rebuild
    if build_dir.exists():
        gen_path = build_dir / "bin" / ".appc"
        if not keep and gen_path.exists():
            if verbose:
                click.echo("Removing old generator to force rebuild...")
            shutil.rmtree(gen_path)

    # Clean if requested - delete entire build directory
    if clean and build_dir.exists():
        click.echo("Cleaning build directory...")
        shutil.rmtree(build_dir)

    build_dir.mkdir(parents=True, exist_ok=True)
    config.update_base_dir(build_dir)

    click.echo(f"Build directory: {build_dir}")

    if incremental and no_incremental:
        click.secho("Error: Cannot use both --incremental and --no-incremental", fg='red', err=True)
        raise click.Abort()

    if clean and incremental:
        click.secho("Warning: --clean overrides --incremental", fg='yellow')
        incremental = False

    if fast_mode and build_paths:
        click.secho("Error: Cannot use both --fast and --this", fg='red', err=True)
        raise click.Abort()

    # Validate --all requires --fast
    if fast_all and not fast_mode:
        click.secho("Error: --all requires --fast", fg='red', err=True)
        raise click.Abort()

    # Determine fast plugin from modules (positional args or -m option)
    fast_plugin = None
    if fast_mode and not fast_all:
        if modules:
            fast_plugin = modules[0]  # Use first module from -m option
        elif module_args:
            fast_plugin = module_args[0]  # Use first positional argument
        else:
            click.secho("Error: --fast requires a module name", fg='red', err=True)
            click.echo("Usage: includecpp rebuild --fast <module_name>")
            click.echo("   or: includecpp rebuild -m <module_name> --fast")
            raise click.Abort()

    final_incremental = True
    if incremental:
        final_incremental = True
    elif no_incremental or clean:
        final_incremental = False

    discovered_modules = []
    if build_paths:
        if verbose:
            click.echo(f"Discovering modules in {len(build_paths)} path(s)...")

        from ..core.path_discovery import PathDiscovery
        discoverer = PathDiscovery(project_root, config)

        for path in build_paths:
            path_obj = Path(path)
            found = discoverer.discover_modules_in_path(path_obj)
            discovered_modules.extend(found)
            if verbose:
                click.echo(f"  {path}: found {len(found)} module(s)")

        if discovered_modules:
            modules = tuple(discovered_modules)
            if verbose:
                click.echo(f"Building {len(modules)} discovered module(s): {', '.join(modules)}")
        else:
            click.secho("No modules found in specified paths", fg='yellow')
            raise click.Abort()

    if fast_plugin and not fast_all:
        if verbose:
            click.echo(f"Fast rebuild mode: {fast_plugin}")

        # Extract module name (strip .cp extension and path prefix)
        fast_module_name = fast_plugin
        if fast_module_name.endswith('.cp'):
            fast_module_name = fast_module_name[:-3]
        # Strip any path prefix (plugins/math_utils -> math_utils)
        if '/' in fast_module_name:
            fast_module_name = fast_module_name.split('/')[-1]
        if '\\' in fast_module_name:
            fast_module_name = fast_module_name.split('\\')[-1]

        # Use the already-imported BuildManager (from line 115)
        temp_builder = BuildManager(project_root, build_dir, config)
        registry = temp_builder._load_registry()
        available_modules = registry.get('modules', {})

        if fast_module_name not in available_modules:
            click.secho(f"Module '{fast_module_name}' not found", fg='red', err=True)
            click.echo(f"Available modules: {', '.join(available_modules.keys())}")
            raise click.Abort()

        dep_graph = temp_builder._build_dependency_graph(available_modules)
        affected_modules = temp_builder._get_affected_modules(fast_module_name, dep_graph, available_modules)

        modules = tuple(affected_modules)
        final_incremental = True
        clean = False

        if verbose:
            click.echo(f"Affected modules ({len(modules)}): {', '.join(modules)}")

    if fast_mode and fast_all:
        if verbose:
            click.echo("Fast rebuild mode: ALL plugins")

        final_incremental = True
        clean = False
        modules = None

        if verbose:
            click.echo("Will rebuild all changed plugins incrementally")

    # v2.0: Show build configuration
    if verbose:
        click.echo(f"Incremental: {final_incremental}")
        click.echo(f"Parallel: {parallel}")
        if parallel:
            click.echo(f"Max jobs: {jobs}")
        if modules:
            click.echo(f"Target modules: {', '.join(modules)}")

    # Build start message
    build_type = "Incremental" if final_incremental else "Full"
    target_modules = list(modules) if modules else []

    click.echo("")
    _safe_echo(_BOX_TOP, fg='cyan')
    _safe_echo(f"{_BOX_SIDE} IncludeCPP {build_type} Build", fg='cyan')
    _safe_echo(_BOX_BOTTOM, fg='cyan')
    click.echo(f"Modules: {len(target_modules) if target_modules else 'all'}")
    click.echo("")

    start_time = time.time()

    try:
        builder = BuildManager(project_root, build_dir, config)

        success = builder.rebuild(
            modules=list(modules) if modules else None,
            incremental=final_incremental,
            parallel=parallel,
            clean=clean,
            verbose=verbose
        )

        build_time = time.time() - start_time

        if success:
            # Get built modules from registry for success message
            registry = builder._load_registry()
            built_modules = list(registry.get('modules', {}).keys()) if not target_modules else target_modules

            # Collect stats
            stats = {}
            total_funcs = 0
            total_classes = 0
            total_structs = 0
            for mod_name, mod_info in registry.get('modules', {}).items():
                if not target_modules or mod_name in target_modules:
                    total_funcs += len(mod_info.get('functions', []))
                    total_classes += len(mod_info.get('classes', []))
                    total_structs += len(mod_info.get('structs', []))

            if total_funcs > 0:
                stats['total_functions'] = total_funcs
            if total_classes > 0:
                stats['total_classes'] = total_classes
            if total_structs > 0:
                stats['total_structs'] = total_structs

            # Print success message
            click.echo("")
            _safe_echo(_BOX_TOP, fg='green')
            _safe_echo(f"{_BOX_SIDE} BUILD SUCCESSFUL", fg='green')
            _safe_echo(_BOX_BOTTOM, fg='green')
            click.echo("")
            click.echo(f"Modules built ({len(built_modules)}):")
            for mod in built_modules:
                _safe_echo(f"  {mod}", fg='green')
            click.echo("")
            click.echo(f"Build time: {build_time:.2f}s")
            if stats:
                if 'total_functions' in stats:
                    click.echo(f"  Functions exported: {stats['total_functions']}")
                if 'total_classes' in stats:
                    click.echo(f"  Classes exported:   {stats['total_classes']}")
                if 'total_structs' in stats:
                    click.echo(f"  Structs exported:   {stats['total_structs']}")
            click.echo("")
            # Show usage with real module name from build
            if built_modules:
                first_mod = built_modules[0]
                first_func = None
                if first_mod in modules and modules[first_mod].get('functions'):
                    func_info = modules[first_mod]['functions'][0]
                    first_func = func_info.get('name', func_info) if isinstance(func_info, dict) else func_info

                click.echo("Usage:")
                click.echo("  from includecpp import CppApi")
                click.echo("  api = CppApi()")
                click.echo(f"  {first_mod} = api.include(\"{first_mod}\")")
                if first_func:
                    click.echo(f"  result = {first_mod}.{first_func}(...)")
            click.echo("")
        else:
            click.echo("")
            _safe_echo(_BOX_TOP, fg='red')
            _safe_echo(f"{_BOX_SIDE} {_CROSS} BUILD FAILED", fg='red')
            _safe_echo(_BOX_BOTTOM, fg='red')
            click.echo("")
            click.echo("To fix:")
            click.echo(f"  {_BULLET} Check the error messages above")
            click.echo(f"  {_BULLET} Run with --verbose for more details")
            click.echo(f"  {_BULLET} Fix errors and run: python -m includecpp rebuild")
            click.echo("")
            raise click.Abort()

    except click.Abort:
        raise
    except Exception as e:
        build_time = time.time() - start_time
        error_msg = str(e)
        module_name = target_modules[0] if target_modules else ""

        # v2.7: Print verbose details FIRST (for those who read everything)
        if verbose:
            click.echo("")
            _safe_echo(_HLINE * 60, fg='yellow')
            click.secho("Full Stack Trace (--verbose):", fg='yellow')
            _safe_echo(_HLINE * 60, fg='yellow')
            import traceback
            traceback.print_exc()
            click.echo("")

        # v2.7: Print detailed error analysis
        formatted_error = BuildErrorFormatter.analyze_error(error_msg, module_name)
        click.echo("")
        click.secho(formatted_error, fg='red', err=True)

        # v2.7: Print short helpful message LAST (for lazy users who only read the end)
        final_msg = BuildErrorFormatter.get_final_message(error_msg, module_name)
        click.echo("")
        click.secho(final_msg, fg='red', bold=True, err=True)

        raise click.Abort()

@cli.command()
@click.option('--clean', is_flag=True, help='Force clean rebuild')
@click.option('--keep', is_flag=True, help='Keep existing plugin_gen.exe')
@click.option('-v', '--verbose', is_flag=True, help='Show detailed output')
@click.option('--no-incremental', is_flag=True, help='Disable incremental builds')
@click.option('--incremental', is_flag=True, help='Force incremental build')
@click.option('--parallel', is_flag=True, help='Build modules in parallel')
@click.option('-j', '--jobs', type=int, default=None, help='Number of parallel jobs')
@click.option('-m', '--modules', multiple=True, help='Specific modules to build')
@click.option('--this', 'build_paths', multiple=True, type=click.Path(exists=True), help='Build from path')
@click.option('--fast', 'fast_mode', is_flag=True, help='Fast incremental build')
@click.option('--all', 'fast_all', is_flag=True, help='With --fast: rebuild all plugins')
@click.option('--info', is_flag=True, help='Show build analysis report')
@click.argument('module_args', nargs=-1)
@click.pass_context
def build(ctx, clean, keep, verbose, no_incremental, incremental, parallel, jobs, modules,
          build_paths, fast_mode, fast_all, info, module_args):
    """Build C++ modules (alias for rebuild)."""
    ctx.invoke(rebuild, clean=clean, keep=keep, verbose=verbose, no_incremental=no_incremental,
               incremental=incremental, parallel=parallel, jobs=jobs, modules=modules,
               build_paths=build_paths, fast_mode=fast_mode, fast_all=fast_all, info=info,
               module_args=module_args)

@cli.command()
@click.argument('module_name')
def add(module_name):
    """Create a new module template."""
    plugins_dir = Path("plugins")
    if not plugins_dir.exists():
        click.echo("Error: plugins/ directory not found")
        return

    cp_file = plugins_dir / f"{module_name}.cp"
    if cp_file.exists():
        click.echo(f"Module {module_name} already exists")
        return

    template = f"""SOURCE(include/{module_name}.cpp) {module_name}

PUBLIC(
    {module_name} FUNC(example_function)
)
"""

    with open(cp_file, 'w') as f:
        f.write(template)

    click.echo(f"Created {cp_file}")
    click.echo(f"Now create include/{module_name}.cpp with your C++ code")

@cli.command('list')
def list_modules():
    """List all available C++ modules."""
    from ..core.build_manager import BuildManager

    project_root = Path.cwd()
    config = CppProjectConfig()
    compiler = detect_compiler()
    build_dir = config.get_build_dir(compiler)

    if not build_dir.exists():
        click.echo("No build directory found. Run 'python -m includecpp rebuild' first.")
        return

    try:
        builder = BuildManager(project_root, build_dir, config)
        registry = builder._load_registry()

        modules = registry.get('modules', {})
        if not modules:
            click.echo("No modules found. Run 'python -m includecpp rebuild' to build modules.")
            return

        click.echo(f"\nAvailable modules ({len(modules)}):")
        click.echo("=" * 60)

        for module_name, module_info in sorted(modules.items()):
            # Get source count
            sources = module_info.get('sources', [])
            source_count = len(sources)

            # Get dependency count
            deps = module_info.get('dependencies', [])
            dep_count = len(deps)

            # Get struct/class/function counts
            structs = module_info.get('structs', [])
            classes = module_info.get('classes', [])
            functions = module_info.get('functions', [])

            click.echo(f"\n  {module_name}")
            click.echo(f"    Sources: {source_count} file(s)")

            if dep_count > 0:
                dep_names = [d.get('target', '?') for d in deps]
                click.echo(f"    Dependencies: {', '.join(dep_names)}")

            if structs:
                click.echo(f"    Structs: {len(structs)}")
            if classes:
                click.echo(f"    Classes: {len(classes)}")
            if functions:
                click.echo(f"    Functions: {len(functions)}")

        click.echo("\n" + "=" * 60)
        click.echo(f"Total: {len(modules)} module(s)\n")

    except Exception as e:
        click.echo(f"Error: {e}", err=True)

@cli.command()
@click.argument('module_name')
def info(module_name):
    """Show detailed information about a module."""
    from ..core.build_manager import BuildManager

    project_root = Path.cwd()
    config = CppProjectConfig()
    compiler = detect_compiler()
    build_dir = config.get_build_dir(compiler)

    if not build_dir.exists():
        click.echo("No build directory found. Run 'python -m includecpp rebuild' first.")
        return

    try:
        builder = BuildManager(project_root, build_dir, config)
        registry = builder._load_registry()

        modules = registry.get('modules', {})
        if module_name not in modules:
            click.echo(f"Module '{module_name}' not found.")
            click.echo(f"Available modules: {', '.join(sorted(modules.keys()))}")
            return

        module_info = modules[module_name]

        click.echo(f"\nModule: {module_name}")
        click.echo("=" * 60)

        # Sources
        sources = module_info.get('sources', [])
        click.echo(f"\nSources ({len(sources)}):")
        for src in sources:
            click.echo(f"  {_BULLET} {src}")

        # Dependencies
        deps = module_info.get('dependencies', [])
        if deps:
            click.echo(f"\nDependencies ({len(deps)}):")
            for dep in deps:
                target = dep.get('target', '?')
                types = dep.get('types', [])
                if types:
                    click.echo(f"  {_BULLET} {target} (types: {', '.join(types)})")
                else:
                    click.echo(f"  {_BULLET} {target}")

        # Structs
        structs = module_info.get('structs', [])
        if structs:
            click.echo(f"\nStructs ({len(structs)}):")
            for struct in structs:
                name = struct.get('name', '?')
                is_template = struct.get('is_template', False)
                if is_template:
                    template_types = struct.get('template_types', [])
                    click.echo(f"  {_BULLET} {name}<{', '.join(template_types)}>")
                else:
                    click.echo(f"  {_BULLET} {name}")

                fields = struct.get('fields', [])
                if fields:
                    for field in fields:
                        field_type = field.get('type', '?')
                        field_name = field.get('name', '?')
                        click.echo(f"      - {field_type} {field_name}")

        # Classes
        classes = module_info.get('classes', [])
        if classes:
            click.echo(f"\nClasses ({len(classes)}):")
            for cls in classes:
                name = cls.get('name', '?')
                methods = cls.get('methods', [])
                click.echo(f"  {_BULLET} {name}")
                if methods:
                    # v2.8: Fix for methods being list of dicts
                    method_names = [m.get('name', m) if isinstance(m, dict) else m for m in methods]
                    click.echo(f"      Methods: {', '.join(method_names)}")

        # Functions
        functions = module_info.get('functions', [])
        if functions:
            click.echo(f"\nFunctions ({len(functions)}):")
            for func in functions:
                name = func.get('name', '?')
                click.echo(f"  {_BULLET} {name}()")

        # Build info
        last_built = module_info.get('last_built', 'Never')
        build_time = module_info.get('build_time_ms', 0)
        click.echo(f"\nLast Built: {last_built}")
        if build_time > 0:
            click.echo(f"Build Time: {build_time}ms")

        click.echo("=" * 60 + "\n")

    except Exception as e:
        click.echo(f"Error: {e}", err=True)

@cli.command()
@click.argument('module_name')
def get(module_name):
    """Display detailed module API information with full signatures.

    Shows all classes, methods, functions, structs with complete
    type information, parameters, and documentation.
    """
    from ..core.build_manager import BuildManager

    project_root = Path.cwd()
    config = CppProjectConfig()
    compiler = detect_compiler()
    build_dir = config.get_build_dir(compiler)

    if not build_dir.exists():
        click.echo("")
        _safe_echo(_BOX_TOP, fg='red')
        _safe_echo(f"{_BOX_SIDE}  BUILD NOT FOUND", fg='red')
        _safe_echo(_BOX_BOTTOM, fg='red')
        click.echo("\nNo build directory found.")
        click.echo("\nTo fix this, run:")
        click.secho("  python -m includecpp rebuild", fg='cyan')
        return

    try:
        builder = BuildManager(project_root, build_dir, config)
        registry = builder._load_registry()

        modules = registry.get('modules', {})
        if module_name not in modules:
            click.echo("")
            _safe_echo(_BOX_TOP, fg='red')
            _safe_echo(f"{_BOX_SIDE}  MODULE NOT FOUND", fg='red')
            _safe_echo(_BOX_BOTTOM, fg='red')
            click.echo(f"\nModule '{module_name}' not found in registry.")
            click.echo(f"\nAvailable modules:")
            for mod in sorted(modules.keys()):
                click.echo(f"  {_BULLET} {mod}")
            click.echo(f"\nTo rebuild all modules:")
            click.secho("  python -m includecpp rebuild", fg='cyan')
            return

        module_info = modules[module_name]

        # Header
        click.echo()
        _safe_echo("+" + "-" * 70 + "+", fg='cyan', bold=True)
        title = f"  Module: {module_name}"
        padding = 70 - len(title) - 1
        _safe_echo(f"|{title}" + " " * padding + "|", fg='cyan', bold=True)
        _safe_echo("+" + "-" * 70 + "+", fg='cyan', bold=True)

        # Sources section
        sources = module_info.get('sources', [])
        click.echo()
        _safe_echo("+-- Sources " + "-" * 58 + "+", fg='blue')
        for src in sources:
            click.echo(f"|  {src}")
        _safe_echo("+" + "-" * 69 + "+", fg='blue')

        # Dependencies section
        deps = module_info.get('dependencies', [])
        if deps:
            click.echo()
            _safe_echo("+-- Dependencies " + "-" * 53 + "+", fg='yellow')
            for dep in deps:
                target = dep.get('target', '?')
                types = dep.get('types', [])
                optional = dep.get('optional', False)
                opt_str = " (optional)" if optional else ""
                if types:
                    _safe_echo(f"|  {target}{opt_str} {_ARROW} types: {', '.join(types)}")
                else:
                    click.echo(f"|  {target}{opt_str}")
            _safe_echo("+" + "-" * 69 + "+", fg='yellow')

        # Classes section
        classes = module_info.get('classes', [])
        if classes:
            click.echo()
            _safe_echo("+-- Classes " + "-" * 58 + "+", fg='green')
            for cls in classes:
                cls_name = cls.get('name', '?')
                cls_doc = cls.get('doc', '')
                methods = cls.get('methods', [])

                click.secho(f"|", fg='green')
                click.secho(f"|  class ", fg='green', nl=False)
                click.secho(f"{cls_name}", fg='white', bold=True)

                if cls_doc:
                    # Wrap documentation
                    click.secho(f"|     -- {cls_doc[:60]}{'...' if len(cls_doc) > 60 else ''}", fg='bright_black')

                if methods:
                    click.secho(f"|", fg='green')
                    click.secho(f"|     Methods:", fg='green')

                    for method_info in methods:
                        if isinstance(method_info, dict):
                            method_name = method_info.get('name', '?')
                            method_doc = method_info.get('doc', '')
                            return_type = method_info.get('return_type', 'void')
                            params = method_info.get('parameters', [])
                            is_const = method_info.get('const', False)
                            is_static = method_info.get('static', False)

                            # Build parameter string
                            param_strs = []
                            for p in params:
                                p_type = p.get('type', '?')
                                p_name = p.get('name', '?')
                                p_default = p.get('default', '')
                                if p.get('const'):
                                    p_type = f"const {p_type}"
                                if p.get('reference'):
                                    p_type += "&"
                                if p.get('pointer'):
                                    p_type += "*"
                                if p_default:
                                    param_strs.append(f"{p_type} {p_name} = {p_default}")
                                else:
                                    param_strs.append(f"{p_type} {p_name}")

                            params_str = ", ".join(param_strs) if param_strs else ""
                            const_str = " const" if is_const else ""
                            static_str = "static " if is_static else ""

                            click.echo(f"|       +- ", nl=False)
                            click.secho(f"{static_str}{return_type} ", fg='magenta', nl=False)
                            click.secho(f"{method_name}", fg='white', bold=True, nl=False)
                            click.echo(f"({params_str}){const_str}")

                            if method_doc:
                                click.secho(f"|       |     -- {method_doc[:55]}{'...' if len(method_doc) > 55 else ''}", fg='bright_black')
                        else:
                            # Simple string method name (legacy format)
                            click.echo(f"|       +- {method_info}()")

            click.secho("|", fg='green')
            _safe_echo("+" + "-" * 69 + "+", fg='green')

        # Structs section
        structs = module_info.get('structs', [])
        if structs:
            click.echo()
            _safe_echo("+-- Structs " + "-" * 58 + "+", fg='magenta')
            for struct in structs:
                struct_name = struct.get('name', '?')
                struct_doc = struct.get('doc', '')
                is_template = struct.get('is_template', False)
                template_types = struct.get('template_types', [])
                fields = struct.get('fields', [])

                click.secho(f"|", fg='magenta')
                if is_template:
                    click.secho(f"|  struct ", fg='magenta', nl=False)
                    click.secho(f"{struct_name}<{', '.join(template_types)}>", fg='white', bold=True)
                else:
                    click.secho(f"|  struct ", fg='magenta', nl=False)
                    click.secho(f"{struct_name}", fg='white', bold=True)

                if struct_doc:
                    click.secho(f"|     -- {struct_doc[:60]}{'...' if len(struct_doc) > 60 else ''}", fg='bright_black')

                if fields:
                    click.secho(f"|", fg='magenta')
                    click.secho(f"|     Fields:", fg='magenta')
                    for field in fields:
                        field_type = field.get('type', '?')
                        field_name = field.get('name', '?')
                        click.echo(f"|       +- ", nl=False)
                        click.secho(f"{field_type} ", fg='cyan', nl=False)
                        click.echo(f"{field_name}")

            click.secho("|", fg='magenta')
            _safe_echo("+" + "-" * 69 + "+", fg='magenta')

        # Functions section
        functions = module_info.get('functions', [])
        if functions:
            click.echo()
            _safe_echo("+-- Functions " + "-" * 56 + "+", fg='cyan')
            for func in functions:
                func_name = func.get('name', '?')
                func_doc = func.get('doc', '')
                return_type = func.get('return_type', 'void')
                params = func.get('parameters', [])
                is_static = func.get('static', False)
                is_const = func.get('const', False)
                is_inline = func.get('inline', False)

                # Build parameter string with types
                param_strs = []
                for p in params:
                    p_type = p.get('type', '?')
                    p_name = p.get('name', '?')
                    p_default = p.get('default', '')
                    if p.get('const'):
                        p_type = f"const {p_type}"
                    if p.get('reference'):
                        p_type += "&"
                    if p.get('pointer'):
                        p_type += "*"
                    if p_default:
                        param_strs.append(f"{p_type} {p_name} = {p_default}")
                    else:
                        param_strs.append(f"{p_type} {p_name}")

                params_str = ", ".join(param_strs) if param_strs else ""
                qualifiers = []
                if is_static:
                    qualifiers.append("static")
                if is_inline:
                    qualifiers.append("inline")
                if is_const:
                    qualifiers.append("const")
                qualifier_str = " ".join(qualifiers) + " " if qualifiers else ""

                click.secho(f"|", fg='cyan')
                click.echo(f"|  ", nl=False)
                click.secho(f"{qualifier_str}{return_type} ", fg='yellow', nl=False)
                click.secho(f"{func_name}", fg='white', bold=True, nl=False)
                click.echo(f"({params_str})")

                if func_doc:
                    click.secho(f"|     -- {func_doc[:60]}{'...' if len(func_doc) > 60 else ''}", fg='bright_black')

                # Show parameters in detail if any
                if params:
                    click.secho(f"|     Parameters:", fg='cyan')
                    for p in params:
                        p_type = p.get('type', '?')
                        p_name = p.get('name', '?')
                        p_default = p.get('default', '')
                        type_info = []
                        if p.get('const'):
                            type_info.append("const")
                        if p.get('reference'):
                            type_info.append("ref")
                        if p.get('pointer'):
                            type_info.append("ptr")
                        type_suffix = f" [{', '.join(type_info)}]" if type_info else ""
                        default_str = f" = {p_default}" if p_default else ""
                        click.echo(f"|       +- {p_name}: ", nl=False)
                        click.secho(f"{p_type}", fg='cyan', nl=False)
                        click.echo(f"{type_suffix}{default_str}")

            click.secho("|", fg='cyan')
            _safe_echo("+" + "-" * 69 + "+", fg='cyan')

        # Usage section
        click.echo()
        _safe_echo("+-- Usage Example " + "-" * 52 + "+", fg='white')
        click.echo(f"|")
        click.echo(f"|  from includecpp import CppApi")
        click.echo(f"|")
        click.echo(f"|  cpp = CppApi()")
        click.echo(f"|  {module_name} = cpp.include(\"{module_name}\")")
        click.echo(f"|")
        if functions:
            first_func = functions[0].get('name', 'function')
            click.echo(f"|  # Call a function")
            click.echo(f"|  result = {module_name}.{first_func}(...)")
        if classes:
            first_cls = classes[0].get('name', 'Class')
            click.echo(f"|")
            click.echo(f"|  # Use a class")
            click.echo(f"|  obj = {module_name}.{first_cls}()")
        click.echo(f"|")
        _safe_echo("+" + "-" * 69 + "+", fg='white')
        click.echo()

    except Exception as e:
        click.echo("")
        _safe_echo(_BOX_TOP, fg='red')
        _safe_echo(f"{_BOX_SIDE}  ERROR", fg='red')
        _safe_echo(_BOX_BOTTOM, fg='red')
        click.echo(f"\n{e}")

@cli.command()
@click.argument('module_name', required=False)
@click.option('--list-all', is_flag=True, help='List all available modules')
def install(module_name, list_all):
    """Install a module from GitHub: https://github.com/liliassg/IncludeCPP/minstall/{module}"""
    import tempfile
    import urllib.request
    import urllib.error
    import json

    # Handle --list-all option
    if list_all:
        click.echo("=" * 60)
        click.secho("Available IncludeCPP Modules", fg='cyan', bold=True)
        click.echo("=" * 60)
        click.echo(f"Source: https://github.com/liliassg/IncludeCPP/tree/main/minstall")
        click.echo()

        try:
            # Fetch directory listing from GitHub API
            api_url = "https://api.github.com/repos/liliassg/IncludeCPP/contents/minstall"
            click.echo("  Fetching module list...", nl=False)

            with urllib.request.urlopen(api_url) as response:
                data = json.loads(response.read())
                click.secho(" OK", fg='green')
                click.echo()

            # Extract module names (directories)
            modules = [item['name'] for item in data if item['type'] == 'dir']

            if not modules:
                click.secho("  No modules found.", fg='yellow')
            else:
                click.secho(f"Found {len(modules)} module(s):", fg='green', bold=True)
                click.echo()
                for module in sorted(modules):
                    click.echo(f"  {_BULLET} {module}")

                click.echo()
                click.echo("Install a module with:")
                click.echo("  includecpp install <module_name>")

        except urllib.error.HTTPError as e:
            click.secho(f"  FAILED (HTTP {e.code})", fg='red', err=True)
            click.echo()
            click.echo("Could not fetch module list from GitHub.")
        except Exception as e:
            click.secho(f"  FAILED ({e})", fg='red', err=True)

        click.echo("=" * 60)
        return

    # Require module_name if not using --list-all
    if not module_name:
        click.secho("Error: Missing argument 'MODULE_NAME'.", fg='red', err=True)
        click.echo("Use 'includecpp install --list-all' to see available modules.")
        return

    # Check if include and plugins directories exist
    include_dir = Path.cwd() / "include"
    plugins_dir = Path.cwd() / "plugins"

    if not include_dir.exists() or not plugins_dir.exists():
        click.secho("Error: Not in a valid IncludeCPP project directory.", fg='red', err=True)
        click.echo("Run 'python -m includecpp init' first.")
        return

    click.echo("=" * 60)
    click.secho(f"Collecting {module_name}", fg='cyan', bold=True)
    click.echo("=" * 60)
    click.echo(f"Source: https://github.com/liliassg/IncludeCPP/tree/main/minstall/{module_name}")
    click.echo()

    # GitHub raw content base URL
    base_url = f"https://raw.githubusercontent.com/liliassg/IncludeCPP/main/minstall/{module_name}/"

    # Try to download .cp, .cpp, and .h files
    files_to_download = [
        (f"{module_name}.cp", plugins_dir, "Plugin definition"),
        (f"{module_name}.cpp", include_dir, "C++ implementation"),
        (f"{module_name}.h", include_dir, "C++ header")
    ]

    downloaded = []

    for filename, target_dir, description in files_to_download:
        url = base_url + filename
        target_path = target_dir / filename

        try:
            click.echo(f"  Collecting {filename:20} ({description})...", nl=False)
            with urllib.request.urlopen(url) as response:
                content = response.read()

                # Write to target directory
                with open(target_path, 'wb') as f:
                    f.write(content)

                downloaded.append(filename)
                file_size_kb = len(content) / 1024
                click.secho(f" OK ({file_size_kb:.1f} KB)", fg='green')

        except urllib.error.HTTPError as e:
            if e.code == 404:
                # File doesn't exist, skip (optional file like .h)
                click.secho(" SKIP (optional)", fg='yellow')
            else:
                click.secho(f" FAILED ({e})", fg='red', err=True)
        except Exception as e:
            click.secho(f" FAILED ({e})", fg='red', err=True)

    click.echo()

    if not downloaded:
        click.secho(f"Error: Module '{module_name}' not found or no files available.", fg='red', err=True)
        click.echo(f"Check https://github.com/liliassg/IncludeCPP/tree/main/minstall for available modules.")
        return

    click.secho(f"Successfully installed {len(downloaded)} file(s):", fg='green', bold=True)
    for f in downloaded:
        click.echo(f"  - {f}")

    click.echo()
    click.secho(f"Module '{module_name}' installed successfully!", fg='green', bold=True)
    click.echo("Run 'python -m includecpp rebuild' to build the module.")
    click.echo("=" * 60)

@cli.command()
@click.argument('module_name', required=False)
@click.option('--list-all', is_flag=True, help='List all available modules')
def minstall(module_name, list_all):
    """Alias for 'install' - Install modules from GitHub minstall repository."""
    # Call the install command with the same arguments
    ctx = click.get_current_context()
    ctx.invoke(install, module_name=module_name, list_all=list_all)

@cli.command()
@click.argument('target_version', required=False)
@click.option('--version', 'show_version', is_flag=True, help='Show current installed version')
@click.option('--all', 'list_all', is_flag=True, help='List all available versions from PyPI')
def update(target_version, show_version, list_all):
    """Update IncludeCPP package from PyPI.

    Usage:
      includecpp update              Upgrade to latest version
      includecpp update --version    Show current version
      includecpp update --all        List all available versions
      includecpp update X.X.X        Install specific version
    """
    import subprocess
    import urllib.request
    import json

    from .. import __version__ as current_version

    # --version: Show current version
    if show_version:
        click.echo("=" * 60)
        click.secho("IncludeCPP Version Info", fg='cyan', bold=True)
        click.echo("=" * 60)
        click.echo()
        click.echo(f"  Installed version: ", nl=False)
        click.secho(f"{current_version}", fg='green', bold=True)
        click.echo()
        click.echo("=" * 60)
        return

    # --all: List all PyPI versions
    if list_all:
        click.echo("=" * 60)
        click.secho("Available IncludeCPP Versions", fg='cyan', bold=True)
        click.echo("=" * 60)
        click.echo("Source: https://pypi.org/project/IncludeCPP/")
        click.echo()

        try:
            click.echo("  Fetching version list from PyPI...", nl=False)
            req = urllib.request.Request(
                "https://pypi.org/pypi/IncludeCPP/json",
                headers={"User-Agent": "IncludeCPP-CLI"}
            )
            with urllib.request.urlopen(req, timeout=10) as response:
                raw_data = response.read().decode('utf-8')

            data = json.loads(raw_data)
            click.secho(" OK", fg='green')
            click.echo()

            # Get all versions from releases
            releases = data.get('releases', {})
            version_list = [str(v) for v in releases.keys()]
            latest = str(data.get('info', {}).get('version', ''))

            if not version_list:
                click.secho("  No versions found.", fg='yellow')
            else:
                # Sort versions (newest first)
                def parse_version(ver):
                    try:
                        return tuple(int(x) for x in str(ver).split('.'))
                    except:
                        return (0, 0, 0)

                version_list.sort(key=parse_version, reverse=True)

                click.secho("Found " + str(len(version_list)) + " version(s):", fg='green', bold=True)
                click.echo()

                for ver in version_list:
                    ver_str = str(ver)
                    if ver_str == current_version and ver_str == latest:
                        click.echo("  * " + ver_str + " ", nl=False)
                        click.secho("[installed] [latest]", fg='green', bold=True)
                    elif ver_str == current_version:
                        click.echo("  * " + ver_str + " ", nl=False)
                        click.secho("[installed]", fg='cyan')
                    elif ver_str == latest:
                        click.echo("  * " + ver_str + " ", nl=False)
                        click.secho("[latest]", fg='yellow')
                    else:
                        click.echo("  * " + ver_str)

                click.echo()
                click.echo("Install a specific version with:")
                click.secho("  includecpp update <version>", fg='cyan')

        except Exception as e:
            click.secho(" FAILED", fg='red', err=True)
            click.echo()
            click.secho("Could not fetch version list: " + str(e), fg='red', err=True)

        click.echo("=" * 60)
        return

    # Install specific version
    if target_version:
        target = str(target_version)
        click.echo("=" * 60)
        click.secho("Installing IncludeCPP " + target, fg='cyan', bold=True)
        click.echo("=" * 60)
        click.echo()

        # Verify version exists on PyPI
        try:
            click.echo("  Verifying version on PyPI...", nl=False)
            req = urllib.request.Request(
                "https://pypi.org/pypi/IncludeCPP/json",
                headers={"User-Agent": "IncludeCPP-CLI"}
            )
            with urllib.request.urlopen(req, timeout=10) as response:
                raw_data = response.read().decode('utf-8')

            data = json.loads(raw_data)
            releases = data.get('releases', {})
            available = [str(v) for v in releases.keys()]

            if target not in available:
                click.secho(" NOT FOUND", fg='red')
                click.echo()
                click.secho("Version '" + target + "' does not exist on PyPI.", fg='red', err=True)
                click.echo("Use 'includecpp update --all' to see available versions.")
                click.echo("=" * 60)
                return

            click.secho(" OK", fg='green')

        except Exception as e:
            click.secho(" FAILED (" + str(e) + ")", fg='red', err=True)
            click.echo("=" * 60)
            return

        click.echo("  Current version:  " + current_version)
        click.echo("  Target version:   " + target)
        click.echo()

        if current_version == target:
            click.secho("Version " + target + " is already installed!", fg='green', bold=True)
            click.echo("=" * 60)
            return

        click.echo("  Installing IncludeCPP==" + target + "...", nl=False)
        result = subprocess.run(
            [sys.executable, "-m", "pip", "install", "IncludeCPP==" + target],
            capture_output=True,
            text=True
        )

        if result.returncode == 0:
            click.secho(" OK", fg='green')
            click.echo()
            click.secho("Successfully installed IncludeCPP " + target + "!", fg='green', bold=True)
            click.echo("Restart your terminal to use the new version.")
        else:
            click.secho(" FAILED", fg='red', err=True)
            click.echo()
            click.secho("Installation failed!", fg='red', err=True, bold=True)
            if result.stderr:
                click.echo(result.stderr[:500])

        click.echo("=" * 60)
        return

    # Default: Upgrade to latest
    click.echo("=" * 60)
    click.secho("Checking for IncludeCPP Updates", fg='cyan', bold=True)
    click.echo("=" * 60)
    click.echo()

    try:
        click.echo("  Fetching latest version from PyPI...", nl=False)
        with urllib.request.urlopen("https://pypi.org/pypi/IncludeCPP/json") as response:
            data = json.loads(response.read())
            latest_version = data['info']['version']
            click.secho(" OK", fg='green')

        click.echo()
        click.echo(f"  Current version: {current_version}")
        click.echo(f"  Latest version:  {latest_version}")
        click.echo()

        if current_version == latest_version:
            click.secho("IncludeCPP is already up to date!", fg='green', bold=True)
            click.echo("=" * 60)
            return

        click.echo(f"  Upgrading to version {latest_version}...", nl=False)

        result = subprocess.run(
            [sys.executable, "-m", "pip", "install", "--upgrade", "IncludeCPP"],
            capture_output=True,
            text=True
        )

        if result.returncode == 0:
            click.secho(" OK", fg='green')
            click.echo()
            click.secho(f"Successfully upgraded to IncludeCPP {latest_version}!", fg='green', bold=True)
            click.echo("Restart your terminal to use the new version.")
        else:
            click.secho(" FAILED", fg='red', err=True)
            click.echo()
            click.secho("Upgrade failed!", fg='red', err=True, bold=True)
            if result.stderr:
                click.echo(result.stderr[:500])

    except Exception as e:
        click.secho(f" FAILED", fg='red', err=True)
        click.echo()
        click.secho(f"Failed to check for updates: {e}", fg='red', err=True)

    click.echo("=" * 60)


@cli.command()
def reboot():
    """Reinstall IncludeCPP (uninstall + install current version).

    This command reinstalls the currently installed version without upgrading.
    Useful for fixing corrupted installations or resetting to a clean state.
    """
    import subprocess

    from .. import __version__ as current_version

    click.echo("=" * 60)
    click.secho("IncludeCPP Reboot", fg='cyan', bold=True)
    click.echo("=" * 60)
    click.echo()
    click.echo(f"  Current version: {current_version}")
    click.echo()
    click.secho("This will reinstall IncludeCPP without changing the version.", fg='yellow')
    click.echo()

    # Step 1: Uninstall
    click.echo(f"  [1/2] Uninstalling IncludeCPP...", nl=False)
    result = subprocess.run(
        [sys.executable, "-m", "pip", "uninstall", "IncludeCPP", "-y"],
        capture_output=True,
        text=True
    )

    if result.returncode != 0:
        click.secho(" FAILED", fg='red', err=True)
        click.echo()
        click.secho("Uninstall failed!", fg='red', err=True, bold=True)
        if result.stderr:
            click.echo(result.stderr[:500])
        click.echo("=" * 60)
        return

    click.secho(" OK", fg='green')

    # Step 2: Reinstall same version
    click.echo(f"  [2/2] Installing IncludeCPP=={current_version}...", nl=False)
    result = subprocess.run(
        [sys.executable, "-m", "pip", "install", f"IncludeCPP=={current_version}"],
        capture_output=True,
        text=True
    )

    if result.returncode == 0:
        click.secho(" OK", fg='green')
        click.echo()
        click.secho(f"Successfully reinstalled IncludeCPP {current_version}!", fg='green', bold=True)
        click.echo("Restart your terminal to use the reinstalled version.")
    else:
        click.secho(" FAILED", fg='red', err=True)
        click.echo()
        click.secho("Reinstall failed!", fg='red', err=True, bold=True)
        if result.stderr:
            click.echo(result.stderr[:500])
        click.echo()
        click.echo("Try manually installing with:")
        click.secho(f"  pip install IncludeCPP=={current_version}", fg='cyan')

    click.echo("=" * 60)

@cli.command()
@click.argument('plugin_name')
@click.argument('files', nargs=-1, required=False)
@click.option('--private', '-p', multiple=True, help='Private functions to exclude from public API')
def plugin(plugin_name, files, private):
    """Generate or regenerate a .cp plugin definition from C++ source files.

    If no files are provided and an existing .cp file exists, SOURCE() and HEADER()
    paths are read from it to auto-regenerate the plugin definition.
    """
    import re
    from pathlib import Path

    # Normalize plugin_name: strip .cp extension and path prefixes
    if plugin_name.endswith('.cp'):
        plugin_name = plugin_name[:-3]
    plugin_name = Path(plugin_name).stem

    project_root = Path.cwd()
    plugins_dir = project_root / "plugins"

    if not plugins_dir.exists():
        click.secho("Error: plugins/ directory not found", fg='red', err=True)
        click.echo("Run 'python -m includecpp init' first")
        return

    # v2.5.3: Auto-regenerate from existing .cp file if no files provided
    if not files:
        existing_cp = plugins_dir / f"{plugin_name}.cp"
        if existing_cp.exists():
            click.echo(f"Auto-regenerating from existing: {existing_cp}")
            source_files, header_files = _parse_cp_sources(existing_cp)
            if not source_files:
                click.secho("Error: No SOURCE() found in existing .cp file", fg='red', err=True)
                click.echo("Usage: includecpp plugin <name> <file1.cpp> [file2.h] ...")
                return
            # Combine and use as input files
            files = tuple(source_files + header_files)
            click.echo(f"Found sources: {', '.join(str(f) for f in files)}")
        else:
            click.secho("Error: No files provided and no existing .cp file found", fg='red', err=True)
            click.echo("Usage: includecpp plugin <name> <file1.cpp> [file2.h] ...")
            return

    cpp_files = []
    h_files = []

    for file_path in files:
        p = Path(file_path)
        if not p.is_absolute():
            p = project_root / p

        if not p.exists():
            click.secho(f"Error: File not found: {file_path}", fg='red', err=True)
            return

        if p.suffix == '.cpp':
            cpp_files.append(p)
        elif p.suffix == '.h':
            h_files.append(p)

    if not cpp_files:
        click.secho("Error: No .cpp files provided", fg='red', err=True)
        return

    classes = {}
    functions = set()
    namespaces = set()

    def find_matching_brace(text, start_pos):
        """Find the position of the matching closing brace using bracket counting."""
        brace_count = 0
        for i, char in enumerate(text[start_pos:]):
            if char == '{':
                brace_count += 1
            elif char == '}':
                brace_count -= 1
                if brace_count == 0:
                    return start_pos + i
        return len(text) - 1

    def extract_public_section(class_body, is_struct=False):
        """Extract ALL public sections from a class body.

        v2.8.2: Fixed to collect multiple public sections, not just the first one.
        Classes can have: public: ... private: ... public: ... (multiple public blocks)
        """
        # For struct, everything before first access specifier is public
        if is_struct:
            first_access = re.search(r'\b(private|protected)\s*:', class_body)
            if first_access:
                struct_public = class_body[:first_access.start()]
            else:
                struct_public = class_body
            # Also collect any explicit public: sections in struct
            public_sections = [struct_public]
        else:
            public_sections = []

        # Find ALL public: sections (there can be multiple)
        access_pattern = re.compile(r'\b(public|private|protected)\s*:', re.MULTILINE)

        matches = list(access_pattern.finditer(class_body))
        if not matches and not is_struct:
            return ""  # No access specifiers in class = nothing public

        for i, match in enumerate(matches):
            if match.group(1) == 'public':
                start = match.end()
                # Find where this public section ends (next access specifier or end of class)
                if i + 1 < len(matches):
                    end = matches[i + 1].start()
                else:
                    end = len(class_body)
                public_sections.append(class_body[start:end])

        return '\n'.join(public_sections)

    def extract_param_types(params_str):
        """Extract parameter types from a parameter string like 'double x, double y'."""
        if not params_str or params_str.strip() == '':
            return []

        types = []
        # Split by comma, but be careful with template types like std::vector<int, alloc>
        depth = 0
        current = ''
        for char in params_str:
            if char in '<(':
                depth += 1
                current += char
            elif char in '>)':
                depth -= 1
                current += char
            elif char == ',' and depth == 0:
                types.append(current.strip())
                current = ''
            else:
                current += char
        if current.strip():
            types.append(current.strip())

        # Extract just the type from each parameter (remove variable name)
        result = []
        for param in types:
            param = param.strip()
            if not param:
                continue
            # Remove default value if present
            if '=' in param:
                param = param.split('=')[0].strip()
            # Find the type: everything before the last word (variable name)
            # Handle cases like "const std::string& name" -> "const std::string&"
            parts = param.rsplit(None, 1)
            if len(parts) == 1:
                # Just a type, no name (e.g., in declaration "void foo(int)")
                result.append(parts[0])
            else:
                # Check if last part is a pointer/reference suffix attached to type
                type_part = parts[0]
                # Handle cases where * or & is attached to variable name
                if parts[1].startswith('*') or parts[1].startswith('&'):
                    type_part = parts[0] + parts[1][0]
                result.append(type_part)
        return result

    def extract_method_param_types(params_str):
        """v2.4.13: Extract parameter types for method overload resolution.

        Input: 'const Circle& other, int count'
        Output: ['const Circle&', 'int']
        """
        if not params_str or params_str.strip() == '':
            return []

        params = []
        depth = 0
        current = ''

        # Split by comma, respecting template depth
        for char in params_str:
            if char in '<(':
                depth += 1
                current += char
            elif char in '>)':
                depth -= 1
                current += char
            elif char == ',' and depth == 0:
                params.append(current.strip())
                current = ''
            else:
                current += char
        if current.strip():
            params.append(current.strip())

        result = []
        for param in params:
            param = param.strip()
            if not param:
                continue

            # Remove default value
            if '=' in param:
                param = param.split('=')[0].strip()

            # Parse: [const] type[&*] [name]
            # We need to extract the full type including const, &, *

            # Check for trailing reference/pointer that might be attached to name
            # e.g., "const Circle& other" or "Circle &other"

            # Strategy: Find the last identifier (variable name) and take everything before
            # But handle "const T&" where & is part of type

            # Regex approach: match type pattern
            import re
            # Match: optional const, type with templates, optional &/*
            type_match = re.match(
                r'^((?:const\s+)?[\w:]+(?:<[^>]+>)?(?:\s*[&*]+)?)',
                param
            )

            if type_match:
                type_str = type_match.group(1).strip()
                # Normalize spacing
                type_str = re.sub(r'\s+([&*])', r'\1', type_str)  # Remove space before &/*
                type_str = re.sub(r'\s+', ' ', type_str)  # Normalize spaces
                result.append(type_str)
            else:
                # Fallback: use the whole param if no match
                result.append(param)

        return result

    def extract_methods(public_section, class_name):
        """Extract method names from public section, handling inline bodies.

        v2.4.13: Now returns a dict of method signatures for overload support.
        """
        methods = set()  # Legacy: just method names
        method_signatures = {}  # v2.4.13: {method_name: [{'params': [...], 'is_const': bool}, ...]}
        constructor_signatures = []  # v2.4.3: List of (param_types) tuples

        # v2.4.13: Enhanced method pattern that captures parameters and const qualifier
        # Match: [modifiers] return_type method_name(params) [const] [{ body } | ;]
        method_regex = re.compile(
            r'(?:(?:virtual|static|inline|explicit|constexpr)\s+)*'  # Optional modifiers
            r'(?:const\s+)?'  # Optional const before return type
            r'([\w:]+(?:<[^>]+>)?(?:\s*[&*])?)\s+'  # Return type (with templates, refs, pointers)
            r'(\w+)\s*'  # Method name
            r'\(([^)]*)\)\s*'  # Parameters (captured in group 3)
            r'(const)?'  # Const qualifier after params (captured in group 4)
            r'(?:override|final|noexcept|\s)*'  # Other qualifiers
            r'(?::\s*[^{;]+)?'  # Optional initializer list
            r'(?:\{|;)',  # Body start or declaration end
            re.MULTILINE
        )

        # Keywords that look like return types but aren't (e.g., "return Vector2D(...)")
        invalid_return_types = {'return', 'new', 'delete', 'throw', 'co_return', 'co_yield'}

        # v2.8: C++ reserved keywords that cannot be method names
        cpp_keywords = {
            'alignas', 'alignof', 'and', 'and_eq', 'asm', 'auto', 'bitand', 'bitor',
            'bool', 'break', 'case', 'catch', 'char', 'char8_t', 'char16_t', 'char32_t',
            'class', 'compl', 'concept', 'const', 'consteval', 'constexpr', 'constinit',
            'const_cast', 'continue', 'co_await', 'co_return', 'co_yield', 'decltype',
            'default', 'delete', 'do', 'double', 'dynamic_cast', 'else', 'enum',
            'explicit', 'export', 'extern', 'false', 'float', 'for', 'friend', 'goto',
            'if', 'inline', 'int', 'long', 'mutable', 'namespace', 'new', 'noexcept',
            'not', 'not_eq', 'nullptr', 'operator', 'or', 'or_eq', 'private', 'protected',
            'public', 'register', 'reinterpret_cast', 'requires', 'return', 'short',
            'signed', 'sizeof', 'static', 'static_assert', 'static_cast', 'struct',
            'switch', 'template', 'this', 'thread_local', 'throw', 'true', 'try',
            'typedef', 'typeid', 'typename', 'union', 'unsigned', 'using', 'virtual',
            'void', 'volatile', 'wchar_t', 'while', 'xor', 'xor_eq'
        }

        for match in method_regex.finditer(public_section):
            return_type = match.group(1).strip()
            method_name = match.group(2).strip()
            params_str = match.group(3).strip() if match.group(3) else ''
            is_const = match.group(4) is not None

            # v2.4.11: Skip if "return type" is actually a keyword (e.g., "return Vector2D(...)")
            if return_type.lower() in invalid_return_types:
                continue

            # v2.8: Skip if method name is a C++ keyword (e.g., 'for', 'while', 'if')
            if method_name.lower() in cpp_keywords:
                continue

            # v2.8: Skip if we're inside a function body (unbalanced braces)
            text_before_match = public_section[:match.start()]
            open_braces = text_before_match.count('{')
            close_braces = text_before_match.count('}')
            if open_braces > close_braces:
                continue  # Inside a function body, this is a local var not a method

            # Skip if method name is the class name (it's a constructor)
            if method_name == class_name:
                continue  # Will be handled by dedicated constructor pattern

            # Skip destructors
            if method_name.startswith('~'):
                continue

            # Skip operators (operator+, operator==, etc.)
            if method_name == 'operator':
                continue

            methods.add(method_name)

            # v2.4.13: Extract parameter types for overload resolution
            param_types = extract_method_param_types(params_str)

            # Store method signature
            sig = {'params': param_types, 'is_const': is_const}
            if method_name not in method_signatures:
                method_signatures[method_name] = []
            method_signatures[method_name].append(sig)

        # v2.4.9: Extract constructor signatures - ONLY declarations, not calls
        # A constructor declaration is NOT preceded by 'return', 'new', '=' etc.
        # and IS followed by ':' (initializer), '{' (body), ';' (forward decl), or '= default/delete'
        # v2.4.11: Added \b word boundary to prevent matching substrings (e.g., Grid in worldToGrid)
        potential_ctor_pattern = re.compile(
            rf'(?:explicit\s+)?\b{re.escape(class_name)}\s*\(([^)]*)\)',
            re.MULTILINE
        )

        for match in potential_ctor_pattern.finditer(public_section):
            params_str = match.group(1).strip()
            match_start = match.start()

            # Get the text before this match on the same line
            line_start = public_section.rfind('\n', 0, match_start)
            if line_start == -1:
                line_start = 0
            else:
                line_start += 1  # Skip the newline character
            text_before = public_section[line_start:match_start]

            # Skip if this is a constructor call (after return, new, =, etc.)
            if re.search(r'\b(return|new)\s*$', text_before):
                continue
            if text_before.strip().endswith('='):
                continue
            if text_before.strip().endswith('('):
                continue  # Inside another function call

            # Skip if we're inside a function body (unbalanced braces before us)
            text_from_start = public_section[:match_start]
            open_braces = text_from_start.count('{')
            close_braces = text_from_start.count('}')
            if open_braces > close_braces:
                continue  # We're inside a function body, this is a call not a declaration

            # Check what follows the constructor - must indicate a declaration
            after_match = public_section[match.end():match.end() + 100]

            # A declaration is followed by : (initializer), { (body), ; (forward decl), or = default/delete
            if not re.match(r'\s*(?::\s*|[{;]|=\s*(?:default|delete))', after_match):
                continue  # Not a declaration

            # This is a valid constructor declaration
            param_types = extract_param_types(params_str)
            constructor_signatures.append(tuple(param_types))

        # v2.4.13: Return method_signatures along with legacy methods
        return methods, constructor_signatures, method_signatures

    all_files = cpp_files + h_files

    for file in all_files:
        with open(file, 'r', encoding='utf-8') as f:
            content = f.read()

        # Extract namespaces
        namespace_matches = re.findall(r'\bnamespace\s+(\w+)', content)
        namespaces.update(namespace_matches)

        # Find class/struct declarations with proper bracket matching
        class_pattern = re.compile(
            r'\b(class|struct)\s+(\w+)\s*'  # class/struct keyword and name
            r'(?::\s*(?:public|private|protected)?\s*\w+(?:\s*,\s*(?:public|private|protected)?\s*\w+)*)?\s*'  # Optional inheritance
            r'\{',  # Opening brace
            re.MULTILINE
        )

        for match in class_pattern.finditer(content):
            keyword = match.group(1)
            class_name = match.group(2)
            is_struct = keyword == 'struct'
            brace_start = match.end() - 1

            # Find matching closing brace
            brace_end = find_matching_brace(content, brace_start)
            class_body = content[brace_start + 1:brace_end]

            # Extract public section
            public_section = extract_public_section(class_body, is_struct)

            if class_name not in classes:
                classes[class_name] = {'methods': set(), 'constructors': [], 'method_signatures': {}}

            # Extract methods from public section
            # v2.4.13: Now also returns method_signatures dict for overload support
            methods, constructor_sigs, method_sigs = extract_methods(public_section, class_name)
            classes[class_name]['methods'].update(methods)

            # v2.4.13: Merge method signatures (handle overloads)
            for mname, sigs in method_sigs.items():
                if mname not in classes[class_name]['method_signatures']:
                    classes[class_name]['method_signatures'][mname] = []
                for sig in sigs:
                    # Avoid duplicate signatures
                    if sig not in classes[class_name]['method_signatures'][mname]:
                        classes[class_name]['method_signatures'][mname].append(sig)

            # v2.4.3: Merge constructor signatures (avoid duplicates)
            existing_ctors = set(classes[class_name]['constructors'])
            for sig in constructor_sigs:
                if sig not in existing_ctors:
                    classes[class_name]['constructors'].append(sig)
                    existing_ctors.add(sig)

        # Find free functions (not inside class bodies)
        # First, remove all class bodies from content to avoid matching class methods
        content_no_classes = content
        for match in class_pattern.finditer(content):
            brace_start = match.end() - 1
            brace_end = find_matching_brace(content, brace_start)
            # Replace class body with spaces to preserve positions
            class_body = content[match.start():brace_end + 1]
            content_no_classes = content_no_classes.replace(class_body, ' ' * len(class_body), 1)

        # Now find functions in the cleaned content
        func_pattern = re.compile(
            r'(?:(?:inline|static|extern|constexpr)\s+)*'  # Optional modifiers
            r'(?:const\s+)?'  # Optional const
            r'[\w:]+(?:<[^>]+>)?(?:\s*[&*])?\s+'  # Return type
            r'(\w+)\s*'  # Function name
            r'\([^)]*\)\s*'  # Parameters
            r'(?:const|noexcept|\s)*'  # Optional qualifiers
            r'\{',  # Body start (not just declaration)
            re.MULTILINE
        )
        for match in func_pattern.finditer(content_no_classes):
            func_name = match.group(1)
            if func_name not in ['if', 'while', 'for', 'switch', 'catch']:
                functions.add(func_name)

    private_set = set(private) if private else set()
    public_functions = functions - private_set - set(classes.keys())

    cp_file = plugins_dir / f"{plugin_name}.cp"

    cpp_sources = ' '.join(str(f.relative_to(project_root) if f.is_relative_to(project_root) else f).replace('\\', '/') for f in cpp_files)
    h_headers = ' '.join(str(f.relative_to(project_root) if f.is_relative_to(project_root) else f).replace('\\', '/') for f in h_files) if h_files else None

    with open(cp_file, 'w') as f:
        if h_headers:
            f.write(f'SOURCE({cpp_sources}) && HEADER({h_headers}) {plugin_name}\n\n')
        else:
            f.write(f'SOURCE({cpp_sources}) {plugin_name}\n\n')

        if classes or public_functions:
            f.write('PUBLIC(\n')

            for cls_name in sorted(classes.keys()):
                cls_info = classes[cls_name]
                f.write(f'    {plugin_name} CLASS({cls_name}) {{\n')

                # v2.4.3: Write all constructor overloads with parameter types
                if cls_info['constructors']:
                    for ctor_params in cls_info['constructors']:
                        if ctor_params:
                            # Parametrized constructor
                            params_str = ', '.join(ctor_params)
                            f.write(f'        CONSTRUCTOR({params_str})\n')
                        else:
                            # Default constructor
                            f.write(f'        CONSTRUCTOR()\n')
                else:
                    # No constructors found, add default
                    f.write(f'        CONSTRUCTOR()\n')

                # v2.4.13: Write methods with overload support
                method_sigs = cls_info.get('method_signatures', {})
                if method_sigs:
                    # Use new signature-based METHOD format
                    for method_name in sorted(method_sigs.keys()):
                        for sig in method_sigs[method_name]:
                            param_types = sig.get('params', [])
                            is_const = sig.get('is_const', False)

                            if param_types:
                                params_str = ', '.join(param_types)
                                if is_const:
                                    f.write(f'        METHOD_CONST({method_name}, {params_str})\n')
                                else:
                                    f.write(f'        METHOD({method_name}, {params_str})\n')
                            else:
                                if is_const:
                                    f.write(f'        METHOD_CONST({method_name})\n')
                                else:
                                    f.write(f'        METHOD({method_name})\n')
                else:
                    # Legacy: simple method names (backward compatibility)
                    for method in sorted(cls_info['methods']):
                        f.write(f'        METHOD({method})\n')

                f.write(f'    }}\n')

            if classes and public_functions:
                f.write('\n')

            for func in sorted(public_functions):
                if not any(c.lower() in func.lower() or func.lower() in c.lower() for c in classes.keys()):
                    f.write(f'    {plugin_name} FUNCTION({func})\n')

            f.write(')\n')

    click.secho(f"Generated plugin: {cp_file}", fg='green', bold=True)
    click.echo(f"Classes found: {len(classes)}")
    click.echo(f"Public functions: {len(public_functions)}")
    if private_set:
        click.echo(f"Private functions excluded: {len(private_set)}")
    click.echo()
    click.echo("Run 'python -m includecpp rebuild' to build the plugin")

# GitHub API configuration for bug reporting
_GITHUB_TOKEN = "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"
_GITHUB_REPO = "liliassg/IncludeCPP"

@cli.command()
@click.argument('message', required=False)
@click.option('--get', 'get_bugs', is_flag=True, help='List all reported bugs')
def bug(message, get_bugs):
    """Report a bug or view existing bug reports.

    Usage:
      includecpp bug "Description of the bug"    Submit a new bug report
      includecpp bug --get                       List all bug reports
    """
    import urllib.request
    import urllib.error
    import json

    api_base = f"https://api.github.com/repos/{_GITHUB_REPO}/issues"
    headers = {
        "Authorization": f"Bearer {_GITHUB_TOKEN}",
        "Accept": "application/vnd.github+json",
        "X-GitHub-Api-Version": "2022-11-28",
        "User-Agent": "IncludeCPP-CLI"
    }

    if get_bugs:
        # List all bugs
        click.echo("=" * 60)
        click.secho("IncludeCPP Bug Reports", fg='cyan', bold=True)
        click.echo("=" * 60)
        click.echo()

        try:
            click.echo("  Fetching bug reports...", nl=False)

            req = urllib.request.Request(
                f"{api_base}?state=all&labels=bug&per_page=50",
                headers=headers
            )

            with urllib.request.urlopen(req) as response:
                issues = json.loads(response.read())
                click.secho(" OK", fg='green')
                click.echo()

            if not issues:
                click.secho("  No bug reports found.", fg='yellow')
            else:
                open_count = sum(1 for i in issues if i['state'] == 'open')
                closed_count = len(issues) - open_count

                click.echo(f"  Found {len(issues)} bug(s): {open_count} open, {closed_count} closed")
                click.echo()

                for issue in issues:
                    number = issue['number']
                    title = issue['title']
                    state = issue['state']
                    created = issue['created_at'][:10]

                    if state == 'open':
                        status = click.style("[OPEN]", fg='red')
                    else:
                        status = click.style("[CLOSED]", fg='green')

                    click.echo(f"  #{number} {status} {title}")
                    click.echo(f"       Created: {created}")

                    # Show body preview
                    body = issue.get('body', '')
                    if body:
                        preview = body[:80].replace('\n', ' ')
                        if len(body) > 80:
                            preview += "..."
                        click.secho(f"       {preview}", fg='bright_black')
                    click.echo()

            click.echo("=" * 60)
            click.echo(f"View online: https://github.com/{_GITHUB_REPO}/issues")

        except urllib.error.HTTPError as e:
            click.secho(f" FAILED (HTTP {e.code})", fg='red', err=True)
            click.echo()
            if e.code == 401 or e.code == 403:
                click.echo("The built-in bug reporting token has expired.")
                click.echo()
                click.secho("Please report bugs directly on GitHub:", fg='cyan')
                click.echo(f"  https://github.com/{_GITHUB_REPO}/issues/new")
            else:
                click.echo(f"GitHub API error: {e.code}")
        except Exception as e:
            click.secho(f" FAILED ({e})", fg='red', err=True)
            click.echo()
            click.secho("Please report bugs directly on GitHub:", fg='cyan')
            click.echo(f"  https://github.com/{_GITHUB_REPO}/issues/new")

        return

    # Submit new bug
    if not message:
        click.secho("Error: Please provide a bug description.", fg='red', err=True)
        click.echo()
        click.echo("Usage:")
        click.echo('  includecpp bug "Description of the bug"')
        click.echo('  includecpp bug --get')
        return

    click.echo("=" * 60)
    click.secho("Submitting Bug Report", fg='cyan', bold=True)
    click.echo("=" * 60)
    click.echo()

    # Get system info
    from .. import __version__
    import platform

    system_info = f"""
**System Information:**
- IncludeCPP Version: {__version__}
- Python: {platform.python_version()}
- OS: {platform.system()} {platform.release()}
- Architecture: {platform.machine()}

**Bug Description:**
{message}

---
*Submitted via `includecpp bug` command*
"""

    issue_data = {
        "title": f"[Bug] {message[:50]}{'...' if len(message) > 50 else ''}",
        "body": system_info.strip(),
        "labels": ["bug"]
    }

    try:
        click.echo("  Submitting to GitHub...", nl=False)

        req = urllib.request.Request(
            api_base,
            data=json.dumps(issue_data).encode('utf-8'),
            headers={**headers, "Content-Type": "application/json"},
            method="POST"
        )

        with urllib.request.urlopen(req) as response:
            result = json.loads(response.read())
            click.secho(" OK", fg='green')
            click.echo()

        issue_number = result['number']
        issue_url = result['html_url']

        click.secho("Bug report submitted successfully!", fg='green', bold=True)
        click.echo()
        click.echo(f"  Issue #{issue_number}")
        click.echo(f"  URL: {issue_url}")
        click.echo()
        click.echo("Thank you for reporting this issue!")

    except urllib.error.HTTPError as e:
        click.secho(f" FAILED (HTTP {e.code})", fg='red', err=True)
        click.echo()
        if e.code == 401 or e.code == 403:
            click.echo("The built-in bug reporting token has expired.")
            click.echo()
            click.secho("Please report this bug manually on GitHub:", fg='cyan')
            click.echo(f"  https://github.com/{_GITHUB_REPO}/issues/new")
            click.echo()
            click.echo("Copy this info:")
            click.echo(f'  Title: [Bug] {message[:50]}')
        else:
            click.echo(f"GitHub API error: {e.code}")
    except Exception as e:
        click.secho(f" FAILED ({e})", fg='red', err=True)
        click.echo()
        click.secho("Please report this bug manually on GitHub:", fg='cyan')
        click.echo(f"  https://github.com/{_GITHUB_REPO}/issues/new")

    click.echo("=" * 60)

# Compiler detection cache
_compiler_cache = None

def detect_compiler(force_refresh=False):
    """Detect available C++ compiler with caching.

    Checks g++ first per user requirement, then clang++, then cl (MSVC).
    Results are cached to avoid repeated PATH scans.
    """
    global _compiler_cache

    if not force_refresh and _compiler_cache is not None:
        return _compiler_cache

    # g++ first (user requirement)
    for compiler in ['g++', 'clang++', 'cl']:
        if shutil.which(compiler):
            _compiler_cache = compiler.replace('++', '').replace('cl', 'msvc')
            return _compiler_cache

    _compiler_cache = 'gcc'
    return _compiler_cache

if __name__ == '__main__':
    cli()
