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"

        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)

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

        try:
            if verbose:
                print(f"Running: {' '.join(cmd)}")
            result = subprocess.run(cmd, capture_output=True, text=True, check=True)
            if verbose and result.stdout:
                print(result.stdout)
        except subprocess.CalledProcessError as e:
            raise CppBuildError(
                f"Generator compilation failed:\n{e.stderr}"
            ) from e

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

    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)} plugin(s): {[f.name for f in cp_files]}")

        return cp_files

    def _generate_bindings(self, verbose: bool = False):
        """Run plugin_gen.exe to generate bindings.cpp."""
        cp_files = self._scan_plugins(verbose)

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

        cmd = [
            str(self.gen_exe),
            str(self.plugins_dir),
            str(bindings_cpp),
            str(sources_txt),
            str(self.registry_file)
        ]

        try:
            if verbose:
                print(f"Running generator: {' '.join(cmd)}")
            result = subprocess.run(cmd, capture_output=True, text=True, check=True, cwd=str(self.project_root))
            if verbose and result.stdout:
                print(result.stdout)
        except subprocess.CalledProcessError as e:
            raise CppBuildError(
                f"Plugin generation failed:\n{e.stderr}"
            ) from e

        if not bindings_cpp.exists():
            raise CppBuildError(f"bindings.cpp not generated: {bindings_cpp}")

        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)
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 = [
                "Visual Studio 17 2022",
                "Visual Studio 16 2019",
                "MinGW Makefiles",
                "Ninja"
            ]
        else:
            generators = ["Unix Makefiles", "Ninja"]

        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)

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

            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 _install_module(self, verbose: bool = False):
        """Copy compiled api.pyd to bindings directory."""
        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)
            )

        dest = self.bindings_dir / f"api{self._get_pyd_suffix()}"

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

    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

    def rebuild_all(self, verbose: bool = False) -> bool:
        """Complete build process.

        1. Build/update plugin_gen.exe
        2. Scan user's plugins/ directory
        3. Generate bindings.cpp
        4. Generate CMakeLists.txt
        5. Configure CMake
        6. Compile C++
        7. Install api.pyd
        8. Validate module
        """
        steps = [
            ("Building plugin generator", self._build_generator),
            ("Generating bindings", self._generate_bindings),
            ("Generating CMake config", self._generate_cmake),
            ("Configuring CMake", self._configure_cmake),
            ("Compiling C++", self._compile_cpp),
            ("Installing module", self._install_module),
            ("Validating module", self._validate_module),
        ]

        for i, (desc, func) in enumerate(steps):
            if verbose:
                print(f"\n[{i+1}/{len(steps)}] {desc}...")

            try:
                func(verbose=verbose)
            except Exception as e:
                raise CppBuildError(f"{desc} failed: {e}") from e

        if verbose:
            print(f"\n{'='*60}")
            print("BUILD SUCCESSFUL!")
            print(f"{'='*60}")
            print(f"Module: {self.bindings_dir / ('api' + self._get_pyd_suffix())}")
            print(f"Registry: {self.registry_file}")

        return True
