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):
        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"
        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:
        if platform.system() == "Windows":
            return f"{base_name}.exe"
        return base_name
    def _compute_hash(self, filepath: Path) -> str:
        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:
        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):
        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:
        env = os.environ.copy()
        if platform.system() == "Windows":
            env["MINGW_PREFIX"] = "/mingw64"
            env["MSYSTEM"] = "MINGW64"
            env["PKG_CONFIG_PATH"] = "/mingw64/lib/pkgconfig:/mingw64/share/pkgconfig"
            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):
        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:
        for compiler in ['g++', 'clang++', 'cl']:
            if shutil.which(compiler):
                return compiler
        return None
    def _copy_inbuilds(self, verbose: bool = False) -> int:
        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
            cp_file = inbuild_dir / f"{inbuild_dir.name}.cp"
            if not cp_file.exists():
                continue
            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]:
        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]]:
        user_plugins = self._scan_plugins(verbose)
        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):
        inbuilds_count = self._copy_inbuilds(verbose)
        if verbose and inbuilds_count > 0:
            print(f"Copied {inbuilds_count} inbuilt module(s)")
        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)
        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}")
        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():
                        with open(cp_file, 'r') as f:
                            cp_content = f.read()
                        import re
                        def replace_path(match):
                            path = match.group(1)
                            if not Path(path).is_absolute():
                                abs_path = str((inbuild_dir / path).resolve())
                                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)
                        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"
        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}")
            result = subprocess.run(cmd, capture_output=True, text=True, check=True, cwd=str(self.project_root))
            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
        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):
        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)
    if(WIN32)
        target_link_options(api PRIVATE -static-libgcc -static-libstdc++ -Wl,-Bstatic -lpthread -Wl,-Bdynamic)
    endif()
endif()