"""
SAGE Bytecode Compiler
编译Python源码为.pyc文件，隐藏企业版源代码
"""

import os
import py_compile
import re
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path

from rich.console import Console
from rich.progress import Progress

from .exceptions import SAGEDevToolkitError

console = Console()


class BytecodeCompiler:
    """字节码编译器 - 集成到SAGE开发工具包"""

    def __init__(self, package_path: Path, temp_dir: Path | None = None):
        """
        初始化字节码编译器

        Args:
            package_path: 要编译的包路径
            temp_dir: 临时目录，如果为None则自动创建
        """
        self.package_path = Path(package_path)
        self.temp_dir = temp_dir
        self.compiled_path = None
        self._binary_extensions = []

        if not self.package_path.exists():
            raise SAGEDevToolkitError(f"Package path does not exist: {package_path}")

        if not self.package_path.is_dir():
            raise SAGEDevToolkitError(f"Package path is not a directory: {package_path}")

    def compile_package(self, output_dir: Path | None = None, use_sage_home: bool = True) -> Path:
        """
        编译包为字节码

        Args:
            output_dir: 输出目录，如果为None则使用SAGE home目录或临时目录
            use_sage_home: 是否使用SAGE home目录作为默认输出

        Returns:
            编译后的包路径
        """
        console.print(f"🔧 编译包: {self.package_path.name}", style="cyan")

        # 确定输出目录
        if output_dir:
            self.temp_dir = Path(output_dir)
            self.temp_dir.mkdir(parents=True, exist_ok=True)
        elif use_sage_home:
            # 使用SAGE home目录
            sage_home = Path.home() / ".sage"
            self.temp_dir = sage_home / "dist"
            self.temp_dir.mkdir(parents=True, exist_ok=True)
            console.print(f"📁 使用SAGE home目录: {self.temp_dir}", style="blue")
        else:
            self.temp_dir = Path(
                tempfile.mkdtemp(prefix=f"sage_bytecode_{self.package_path.name}_")
            )

        # 复制项目结构
        self.compiled_path = self.temp_dir / self.package_path.name
        console.print(f"📁 复制项目结构到: {self.compiled_path}")
        try:
            # symlinks=True: 复制符号链接本身，而不是跟随链接复制文件
            # 这样可以避免符号链接指向外部路径时的问题
            shutil.copytree(self.package_path, self.compiled_path, symlinks=True)
        except Exception as e:
            console.print(f"❌ 复制项目结构失败: {e}", style="red")
            import traceback

            traceback.print_exc()
            raise

        # 编译Python文件
        self._compile_python_files()

        # 删除.py源文件
        self._remove_source_files()

        # 更新pyproject.toml排除源文件
        self._update_pyproject()

        console.print(f"✅ 包编译完成: {self.package_path.name}", style="green")
        return self.compiled_path

    def _compile_python_files(self):
        """编译所有Python文件"""
        python_files = list(self.compiled_path.rglob("*.py"))

        # 过滤要跳过的文件
        files_to_compile = []
        skipped_count = 0
        for py_file in python_files:
            if self._should_skip_file(py_file):
                skipped_count += 1
                continue
            files_to_compile.append(py_file)

        if not files_to_compile:
            console.print("  ⚠️ 没有找到需要编译的Python文件", style="yellow")
            return

        console.print(
            f"  📝 找到 {len(files_to_compile)} 个Python文件需要编译 (跳过 {skipped_count} 个)"
        )

        # 检查和保留二进制扩展文件
        self._preserve_binary_extensions()

        # 使用进度条显示编译进度
        with Progress() as progress:
            task = progress.add_task("编译Python文件", total=len(files_to_compile))

            compiled_count = 0
            failed_count = 0
            failed_files = []

            for py_file in files_to_compile:
                try:
                    # 编译为.pyc
                    pyc_file = py_file.with_suffix(".pyc")
                    py_compile.compile(py_file, pyc_file, doraise=True)
                    compiled_count += 1

                except py_compile.PyCompileError as e:
                    failed_count += 1
                    failed_files.append((py_file.relative_to(self.compiled_path), str(e)))
                except Exception as e:
                    failed_count += 1
                    failed_files.append((py_file.relative_to(self.compiled_path), str(e)))

                progress.update(task, advance=1)

        console.print(f"  📊 编译统计: 成功 {compiled_count}, 失败 {failed_count}")

        # Only show failed files if there are any
        if failed_files:
            console.print("  ❌ 编译失败的文件:", style="red")
            for file_path, error in failed_files[:5]:  # Show max 5 failed files
                console.print(f"     - {file_path}: {error[:80]}", style="red")
            if len(failed_files) > 5:
                console.print(f"     ... 和其他 {len(failed_files) - 5} 个文件", style="red")

    def _preserve_binary_extensions(self):
        """检查和保留二进制扩展文件"""
        # 查找所有二进制扩展文件
        extensions = []
        for ext in ["*.so", "*.pyd", "*.dylib"]:
            extensions.extend(self.compiled_path.rglob(ext))

        if not extensions:
            console.print("  ℹ️ 未找到二进制扩展文件", style="dim")
            return

        console.print(f"  🔧 找到 {len(extensions)} 个二进制扩展文件")

        # 记录所有扩展文件 (only show details in verbose mode)
        self._binary_extensions = extensions

    def _should_skip_file(self, py_file: Path) -> bool:
        """判断是否应该跳过文件"""
        # 跳过setup.py等特殊文件
        skip_files = ["setup.py", "conftest.py"]

        if py_file.name in skip_files:
            return True

        # 跳过测试文件 - 更精确的模式匹配
        file_str = str(py_file)

        # 检查是否在tests目录中
        if "/tests/" in file_str or file_str.endswith("/tests"):
            return True

        # 检查文件名是否以test_开头或以_test.py结尾
        if py_file.name.startswith("test_") or py_file.name.endswith("_test.py"):
            return True

        return False

    def _remove_source_files(self):
        """删除源文件,只保留字节码"""
        python_files = list(self.compiled_path.rglob("*.py"))

        removed_count = 0
        kept_count = 0

        console.print("  🗑️ 清理源文件...")

        for py_file in python_files:
            # 保留必要的文件
            if self._should_keep_source(py_file):
                kept_count += 1
                continue

            # 对于__init__.py和其他.py文件，如果有对应的.pyc，则删除.py
            pyc_file = py_file.with_suffix(".pyc")
            if pyc_file.exists():
                py_file.unlink()
                removed_count += 1
            else:
                # 如果没有编译成功，保留源文件避免包损坏
                kept_count += 1

        console.print(f"  📊 清理统计: 删除 {removed_count}, 保留 {kept_count}")

    def _should_keep_source(self, py_file: Path) -> bool:
        """判断是否应该保留源文件"""
        # 必须保留的文件
        keep_files = ["setup.py", "_version.py"]

        if py_file.name in keep_files:
            return True

        return False

    def _update_pyproject(self):
        """更新pyproject.toml包含.pyc文件"""
        pyproject_file = self.compiled_path / "pyproject.toml"

        if not pyproject_file.exists():
            console.print("  ⚠️ 未找到pyproject.toml文件", style="yellow")
            return

        try:
            content = pyproject_file.read_text(encoding="utf-8")

            # 检查是否使用了 scikit-build-core
            uses_scikit_build = "scikit_build_core" in content

            if uses_scikit_build:
                console.print("  🔧 检测到 scikit-build-core，切换到 setuptools", style="yellow")

                # 替换 build-backend 为 setuptools
                content = re.sub(
                    r'build-backend\s*=\s*["\']scikit_build_core\.build["\']',
                    'build-backend = "setuptools.build_meta"',
                    content,
                )

                # 简化 build-system requires
                content = re.sub(
                    r"\[build-system\][\s\S]*?(?=\n\[)",
                    '[build-system]\nrequires = ["setuptools>=64", "wheel"]\nbuild-backend = "setuptools.build_meta"\n\n',
                    content,
                )

                # 移除 scikit-build 相关配置
                content = re.sub(r"\[tool\.scikit-build\][\s\S]*?(?=\n\[|\Z)", "", content)
                content = re.sub(r"\[tool\.scikit-build\..*?\][\s\S]*?(?=\n\[|\Z)", "", content)

            # 检查现有的包配置
            has_packages_list = "packages = [" in content  # 静态包列表
            has_packages_find = "[tool.setuptools.packages.find]" in content  # 动态查找
            has_pyc_package_data = (
                '"*.pyc"' in content and "[tool.setuptools.package-data]" in content
            )
            has_include_package_data = "include-package-data = true" in content.lower()

            modified = False

            # 需要添加配置
            if not has_packages_list and not has_packages_find:
                content += """
[tool.setuptools.packages.find]
where = ["src"]
"""
                modified = True

            # 确保include-package-data设置为true
            if not has_include_package_data:
                # 检查是否有[tool.setuptools]部分
                if "[tool.setuptools]" in content:
                    # 在现有部分添加
                    pattern = r"(\[tool\.setuptools\][\s\S]*?)(?=\n\[|\n$|$)"
                    match = re.search(pattern, content)
                    if match:
                        existing_section = match.group(1)
                        if "include-package-data" not in existing_section:
                            updated_section = (
                                existing_section.rstrip() + "\ninclude-package-data = true\n"
                            )
                            content = content.replace(existing_section, updated_section)
                            modified = True
                else:
                    # 添加新部分
                    content += """
[tool.setuptools]
include-package-data = true
"""
                    modified = True

            # 添加package-data配置
            if not has_pyc_package_data:
                # 检查是否已有package-data部分
                if "[tool.setuptools.package-data]" in content:
                    # 需要更新现有的package-data配置
                    pattern = r"(\[tool\.setuptools\.package-data\][\s\S]*?)(?=\n\[|\n$|$)"
                    match = re.search(pattern, content)
                    if match:
                        existing_data = match.group(1)
                        if '"*.pyc"' not in existing_data:
                            # 查找现有的 "*" 键并合并（支持多行数组）
                            star_pattern = r'"(\*)" = \[([^\]]*)\]'
                            star_matches = list(
                                re.finditer(star_pattern, existing_data, re.MULTILINE)
                            )

                            if star_matches:
                                # 找到第一个 "*" 键，合并所有内容到它
                                first_match = star_matches[0]

                                # 收集所有现有的项
                                all_items = []
                                for m in star_matches:
                                    items = m.group(2).strip()
                                    if items:
                                        # 分割并清理每个项
                                        for item in items.split(","):
                                            item = item.strip().strip('"').strip("'")
                                            if item and item not in all_items:
                                                all_items.append(item)

                                # 添加新的二进制文件模式
                                binary_patterns = [
                                    "*.pyc",
                                    "*.pyo",
                                    "__pycache__/*",
                                    "*.so",
                                    "*.pyd",
                                    "*.dylib",
                                ]
                                for pattern in binary_patterns:
                                    if pattern not in all_items:
                                        all_items.append(pattern)

                                # 构建合并后的数组
                                formatted_items = ",\n    ".join(f'"{item}"' for item in all_items)
                                updated_line = f'"*" = [\n    {formatted_items},\n]'

                                # 替换第一个 "*" 键
                                updated_data = existing_data.replace(
                                    first_match.group(0), updated_line
                                )

                                # 删除其他重复的 "*" 键
                                for m in star_matches[1:]:
                                    updated_data = updated_data.replace(m.group(0), "")

                                # 清理多余的空行
                                updated_data = re.sub(r"\n\s*\n\s*\n", "\n\n", updated_data)
                            else:
                                # 在现有配置中添加新的通配符键
                                updated_data = (
                                    existing_data.rstrip()
                                    + '\n"*" = ["*.pyc", "*.pyo", "__pycache__/*", "*.so", "*.pyd", "*.dylib"]\n'
                                )

                            content = content.replace(existing_data, updated_data)
                            modified = True
                else:
                    # 添加新的package-data配置
                    content += """
[tool.setuptools.package-data]
"*" = ["*.pyc", "*.pyo", "__pycache__/*", "*.so", "*.pyd", "*.dylib"]
"""
                    modified = True

            # 清理多余的空行
            content = re.sub(r"\n\n\n+", "\n\n", content)

            # 添加MANIFEST.in文件以确保包含所有二进制文件
            manifest_file = self.compiled_path / "MANIFEST.in"
            manifest_content = """
# 包含所有编译文件和二进制扩展
recursive-include src *.pyc
recursive-include src *.pyo
recursive-include src __pycache__/*
recursive-include src *.so
recursive-include src *.pyd
recursive-include src *.dylib
"""
            manifest_file.write_text(manifest_content, encoding="utf-8")

            # 添加setup.py文件确保包含所有文件
            setup_py_file = self.compiled_path / "setup.py"
            setup_py_content = """
from setuptools import setup

setup(
    include_package_data=True,
    package_data={
        "": ["*.pyc", "*.pyo", "__pycache__/*", "*.so", "*.pyd", "*.dylib"],
    },
)
"""
            setup_py_file.write_text(setup_py_content, encoding="utf-8")

            if modified or uses_scikit_build:
                pyproject_file.write_text(content, encoding="utf-8")
                console.print("  ✅ 更新pyproject.toml配置", style="green")
            else:
                console.print("  ✓ pyproject.toml配置已满足要求", style="dim")

        except Exception as e:
            console.print(f"  ❌ 更新pyproject.toml失败: {e}", style="red")

    def build_wheel(
        self,
        compiled_path: Path | None = None,
    ) -> Path:
        """
        构建wheel包

        Args:
            compiled_path: 已编译的包路径，如果未提供则使用self.compiled_path

        Returns:
            wheel文件路径
        """
        target_path = compiled_path or self.compiled_path

        if not target_path:
            raise SAGEDevToolkitError("Package not compiled yet. Call compile_package() first.")

        console.print(f"📦 构建wheel包: {target_path.name}", style="cyan")

        # 保存当前目录
        original_dir = Path.cwd()

        try:
            # 进入包目录
            os.chdir(target_path)

            # 清理旧构建
            for build_dir in ["dist", "build"]:
                if Path(build_dir).exists():
                    shutil.rmtree(build_dir)
                    console.print(f"  🧹 清理目录: {build_dir}")

            # 验证.pyc文件是否存在
            pyc_files = list(Path(".").rglob("*.pyc"))
            console.print(f"  📊 找到 {len(pyc_files)} 个.pyc文件")

            # 构建wheel（使用 isolation 模式自动处理构建依赖）
            console.print("  🔨 构建wheel...")
            result = subprocess.run(
                [sys.executable, "-m", "build", "--wheel"],
                capture_output=True,
                text=True,
            )

            if result.returncode == 0:
                console.print("  ✅ 构建成功", style="green")

                # 查找构建的wheel文件
                dist_files = list(Path("dist").glob("*.whl"))
                if not dist_files:
                    raise SAGEDevToolkitError("构建完成但未找到wheel文件")

                wheel_file = dist_files[0]  # 通常只有一个wheel文件
                file_size = wheel_file.stat().st_size / 1024  # KB
                console.print(f"    📄 {wheel_file.name} ({file_size:.2f} KB)")

                # 验证wheel内容
                self._verify_wheel_contents(wheel_file)

                # 返回绝对路径
                return wheel_file.resolve()

            else:
                # 构建失败，收集错误信息
                error_msg = "构建失败"
                if result.stderr.strip():
                    error_msg += f": {result.stderr.strip()}"
                if result.stdout.strip():
                    error_msg += f"\n详细信息: {result.stdout.strip()}"
                raise SAGEDevToolkitError(error_msg)

        except Exception as e:
            console.print(f"  💥 构建异常: {e}", style="red")
            raise

        finally:
            # 返回原目录
            os.chdir(original_dir)

    def _verify_wheel_contents(self, wheel_file: Path):
        """验证wheel包内容是否包含.pyc文件"""
        console.print("  🔍 验证wheel包内容...", style="cyan")

        try:
            # 创建临时目录解压wheel
            with tempfile.TemporaryDirectory() as temp_dir:
                temp_path = Path(temp_dir)

                # 解压wheel
                import zipfile

                with zipfile.ZipFile(wheel_file, "r") as zip_ref:
                    zip_ref.extractall(temp_path)

                    # 列出所有文件
                    all_files = list(zip_ref.namelist())

                # 计数
                pyc_count = sum(1 for f in all_files if f.endswith(".pyc"))
                py_count = sum(1 for f in all_files if f.endswith(".py"))
                binary_count = sum(1 for f in all_files if f.endswith((".so", ".pyd", ".dylib")))
                total_count = len(all_files)

                console.print(
                    f"    📊 文件总数: {total_count} (.pyc: {pyc_count}, .py: {py_count}, binary: {binary_count})"
                )

                # 检查包是否太小
                if total_count < 10:
                    console.print(
                        "    ⚠️ 警告: wheel包文件数量过少，可能打包不完整",
                        style="yellow",
                    )

                if pyc_count == 0 and binary_count == 0:
                    console.print("    ❌ 错误: wheel包中没有.pyc或二进制扩展文件！", style="red")
                    console.print("    💡 尝试使用以下步骤修复:")
                    console.print("       1. 确保pyproject.toml中设置了include-package-data = true")
                    console.print("       2. 确保pyproject.toml中设置了package-data配置")
                    console.print("       3. 检查MANIFEST.in文件是否包含了*.pyc和*.so等")

                    # 尝试输出部分文件列表以帮助诊断
                    console.print("    📁 wheel包内容示例:")
                    for f in all_files[:10]:
                        console.print(f"       - {f}")
                    if len(all_files) > 10:
                        console.print(f"       ... 还有 {len(all_files) - 10} 个文件")
                else:
                    console.print("    ✅ wheel包包含编译文件", style="green")

        except Exception as e:
            console.print(f"    ❌ 验证wheel内容失败: {e}", style="red")

    def cleanup_temp_dir(self):
        """清理临时目录"""
        if self.temp_dir and self.temp_dir.exists():
            try:
                shutil.rmtree(self.temp_dir)
                console.print(f"🧹 清理临时目录: {self.temp_dir}", style="dim")
            except Exception as e:
                console.print(f"⚠️ 清理临时目录失败: {e}", style="yellow")


def compile_multiple_packages(
    package_paths: list[Path],
    output_dir: Path | None = None,
    build_wheels: bool = False,
    use_sage_home: bool = True,
    create_symlink: bool = True,
) -> dict[str, bool]:
    """
    编译多个包

    Args:
        package_paths: 包路径列表
        output_dir: 输出目录
    build_wheels: 是否构建wheel包
        use_sage_home: 是否使用SAGE home目录
        create_symlink: 是否创建软链接

    Returns:
        编译结果字典 {package_name: success}
    """
    results = {}

    console.print(f"🎯 批量编译 {len(package_paths)} 个包", style="bold cyan")
    console.print("=" * 60)

    # 创建SAGE home目录软链接（如果需要）
    sage_home_link = None
    if use_sage_home and create_symlink:
        sage_home_link = _create_sage_home_symlink()

    for i, package_path in enumerate(package_paths, 1):
        console.print(f"\n[{i}/{len(package_paths)}] 处理包: {package_path.name}", style="bold")

        try:
            # 编译包
            compiler = BytecodeCompiler(package_path)
            compiler.compile_package(output_dir, use_sage_home)

            # 构建wheel（如果需要）
            if build_wheels:
                compiler.build_wheel()
                results[package_path.name] = True
            else:
                results[package_path.name] = True

            # 不清理临时目录，让用户可以检查结果
            # compiler.cleanup_temp_dir()

        except Exception as e:
            console.print("❌ 处理失败", style="bold red")
            console.print(f"错误: {e}", style="red")
            # 打印完整的异常堆栈
            import traceback

            traceback.print_exc()
            results[package_path.name] = False

    # 显示汇总结果
    console.print("\n" + "=" * 60)
    console.print("📊 编译结果汇总:", style="bold")

    success_count = sum(1 for success in results.values() if success)
    total_count = len(results)

    for package_name, success in results.items():
        status = "✅" if success else "❌"
        style = "green" if success else "red"
        console.print(f"  {status} {package_name}", style=style)

    console.print(f"\n🎉 成功: {success_count}/{total_count}", style="bold green")

    # 显示软链接信息
    if sage_home_link:
        console.print(f"\n🔗 软链接已创建: {sage_home_link} -> ~/.sage", style="blue")

    return results


def _create_sage_home_symlink() -> Path | None:
    """
    在当前目录创建指向SAGE home的软链接

    Returns:
        软链接路径，如果创建失败则返回None
    """

    current_dir = Path.cwd()
    sage_home = Path.home() / ".sage"
    symlink_path = current_dir / ".sage"

    try:
        # 如果软链接已存在，先检查是否指向正确的目标
        if symlink_path.exists() or symlink_path.is_symlink():
            if symlink_path.is_symlink():
                existing_target = symlink_path.readlink()
                if existing_target == sage_home:
                    console.print(f"✓ 软链接已存在: {symlink_path}", style="green")
                    return symlink_path
                else:
                    console.print(
                        f"⚠️ 软链接指向错误目标，重新创建: {existing_target} -> {sage_home}",
                        style="yellow",
                    )
                    symlink_path.unlink()
            else:
                console.print(f"⚠️ 路径已存在且不是软链接: {symlink_path}", style="yellow")
                return None

        # 确保SAGE home目录存在
        sage_home.mkdir(parents=True, exist_ok=True)

        # 创建软链接
        symlink_path.symlink_to(sage_home)
        console.print(f"🔗 创建软链接: {symlink_path} -> {sage_home}", style="green")

        return symlink_path

    except Exception as e:
        console.print(f"❌ 创建软链接失败: {e}", style="red")
        return None


def _get_sage_home_info():
    """显示SAGE home目录信息"""
    sage_home = Path.home() / ".sage"
    dist_dir = sage_home / "dist"

    console.print("📂 SAGE Home 目录信息:", style="bold blue")
    console.print(f"  🏠 Home: {sage_home}")
    console.print(f"  📦 Dist: {dist_dir}")

    if dist_dir.exists():
        compiled_packages = list(dist_dir.iterdir())
        console.print(f"  📊 已编译包: {len(compiled_packages)}")

        for pkg in compiled_packages[:5]:  # 显示前5个
            if pkg.is_dir():
                console.print(f"    📁 {pkg.name}")

        if len(compiled_packages) > 5:
            console.print(f"    ... 和其他 {len(compiled_packages) - 5} 个包")
    else:
        console.print("  📊 已编译包: 0 (目录不存在)")

    # 检查当前目录的软链接
    current_symlink = Path.cwd() / ".sage"
    if current_symlink.exists() and current_symlink.is_symlink():
        target = current_symlink.readlink()
        console.print(f"  🔗 当前软链接: {current_symlink} -> {target}")
    else:
        console.print("  🔗 当前软链接: 不存在")
