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

                # 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."""
        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 _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":
                # Add Python library
                python_libs = subprocess.check_output(
                    [sys.executable, "-c", "import sysconfig; print(sysconfig.get_config_var('LIBDIR'))"],
                    text=True
                ).strip()
                python_lib = subprocess.check_output(
                    [sys.executable, "-c", "import sysconfig; print(sysconfig.get_config_var('LIBRARY'))"],
                    text=True
                ).strip()
                if python_libs and python_lib:
                    cmd.extend([f"-L{python_libs}", f"-l{python_lib.replace('lib', '').replace('.a', '')}"])
            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)}")

        try:
            result = subprocess.run(cmd, capture_output=True, text=True, check=True, cwd=str(self.bindings_dir))
            if verbose and result.stdout:
                print(result.stdout)
            if verbose:
                print(f"Module compiled: {output_file}")
        except subprocess.CalledProcessError as e:
            raise CppBuildError(
                f"Direct compilation failed:\n{e.stderr}"
            ) from e

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

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

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

        return True
