import sys
import os
import subprocess
import hashlib
import shutil
import json
import platform
from pathlib import Path
from typing import List, Optional, Dict, Any

from .exceptions import CppBuildError, CppValidationError

class BuildManager:
    def __init__(self, project_root: Path, build_dir: Path, config):
        """Initialize BuildManager for PyPI-installed package.

        Args:
            project_root: User's project directory (with cpp.proj)
            build_dir: AppData build directory
            config: CppProjectConfig instance
        """
        self.project_root = project_root
        self.build_dir = build_dir
        self.config = config

        self.plugins_dir = config.plugins_dir
        self.include_dir = config.include_dir

        self.bin_dir = build_dir / "bin" / ".appc"
        self.bindings_dir = build_dir / "bindings"
        self.cmake_build_dir = build_dir / "build"

        # Inbuilds directory (from package)
        package_root = Path(__file__).parent.parent
        self.inbuilds_source_dir = package_root / "inbuilds"
        self.inbuilds_build_dir = build_dir / "inbuilds"

        self.gen_exe = self.bin_dir / self._get_exe_name("plugin_gen")
        self.registry_file = build_dir / ".module_registry.json"

        self.bin_dir.mkdir(parents=True, exist_ok=True)
        self.bindings_dir.mkdir(parents=True, exist_ok=True)
        self.cmake_build_dir.mkdir(parents=True, exist_ok=True)
        self.inbuilds_build_dir.mkdir(parents=True, exist_ok=True)

    def _get_exe_name(self, base_name: str) -> str:
        """Get platform-specific executable name."""
        if platform.system() == "Windows":
            return f"{base_name}.exe"
        return base_name

    def _compute_hash(self, filepath: Path) -> str:
        """Compute SHA256 hash of file."""
        if not filepath.exists():
            return "0"
        try:
            with open(filepath, 'rb') as f:
                return hashlib.sha256(f.read()).hexdigest()[:16]
        except Exception:
            return "0"

    def _get_generator_source(self) -> Path:
        """Get path to parser.cpp in installed package."""
        package_dir = Path(__file__).parent.parent
        parser_cpp = package_dir / "generator" / "parser.cpp"

        if not parser_cpp.exists():
            raise CppBuildError(
                f"Generator source not found: {parser_cpp}\n"
                "This is a package installation error."
            )

        return parser_cpp

    def _build_generator(self, verbose: bool = False):
        """Compile plugin_gen.exe from parser.cpp."""
        if self.gen_exe.exists():
            if verbose:
                print(f"Generator exists: {self.gen_exe}")
            return

        parser_cpp = self._get_generator_source()
        package_generator_dir = parser_cpp.parent

        if verbose:
            print(f"Compiling generator from: {parser_cpp}")

        compiler = self._detect_cpp_compiler()

        if compiler == "g++":
            cmd = [
                "g++",
                "-std=c++17",
                "-O2",
                f"-I{package_generator_dir}",
                str(parser_cpp),
                "-o",
                str(self.gen_exe)
            ]
        elif compiler == "clang++":
            cmd = [
                "clang++",
                "-std=c++17",
                "-O2",
                f"-I{package_generator_dir}",
                str(parser_cpp),
                "-o",
                str(self.gen_exe)
            ]
        elif compiler == "cl":
            cmd = [
                "cl",
                "/std:c++17",
                "/O2",
                f"/I{package_generator_dir}",
                str(parser_cpp),
                f"/Fe:{self.gen_exe}"
            ]
        else:
            raise CppBuildError("No C++ compiler found (g++, clang++, or cl)")

        self._run_compiler_command(cmd, verbose=verbose)

        if not self.gen_exe.exists():
            raise CppBuildError(f"Generator executable not created: {self.gen_exe}")

        if verbose:
            print(f"Generator compiled: {self.gen_exe}")

    def _get_msys2_env(self) -> dict:
        """Get MSYS2 MINGW64 environment variables for g++ on Windows."""
        env = os.environ.copy()
        if platform.system() == "Windows":
            # Set MSYS2 MINGW64 environment
            env["MINGW_PREFIX"] = "/mingw64"
            env["MSYSTEM"] = "MINGW64"
            env["PKG_CONFIG_PATH"] = "/mingw64/lib/pkgconfig:/mingw64/share/pkgconfig"

            # Prepend MSYS2 paths
            msys_paths = [
                "C:/msys64/mingw64/bin",
                "C:/msys64/usr/local/bin",
                "C:/msys64/usr/bin",
                "C:/msys64/bin"
            ]
            existing_path = env.get("PATH", "")
            env["PATH"] = ";".join(msys_paths) + ";" + existing_path
        return env

    def _run_compiler_command(self, cmd: list, verbose: bool = False, cwd: str = None):
        """Run compiler command with appropriate environment (MSYS2 on Windows)."""
        env = self._get_msys2_env()

        try:
            if verbose:
                print(f"Running: {' '.join(cmd)}")
            result = subprocess.run(cmd, capture_output=True, text=True, check=True, env=env, cwd=cwd)
            if verbose and result.stdout:
                print(result.stdout)
            return result
        except subprocess.CalledProcessError as e:
            raise CppBuildError(
                f"Compilation failed:\nCommand: {' '.join(cmd)}\nStderr: {e.stderr}\nStdout: {e.stdout}"
            ) from e

    def _detect_cpp_compiler(self) -> str:
        """Detect available C++ compiler."""
        for compiler in ['g++', 'clang++', 'cl']:
            if shutil.which(compiler):
                return compiler
        return None

    def _copy_inbuilds(self, verbose: bool = False) -> int:
        """Copy inbuilt modules to build directory.

        Returns:
            Number of inbuilds copied
        """
        if not self.inbuilds_source_dir.exists():
            if verbose:
                print("No inbuilds directory found in package")
            return 0

        copied_count = 0

        for inbuild_dir in self.inbuilds_source_dir.iterdir():
            if not inbuild_dir.is_dir():
                continue

            # Check if this is a valid inbuild (has .cp file)
            cp_file = inbuild_dir / f"{inbuild_dir.name}.cp"
            if not cp_file.exists():
                continue

            # Copy to build directory
            dest_dir = self.inbuilds_build_dir / inbuild_dir.name
            if dest_dir.exists():
                shutil.rmtree(dest_dir)

            shutil.copytree(inbuild_dir, dest_dir)
            copied_count += 1

            if verbose:
                print(f"Copied inbuild: {inbuild_dir.name}")

        return copied_count

    def _scan_plugins(self, verbose: bool = False) -> List[Path]:
        """Scan user's plugins directory for .cp files."""
        if not self.plugins_dir.exists():
            raise CppBuildError(
                f"Plugins directory not found: {self.plugins_dir}\n"
                f"Create it with: mkdir {self.plugins_dir}"
            )

        cp_files = list(self.plugins_dir.glob("*.cp"))

        if not cp_files:
            raise CppBuildError(
                f"No .cp files found in {self.plugins_dir}\n"
                "Create plugin definitions first."
            )

        if verbose:
            print(f"Found {len(cp_files)} user plugin(s): {[f.name for f in cp_files]}")

        return cp_files

    def _scan_all_plugins(self, verbose: bool = False) -> tuple[List[Path], List[Path]]:
        """Scan both user plugins and inbuilds.

        Returns:
            Tuple of (user_plugins, inbuild_plugins)
        """
        # Scan user plugins
        user_plugins = self._scan_plugins(verbose)

        # Scan inbuilds
        inbuild_plugins = []
        if self.inbuilds_build_dir.exists():
            for inbuild_dir in self.inbuilds_build_dir.iterdir():
                if inbuild_dir.is_dir():
                    cp_file = inbuild_dir / f"{inbuild_dir.name}.cp"
                    if cp_file.exists():
                        inbuild_plugins.append(cp_file)

        if verbose and inbuild_plugins:
            print(f"Found {len(inbuild_plugins)} inbuild plugin(s): {[f.stem for f in inbuild_plugins]}")

        return user_plugins, inbuild_plugins

    def _generate_bindings(self, verbose: bool = False):
        """Run plugin_gen.exe to generate bindings.cpp for user plugins AND inbuilds."""

        # Copy inbuilds to build directory first
        inbuilds_count = self._copy_inbuilds(verbose)
        if verbose and inbuilds_count > 0:
            print(f"Copied {inbuilds_count} inbuilt module(s)")

        # Create combined plugins directory with symlinks
        combined_plugins_dir = self.build_dir / "all_plugins"
        if combined_plugins_dir.exists():
            shutil.rmtree(combined_plugins_dir)
        combined_plugins_dir.mkdir(parents=True, exist_ok=True)

        # Copy user plugins (as files, not symlinks for cross-platform compatibility)
        user_plugins = list(self.plugins_dir.glob("*.cp"))
        for cp_file in user_plugins:
            dest = combined_plugins_dir / cp_file.name
            shutil.copy2(cp_file, dest)
            if verbose:
                print(f"Added user plugin: {cp_file.name}")

        # Copy inbuild .cp files with path adjustments
        inbuild_count = 0
        if self.inbuilds_build_dir.exists():
            for inbuild_dir in self.inbuilds_build_dir.iterdir():
                if inbuild_dir.is_dir():
                    cp_file = inbuild_dir / f"{inbuild_dir.name}.cp"
                    if cp_file.exists():
                        # Read .cp file and adjust paths to be absolute
                        with open(cp_file, 'r') as f:
                            cp_content = f.read()

                        # Replace relative paths with absolute paths
                        # SOURCE(filename.cpp) -> SOURCE(/absolute/path/to/inbuilds/module/filename.cpp)
                        # HEADER(filename.h) -> HEADER(/absolute/path/to/inbuilds/module/filename.h)
                        import re

                        def replace_path(match):
                            """Replace relative path with absolute."""
                            path = match.group(1)
                            if not Path(path).is_absolute():
                                # Make absolute relative to inbuild directory
                                abs_path = str((inbuild_dir / path).resolve())
                                # Normalize path separators for cross-platform
                                abs_path = abs_path.replace('\\', '/')
                                return f'{match.group(0).split("(")[0]}({abs_path})'
                            return match.group(0)

                        cp_content = re.sub(r'SOURCE\(([^)]+)\)', replace_path, cp_content)
                        cp_content = re.sub(r'HEADER\(([^)]+)\)', replace_path, cp_content)

                        # Write adjusted .cp file
                        dest = combined_plugins_dir / cp_file.name
                        with open(dest, 'w') as f:
                            f.write(cp_content)

                        inbuild_count += 1
                        if verbose:
                            print(f"Added inbuild plugin: {cp_file.name} (paths adjusted)")

        total_plugins = len(user_plugins) + inbuild_count

        if total_plugins == 0:
            raise CppBuildError("No plugins found (neither user nor inbuilt)")

        bindings_cpp = self.bindings_dir / "bindings.cpp"
        sources_txt = self.bindings_dir / "sources.txt"

        # Run generator on combined directory
        cmd = [
            str(self.gen_exe),
            str(combined_plugins_dir),
            str(bindings_cpp),
            str(sources_txt),
            str(self.registry_file)
        ]

        try:
            if verbose:
                print(f"Running generator on {total_plugins} total plugin(s)...")
                print(f"  Combined dir: {combined_plugins_dir}")
                print(f"  Bindings output: {bindings_cpp}")

            # Important: Run from project_root so relative paths in .cp files work
            result = subprocess.run(cmd, capture_output=True, text=True, check=True, cwd=str(self.project_root))

            # ALWAYS print stdout/stderr in verbose mode, even on success
            if verbose:
                if result.stdout:
                    print("Generator stdout:")
                    print(result.stdout)
                if result.stderr:
                    print("Generator stderr:")
                    print(result.stderr)

        except subprocess.CalledProcessError as e:
            error_msg = f"Plugin generation failed (exit code {e.returncode}):\n"
            error_msg += f"Command: {' '.join(cmd)}\n"
            if e.stdout:
                error_msg += f"Stdout:\n{e.stdout}\n"
            if e.stderr:
                error_msg += f"Stderr:\n{e.stderr}\n"
            raise CppBuildError(error_msg) from e

        # Verify outputs were created
        if not bindings_cpp.exists():
            error_msg = f"bindings.cpp not generated: {bindings_cpp}\n"
            error_msg += f"Plugin generator ran successfully but did not create output file.\n"
            error_msg += f"This usually means no .cp files were found or parsed successfully.\n"
            error_msg += f"Total plugins: {total_plugins}"
            raise CppBuildError(error_msg)

        if verbose:
            print(f"Generated: {bindings_cpp}")
            print(f"Generated: {sources_txt}")
            print(f"Generated: {self.registry_file}")

    def _generate_cmake(self, verbose: bool = False):
        """Generate CMakeLists.txt in build directory."""
        sources_txt = self.bindings_dir / "sources.txt"

        if not sources_txt.exists():
            raise CppBuildError(f"sources.txt not found: {sources_txt}")

        with open(sources_txt) as f:
            sources = [line.strip() for line in f if line.strip()]

        source_paths = []
        for src in sources:
            src_path = self.project_root / src
            if not src_path.exists():
                raise CppBuildError(f"Source file not found: {src_path}")
            source_paths.append(str(src_path))

        bindings_cpp = self.bindings_dir / "bindings.cpp"

        cmake_content = f'''cmake_minimum_required(VERSION 3.15)
project(includecpp_api VERSION 1.0.0)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)

find_package(Python COMPONENTS Interpreter Development REQUIRED)
find_package(pybind11 CONFIG REQUIRED)

pybind11_add_module(api
    "{bindings_cpp}"
{chr(10).join(f'    "{src}"' for src in source_paths)}
)

target_include_directories(api PRIVATE
    "{self.project_root}"
    "{self.include_dir}"
)

if(MSVC)
    target_compile_options(api PRIVATE /W3 /O2 /EHsc /MT)
else()
    target_compile_options(api PRIVATE -Wall -O3 -pthread)
    # MinGW on Windows: static linking for MinGW runtime and pthread
    if(WIN32)
        target_link_options(api PRIVATE -static-libgcc -static-libstdc++ -Wl,-Bstatic -lpthread -Wl,-Bdynamic)
    endif()
endif()
'''

        cmake_file = self.build_dir / "CMakeLists.txt"
        cmake_file.write_text(cmake_content)

        if verbose:
            print(f"Generated CMakeLists.txt with {len(source_paths)} source(s)")

    def _configure_cmake(self, verbose: bool = False):
        """Configure CMake build."""
        generators = []

        if platform.system() == "Windows":
            generators = [
                "Ninja",
                "MinGW Makefiles",
                "Visual Studio 17 2022",
                "Visual Studio 16 2019"
            ]
        else:
            generators = ["Unix Makefiles", "Ninja"]

        # Use MSYS2 environment for proper g++/cmake operation
        env = self._get_msys2_env()

        last_error = None
        for generator in generators:
            try:
                cmd = [
                    "cmake",
                    "-B", str(self.cmake_build_dir),
                    "-S", str(self.build_dir),
                    "-G", generator
                ]

                if verbose:
                    print(f"Trying CMake generator: {generator}")

                result = subprocess.run(cmd, capture_output=True, text=True, check=True, env=env)

                if verbose:
                    print(f"CMake configured with {generator}")
                    if result.stdout:
                        print(result.stdout)

                return

            except subprocess.CalledProcessError as e:
                last_error = e
                if verbose:
                    print(f"Generator {generator} failed, trying next...")

                # Clean CMake cache before trying next generator
                cmake_cache = self.cmake_build_dir / "CMakeCache.txt"
                cmake_files = self.cmake_build_dir / "CMakeFiles"

                if cmake_cache.exists():
                    cmake_cache.unlink()
                if cmake_files.exists():
                    shutil.rmtree(cmake_files)

                continue

        raise CppBuildError(
            f"CMake configuration failed with all generators.\n"
            f"Last error: {last_error.stderr if last_error else 'Unknown'}"
        ) from last_error

    def _compile_cpp(self, verbose: bool = False):
        """Compile C++ code with CMake."""
        # Use MSYS2 environment for proper g++/cmake operation
        env = self._get_msys2_env()

        cmd = [
            "cmake",
            "--build", str(self.cmake_build_dir),
            "--config", "Release"
        ]

        try:
            if verbose:
                print(f"Running: {' '.join(cmd)}")

            result = subprocess.run(cmd, capture_output=True, text=True, check=True, env=env)

            if verbose and result.stdout:
                print(result.stdout)

        except subprocess.CalledProcessError as e:
            raise CppBuildError(
                f"C++ compilation failed:\n{e.stderr}"
            ) from e

        if verbose:
            print("C++ compilation successful")

    def _compile_direct(self, verbose: bool = False):
        """Direct compilation with g++/clang++ without CMake (fallback)."""
        compiler = self._detect_cpp_compiler()
        if not compiler:
            raise CppBuildError("No C++ compiler found (g++, clang++, or cl)")

        # Get source files
        sources_txt = self.bindings_dir / "sources.txt"
        if not sources_txt.exists():
            raise CppBuildError(f"sources.txt not found: {sources_txt}")

        with open(sources_txt) as f:
            sources = [line.strip() for line in f if line.strip()]

        source_paths = [str(self.project_root / src) for src in sources]
        bindings_cpp = str(self.bindings_dir / "bindings.cpp")

        # Get Python include and lib paths
        python_include = subprocess.check_output(
            [sys.executable, "-c", "import sysconfig; print(sysconfig.get_path('include'))"],
            text=True
        ).strip()

        pybind11_include = subprocess.check_output(
            [sys.executable, "-c", "import pybind11; print(pybind11.get_include())"],
            text=True
        ).strip()

        # Output file
        output_file = str(self.bindings_dir / f"api{self._get_pyd_suffix()}")

        # Build command
        if compiler in ['g++', 'clang++']:
            cmd = [
                compiler,
                "-O3",
                "-Wall",
                "-shared",
                "-std=c++17",
                "-fPIC",
                f"-I{python_include}",
                f"-I{pybind11_include}",
                f"-I{self.project_root}",
                f"-I{self.include_dir}",
                bindings_cpp,
                *source_paths,
                "-o", output_file
            ]

            # Platform-specific flags
            if platform.system() == "Windows":
                # MinGW on Windows: Need to link Python library for symbols
                # Find Python libs directory
                python_libs_cmd = [sys.executable, "-c",
                    "import sysconfig; import os; "
                    "libdir = sysconfig.get_config_var('LIBDIR'); "
                    "prefix = sysconfig.get_config_var('prefix'); "
                    "print(libdir if libdir and os.path.exists(libdir) else os.path.join(prefix, 'libs'))"]
                python_libs_dir = subprocess.check_output(python_libs_cmd, text=True).strip()

                # Get Python library name (e.g., python312)
                py_version = f"python{sys.version_info.major}{sys.version_info.minor}"

                cmd.extend([
                    "-static-libgcc",
                    "-static-libstdc++",
                    "-Wl,-Bstatic",
                    "-lpthread",
                    "-Wl,-Bdynamic",
                    f"-L{python_libs_dir}",
                    f"-l{py_version}"
                ])
                if verbose:
                    print(f"Linking with {py_version} from {python_libs_dir}")
            else:
                # Linux/macOS
                cmd.append("-pthread")

        elif compiler == 'cl':  # MSVC
            cmd = [
                "cl",
                "/O2",
                "/EHsc",
                "/std:c++17",
                "/LD",  # Create DLL
                f"/I{python_include}",
                f"/I{pybind11_include}",
                f"/I{self.project_root}",
                f"/I{self.include_dir}",
                bindings_cpp,
                *source_paths,
                f"/Fe:{output_file}"
            ]
        else:
            raise CppBuildError(f"Unsupported compiler: {compiler}")

        if verbose:
            print(f"Direct compilation with {compiler}")
            print(f"Command: {' '.join(cmd)}")

        self._run_compiler_command(cmd, verbose=verbose, cwd=str(self.bindings_dir))

        if verbose:
            print(f"Module compiled: {output_file}")

        # Copy MinGW DLLs if on Windows (after direct compilation)
        self._copy_mingw_dlls(verbose=verbose)

    def _copy_mingw_dlls(self, verbose: bool = False):
        """Copy required MinGW DLLs to bindings directory on Windows."""
        if platform.system() != "Windows":
            return

        msys_bin = Path("C:/msys64/mingw64/bin")
        if not msys_bin.exists():
            return

        required_dlls = ["libwinpthread-1.dll", "libgcc_s_seh-1.dll", "libstdc++-6.dll"]

        for dll_name in required_dlls:
            source = msys_bin / dll_name
            dest = self.bindings_dir / dll_name

            if source.exists() and not dest.exists():
                shutil.copy2(str(source), str(dest))
                if verbose:
                    print(f"Copied {dll_name} to bindings directory")

    def _generate_pyi_stub(self, verbose: bool = False):
        """Generate .pyi stub file for VSCode IntelliSense with full type support."""
        if not self.registry_file.exists():
            if verbose:
                print("No registry file found, skipping .pyi generation")
            return

        try:
            with open(self.registry_file, 'r') as f:
                registry = json.load(f)
        except Exception as e:
            if verbose:
                print(f"Failed to load registry for .pyi generation: {e}")
            return

        # Handle v2.0 format: {"schema_version": "2.0", "modules": {...}}
        modules = registry.get('modules', registry)

        pyi_file = self.bindings_dir / "api.pyi"

        with open(pyi_file, 'w') as f:
            f.write('"""Auto-generated type stubs for IncludeCPP bindings\n\n')
            f.write('This file provides full type hints for VSCode IntelliSense.\n')
            f.write('Generated by IncludeCPP v2.0+\n')
            f.write('"""\n\n')
            f.write('from typing import Any, List, Dict, Optional, Union, overload\n\n')

            # Generate stubs for each module
            for module_name, module_info in modules.items():
                f.write(f'class {module_name}:\n')
                f.write(f'    """Module: {module_name}\n\n')

                # Add module docstring with summary
                sources = module_info.get('sources', [])
                if sources:
                    f.write(f'    Sources: {", ".join(sources)}\n')

                deps = module_info.get('dependencies', [])
                if deps:
                    dep_names = [d.get('target', '?') for d in deps]
                    f.write(f'    Dependencies: {", ".join(dep_names)}\n')

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

                # Generate STRUCT types (v2.0+)
                structs = module_info.get('structs', [])
                for struct in structs:
                    struct_name = struct.get('name', '')
                    is_template = struct.get('is_template', False)
                    template_types = struct.get('template_types', [])
                    fields = struct.get('fields', [])
                    doc = struct.get('doc', '')

                    if is_template:
                        # Generate struct for each template type
                        for ttype in template_types:
                            full_name = f"{struct_name}_{ttype}"
                            f.write(f'    class {full_name}:\n')
                            if doc:
                                f.write(f'        """{doc}\n\n')
                                f.write(f'        Template instantiation: {struct_name}<{ttype}>\n')
                                f.write('        """\n\n')
                            else:
                                f.write(f'        """POD struct: {struct_name}<{ttype}>"""\n\n')

                            # Constructor
                            f.write(f'        def __init__(self')
                            for field in fields:
                                field_type = field.get('type', 'Any')
                                field_name = field.get('name', '')
                                # Replace template parameter T
                                if field_type == 'T':
                                    field_type = self._cpp_to_python_type(ttype)
                                else:
                                    field_type = self._cpp_to_python_type(field_type)
                                f.write(f', {field_name}: {field_type} = ...')
                            f.write(') -> None:\n')
                            f.write(f'            """Initialize {full_name}"""\n')
                            f.write('            ...\n\n')

                            # Fields with actual types
                            for field in fields:
                                field_type = field.get('type', 'Any')
                                field_name = field.get('name', '')
                                if field_type == 'T':
                                    field_type = self._cpp_to_python_type(ttype)
                                else:
                                    field_type = self._cpp_to_python_type(field_type)
                                f.write(f'        {field_name}: {field_type}\n')
                            f.write('\n')

                            # to_dict method
                            f.write('        def to_dict(self) -> Dict[str, Any]:\n')
                            f.write('            """Convert struct to dictionary"""\n')
                            f.write('            ...\n\n')

                            # from_dict static method
                            f.write('        @staticmethod\n')
                            f.write(f'        def from_dict(d: Dict[str, Any]) -> "{module_name}.{full_name}":\n')
                            f.write('            """Create struct from dictionary"""\n')
                            f.write('            ...\n\n')
                    else:
                        # Non-template struct
                        f.write(f'    class {struct_name}:\n')
                        if doc:
                            f.write(f'        """{doc}"""\n\n')
                        else:
                            f.write(f'        """POD struct: {struct_name}"""\n\n')

                        # Constructor
                        f.write(f'        def __init__(self')
                        for field in fields:
                            field_type = self._cpp_to_python_type(field.get('type', 'Any'))
                            field_name = field.get('name', '')
                            f.write(f', {field_name}: {field_type} = ...')
                        f.write(') -> None:\n')
                        f.write(f'            """Initialize {struct_name}"""\n')
                        f.write('            ...\n\n')

                        # Fields
                        for field in fields:
                            field_type = self._cpp_to_python_type(field.get('type', 'Any'))
                            field_name = field.get('name', '')
                            f.write(f'        {field_name}: {field_type}\n')
                        f.write('\n')

                        # to_dict method
                        f.write('        def to_dict(self) -> Dict[str, Any]:\n')
                        f.write('            """Convert struct to dictionary"""\n')
                        f.write('            ...\n\n')

                        # from_dict static method
                        f.write('        @staticmethod\n')
                        f.write(f'        def from_dict(d: Dict[str, Any]) -> "{module_name}.{struct_name}":\n')
                        f.write('            """Create struct from dictionary"""\n')
                        f.write('            ...\n\n')

                # Generate classes
                classes = module_info.get('classes', [])
                for cls in classes:
                    class_name = cls.get('name', '')
                    class_doc = cls.get('doc', '')

                    f.write(f'    class {class_name}:\n')
                    if class_doc:
                        f.write(f'        """{class_doc}"""\n\n')
                    else:
                        f.write(f'        """C++ class: {class_name}"""\n\n')

                    # Constructor
                    f.write(f'        def __init__(self, *args: Any, **kwargs: Any) -> None:\n')
                    f.write(f'            """Initialize {class_name} instance"""\n')
                    f.write(f'            ...\n\n')

                    # Initialize method (factory method)
                    f.write(f'        @staticmethod\n')
                    f.write(f'        def Initialize(*args: Any, **kwargs: Any) -> "{module_name}.{class_name}":\n')
                    f.write(f'            """Create and initialize a new {class_name} instance"""\n')
                    f.write(f'            ...\n\n')

                    # Generate methods
                    methods = cls.get('methods', [])
                    if isinstance(methods, list):
                        for method in methods:
                            if isinstance(method, dict):
                                method_name = method.get('name', '')
                                method_doc = method.get('doc', '')
                            else:
                                method_name = str(method)
                                method_doc = ''

                            if method_name:
                                f.write(f'        def {method_name}(self, *args: Any, **kwargs: Any) -> Any:\n')
                                if method_doc:
                                    f.write(f'            """{method_doc}"""\n')
                                else:
                                    f.write(f'            """C++ method: {method_name}"""\n')
                                f.write(f'            ...\n\n')

                    # Generate fields as properties
                    fields = cls.get('fields', [])
                    for field in fields:
                        if isinstance(field, dict):
                            field_name = field.get('name', '')
                            field_type = self._cpp_to_python_type(field.get('type', 'Any'))
                        else:
                            field_name = str(field)
                            field_type = 'Any'

                        if field_name:
                            f.write(f'        {field_name}: {field_type}\n')

                    if not methods and not fields:
                        f.write(f'        pass\n\n')

                # Generate functions
                functions = module_info.get('functions', [])
                for func in functions:
                    if isinstance(func, dict):
                        func_name = func.get('name', '')
                        func_doc = func.get('doc', '')
                    else:
                        func_name = str(func)
                        func_doc = ''

                    if func_name:
                        f.write(f'    @staticmethod\n')
                        f.write(f'    def {func_name}(*args: Any, **kwargs: Any) -> Any:\n')
                        if func_doc:
                            f.write(f'        """{func_doc}"""\n')
                        else:
                            f.write(f'        """C++ function: {func_name}"""\n')
                        f.write(f'        ...\n\n')

                if not classes and not functions and not structs:
                    f.write(f'    pass\n')

                f.write('\n')

        if verbose:
            print(f"Generated type stub: {pyi_file}")

    def _cpp_to_python_type(self, cpp_type: str) -> str:
        """Convert C++ type to Python type hint."""
        # Basic type mappings
        type_map = {
            'int': 'int',
            'long': 'int',
            'short': 'int',
            'float': 'float',
            'double': 'float',
            'bool': 'bool',
            'string': 'str',
            'std::string': 'str',
            'void': 'None',
            'char': 'str',
        }

        # Remove const, &, *
        clean_type = cpp_type.strip()
        clean_type = clean_type.replace('const', '').strip()
        clean_type = clean_type.rstrip('&*').strip()

        # Check basic types
        if clean_type in type_map:
            return type_map[clean_type]

        # Handle vector<T>
        if 'vector<' in clean_type or 'std::vector<' in clean_type:
            start = clean_type.find('<') + 1
            end = clean_type.rfind('>')
            if start > 0 and end > start:
                element_type = clean_type[start:end].strip()
                return f'List[{self._cpp_to_python_type(element_type)}]'

        # Handle map<K,V>
        if 'map<' in clean_type or 'std::map<' in clean_type:
            start = clean_type.find('<') + 1
            end = clean_type.rfind('>')
            if start > 0 and end > start:
                inner = clean_type[start:end]
                # Simple split by comma (doesn't handle nested templates perfectly)
                parts = [p.strip() for p in inner.split(',', 1)]
                if len(parts) == 2:
                    key_type = self._cpp_to_python_type(parts[0])
                    val_type = self._cpp_to_python_type(parts[1])
                    return f'Dict[{key_type}, {val_type}]'

        # Unknown type - return as is
        return 'Any'

    def _install_module(self, verbose: bool = False, skip_if_exists: bool = False):
        """Copy compiled api.pyd to bindings directory."""
        dest = self.bindings_dir / f"api{self._get_pyd_suffix()}"

        # If module already in bindings_dir (direct compilation), skip
        if skip_if_exists and dest.exists():
            if verbose:
                print(f"Module already in place: {dest}")
            # Still copy MinGW DLLs
            self._copy_mingw_dlls(verbose=verbose)
            return

        pyd_locations = [
            self.cmake_build_dir / "Release" / f"api{self._get_pyd_suffix()}",
            self.cmake_build_dir / f"api{self._get_pyd_suffix()}",
        ]

        api_pyd = None
        for loc in pyd_locations:
            if loc.exists():
                api_pyd = loc
                break

        if not api_pyd:
            raise CppBuildError(
                f"Compiled module not found. Searched:\n" +
                "\n".join(f"  - {loc}" for loc in pyd_locations)
            )

        if dest.exists():
            backup = dest.with_suffix(dest.suffix + ".backup")
            shutil.move(str(dest), str(backup))
            if verbose:
                print(f"Backed up old module: {backup}")

        shutil.copy2(str(api_pyd), str(dest))

        if verbose:
            print(f"Installed module: {dest}")

        # Copy MinGW DLLs if on Windows
        self._copy_mingw_dlls(verbose=verbose)

    def _get_pyd_suffix(self) -> str:
        """Get platform-specific Python extension suffix."""
        if platform.system() == "Windows":
            return ".pyd"
        else:
            return ".so"

    def _validate_module(self, verbose: bool = False):
        """Validate that api module can be imported."""
        bindings_path = str(self.bindings_dir)

        if bindings_path not in sys.path:
            sys.path.insert(0, bindings_path)

        try:
            if 'api' in sys.modules:
                del sys.modules['api']

            import api

            if verbose:
                print(f"Module validated: {api.__name__}")
                if hasattr(api, '__doc__'):
                    print(f"  Doc: {api.__doc__}")

        except ImportError as e:
            raise CppValidationError(
                f"Module import failed: {e}\n"
                f"Check that api{self._get_pyd_suffix()} exists in {self.bindings_dir}"
            ) from e

    # ========================================================================
    # v2.0: Per-Module Build System
    # ========================================================================

    def _load_registry(self) -> Dict[str, Any]:
        """Load module registry (v1.6 or v2.0 format)."""
        if not self.registry_file.exists():
            return {"schema_version": "2.0", "modules": {}}

        try:
            with open(self.registry_file, 'r') as f:
                data = json.load(f)

            # Detect version: v1.6.0 has modules at root, v2.0 has "modules" key
            if "schema_version" not in data:
                # v1.6.0 format - convert to v2.0
                return {"schema_version": "1.6", "modules": data}

            return data

        except json.JSONDecodeError:
            return {"schema_version": "2.0", "modules": {}}

    def _parse_all_modules(self, verbose: bool = False) -> Dict[str, Dict]:
        """Parse all .cp files and return module descriptors.

        Returns:
            Dict mapping module_name -> module_descriptor (from registry JSON)
        """
        # Run plugin_gen to generate registry
        self._generate_bindings(verbose=verbose)

        # Load registry to get module info
        registry = self._load_registry()
        return registry.get("modules", {})

    def _build_dependency_graph(self, modules: Dict[str, Dict]) -> List[List[str]]:
        """Build topological dependency order using Kahn's algorithm.

        Args:
            modules: Dict of module_name -> module_descriptor

        Returns:
            List of dependency levels: [[level0_modules], [level1_modules], ...]
            Each level can be compiled in parallel.

        Raises:
            CppBuildError: If circular dependency detected
        """
        # Build adjacency list
        graph = {}
        in_degree = {}

        for module_name in modules:
            graph[module_name] = []
            in_degree[module_name] = 0

        # Build edges: dependency -> module (reversed for build order)
        for module_name, descriptor in modules.items():
            for dep in descriptor.get('dependencies', []):
                dep_module = dep.get('target')
                if dep_module not in modules:
                    raise CppBuildError(
                        f"Module '{module_name}' depends on unknown module '{dep_module}'"
                    )

                # dep_module must be built before module_name
                graph[dep_module].append(module_name)
                in_degree[module_name] += 1

        # Kahn's algorithm for topological sort with levels
        levels = []
        visited = set()

        while len(visited) < len(modules):
            # Find all nodes with in_degree 0 (no dependencies)
            current_level = []
            for module in modules:
                if module not in visited and in_degree[module] == 0:
                    current_level.append(module)

            if not current_level:
                # Circular dependency detected
                remaining = set(modules.keys()) - visited
                raise CppBuildError(
                    f"Circular dependency detected among: {', '.join(remaining)}\n"
                    f"Cannot determine build order."
                )

            levels.append(current_level)

            # Update in_degrees
            for module in current_level:
                visited.add(module)
                for neighbor in graph.get(module, []):
                    in_degree[neighbor] -= 1

        return levels

    def _module_needs_rebuild(self, module_name: str, module_info: Dict, registry: Dict) -> tuple[bool, str]:
        """Check if module needs rebuild using hashes.

        Returns:
            (needs_rebuild: bool, reason: str)
        """
        pyd_name = f"api_{module_name}"
        pyd_path = self.bindings_dir / f"{pyd_name}{self._get_pyd_suffix()}"

        # Check if .pyd exists
        if not pyd_path.exists():
            return (True, f".pyd not found: {pyd_path.name}")

        # Get stored hashes
        old_registry = registry.get('modules', {}).get(module_name, {})
        stored_hashes = old_registry.get('hashes', {})

        # Check source file hashes
        for source_file in module_info.get('sources', []):
            source_path = self.project_root / source_file
            if not source_path.exists():
                source_path = self.include_dir / source_file

            if source_path.exists():
                current_hash = self._compute_hash(source_path)
                stored_hash = stored_hashes.get(source_path.name, "0")

                if current_hash != stored_hash:
                    return (True, f"Source changed: {source_path.name}")

        # Check .cp file hash
        cp_file = module_info.get('cp_file', '')
        if cp_file:
            cp_path = Path(cp_file)
            if cp_path.exists():
                current_hash = self._compute_hash(cp_path)
                stored_hash = stored_hashes.get(f"{module_name}.cp", "0")

                if current_hash != stored_hash:
                    return (True, f".cp file changed")

        # Check if dependencies were rebuilt more recently
        for dep in module_info.get('dependencies', []):
            dep_module = dep.get('target')
            dep_info = registry.get('modules', {}).get(dep_module, {})

            dep_built = dep_info.get('last_built', '')
            self_built = old_registry.get('last_built', '')

            if dep_built > self_built:
                return (True, f"Dependency rebuilt: {dep_module}")

        return (False, "Up to date")

    def rebuild(self,
                modules: Optional[List[str]] = None,
                incremental: bool = True,
                parallel: bool = False,
                clean: bool = False,
                verbose: bool = False) -> bool:
        """v2.0: Rebuild modules with incremental and per-module support.

        Args:
            modules: List of specific modules to rebuild (None = all)
            incremental: Use incremental compilation (skip unchanged)
            parallel: Compile independent modules in parallel (not implemented yet)
            clean: Force clean rebuild (ignore incremental)
            verbose: Print detailed output

        Returns:
            True if build succeeded
        """
        if verbose:
            print("=" * 60)
            print("IncludeCPP v2.0 Build System")
            print("=" * 60)
            print(f"Project: {self.config.config.get('project', 'unnamed')}")
            print(f"Incremental: {incremental and not clean}")
            print("=" * 60)

        # Phase 1: Build generator
        if verbose:
            print("\n[1/5] Building plugin generator...")
        self._build_generator(verbose=verbose)

        # Phase 2: Parse all .cp files (generates registry)
        if verbose:
            print("\n[2/5] Parsing .cp files...")
        all_modules = self._parse_all_modules(verbose=verbose)

        if not all_modules:
            raise CppBuildError("No modules found in plugins directory")

        # Phase 3: Load existing registry for incremental check
        if verbose:
            print("\n[3/5] Determining rebuild targets...")

        registry = self._load_registry()

        # Determine what to rebuild
        if clean:
            to_rebuild = list(all_modules.keys())
            if verbose:
                print(f"  Clean build: rebuilding all {len(to_rebuild)} modules")

        elif modules is not None:
            # User specified modules
            to_rebuild = [m for m in modules if m in all_modules]
            if verbose:
                print(f"  User specified: {', '.join(to_rebuild)}")

        elif incremental:
            # Incremental: only rebuild changed modules
            to_rebuild = []
            for module_name, module_info in all_modules.items():
                needs_rebuild, reason = self._module_needs_rebuild(module_name, module_info, registry)
                if needs_rebuild:
                    to_rebuild.append(module_name)
                    if verbose:
                        print(f"  -> {module_name}: {reason}")

            if not to_rebuild:
                if verbose:
                    print("  All modules up to date!")
                return True

        else:
            # Full rebuild
            to_rebuild = list(all_modules.keys())
            if verbose:
                print(f"  Full rebuild: {len(to_rebuild)} modules")

        # Phase 4: Build dependency order
        if verbose:
            print(f"\n[4/5] Building {len(to_rebuild)} module(s)...")

        try:
            dep_levels = self._build_dependency_graph(all_modules)

            # Filter to only modules we need to rebuild
            filtered_levels = []
            for level in dep_levels:
                filtered = [m for m in level if m in to_rebuild]
                if filtered:
                    filtered_levels.append(filtered)

            if verbose and len(filtered_levels) > 1:
                print(f"  Build order: {len(filtered_levels)} dependency level(s)")

        except CppBuildError as e:
            raise CppBuildError(f"Dependency resolution failed: {e}") from e

        # Phase 5: Build modules (sequential for now, parallel in future)
        # For v2.0, we still use monolithic build but with per-module tracking
        # True per-module builds will come later in implementation
        success = self.rebuild_all(verbose=verbose)

        if verbose:
            print("\n" + "=" * 60)
            print("BUILD COMPLETED")
            print("=" * 60)

        return success

    def rebuild_all(self, verbose: bool = False) -> bool:
        """Complete build process with CMake fallback to direct compilation.

        1. Build/update plugin_gen.exe
        2. Generate bindings.cpp
        3. Try CMake build, fallback to direct compilation if CMake fails
        4. Validate module
        """
        use_direct = False

        # Phase 1: Always needed
        basic_steps = [
            ("Building plugin generator", self._build_generator),
            ("Generating bindings", self._generate_bindings),
        ]

        for i, (desc, func) in enumerate(basic_steps):
            if verbose:
                print(f"\n[{i+1}/7] {desc}...")
            try:
                func(verbose=verbose)
            except Exception as e:
                raise CppBuildError(f"{desc} failed: {e}") from e

        # Phase 2: Try CMake, fallback to direct compilation
        step_num = len(basic_steps) + 1

        try:
            # Try CMake build
            if verbose:
                print(f"\n[{step_num}/7] Generating CMake config...")
            self._generate_cmake(verbose=verbose)

            if verbose:
                print(f"\n[{step_num+1}/7] Configuring CMake...")
            self._configure_cmake(verbose=verbose)

            if verbose:
                print(f"\n[{step_num+2}/7] Compiling C++...")
            self._compile_cpp(verbose=verbose)

            if verbose:
                print(f"\n[{step_num+3}/7] Installing module...")
            self._install_module(verbose=verbose)

        except CppBuildError as e:
            if verbose:
                print(f"\nCMake build failed: {e}")
                print("Falling back to direct compilation...")

            # Direct compilation fallback
            if verbose:
                print(f"\n[{step_num}/7] Compiling with direct g++ (fallback)...")
            try:
                self._compile_direct(verbose=verbose)
                # Module is already in bindings_dir, skip install
                use_direct = True
            except Exception as e2:
                raise CppBuildError(f"Both CMake and direct compilation failed.\nCMake: {e}\nDirect: {e2}") from e2

        # Phase 3: Validate
        if verbose:
            print(f"\n[7/7] Validating module...")
        try:
            self._validate_module(verbose=verbose)
        except Exception as e:
            raise CppBuildError(f"Module validation failed: {e}") from e

        # Phase 4: Generate .pyi stub for VSCode IntelliSense
        try:
            self._generate_pyi_stub(verbose=verbose)
        except Exception as e:
            if verbose:
                print(f"Warning: .pyi generation failed: {e}")
            # Don't fail build if .pyi generation fails

        if verbose:
            print(f"\n{'='*60}")
            print("BUILD SUCCESSFUL!")
            if use_direct:
                print("(Used direct compilation fallback)")
            print(f"{'='*60}")
            print(f"Module: {self.bindings_dir / ('api' + self._get_pyd_suffix())}")
            print(f"Registry: {self.registry_file}")
            print(f"Type Stubs: {self.bindings_dir / 'api.pyi'}")

        return True
