"""Package workflow orchestrator."""

from __future__ import annotations

import asyncio
import os
import platform
import shutil
import subprocess
import time
from dataclasses import dataclass, field
from functools import cached_property
from pathlib import Path
from typing import Any, Protocol

from ..components.archiver import PyArchiveConfig, PyArchiver
from ..components.embedinstaller import EmbedInstaller
from ..components.libpacker import (
    PyLibPacker,
    PyLibPackerConfig,
)
from ..components.loader import PyLoaderGenerator
from ..components.sourcepacker import PySourcePacker
from ..core.concurrent import PerformanceOptimizer
from ..core.config import WorkflowConfig
from ..core.constants import (
    CLEANABLE_DIRS,
    LOG_SEPARATOR,
    PROTECTED_DIRS,
)
from ..models.solution import Solution

if False:  # TYPE_CHECKING
    from ..models.project import Project

from pytola.core.logger import get_logger

logger = get_logger(__name__)
is_windows = platform.system() == "Windows"
ext = ".exe" if is_windows else ""
WINDOWS_ACCESS_VIOLATION_CODE = -1073741819


class CleaningStrategy(Protocol):
    """Protocol for cleaning strategies."""

    def should_clean(self, entry: Path) -> bool:
        """Determine if an entry should be cleaned."""
        ...

    def clean_entry(self, entry: Path) -> tuple[bool, str]:
        """Clean an entry and return success status and message."""
        ...


class StandardCleaningStrategy:
    """Standard cleaning strategy implementation."""

    @staticmethod
    def should_clean(entry: Path) -> bool:
        """Determine if entry should be cleaned using standard rules."""
        # Special case: projects.json file should always be cleaned
        if entry.is_file():
            return entry.name == "projects.json"

        # Protected directories starting with dot
        if entry.name.startswith(".") and entry.name in PROTECTED_DIRS:
            return False

        # Clean temporary and build directories
        return entry.name.startswith(".") or entry.name.startswith("__") or entry.name in CLEANABLE_DIRS

    @staticmethod
    def clean_entry(entry: Path) -> tuple[bool, str]:
        """Clean entry and return success status with message."""
        if not entry.exists():
            return True, "Entry does not exist"

        try:
            if entry.is_dir():
                shutil.rmtree(entry)
                return True, f"Removed directory: {entry}"
            if entry.is_file():
                entry.unlink()
                return True, f"Removed file: {entry}"
            return False, f"Unknown entry type: {entry}"
        except Exception as e:
            return False, f"Failed to remove {entry}: {e}"


@dataclass(frozen=True)
class PackageWorkflow:
    """Package workflow orchestrator using strategy pattern for cleaning."""

    root_dir: Path
    config: WorkflowConfig
    cleaning_strategy: CleaningStrategy = field(
        default_factory=StandardCleaningStrategy,
    )

    @cached_property
    def solution(self) -> Solution:
        """Get the solution for the root directory."""
        return Solution.from_directory(root_dir=self.root_dir)

    @cached_property
    def projects(self) -> dict[str, Project]:
        """Get the projects for the solution."""
        return self.solution.projects

    @cached_property
    def sorted_projects(self) -> dict[str, Project]:
        """Get the sorted projects for the solution."""
        return dict(sorted(self.projects.items()))

    @property
    def dist_dir(self) -> Path:
        """Get distribution directory path."""
        return self.config.dist_dir

    @property
    def build_dir(self) -> Path:
        """Get build directory path."""
        return self.config.build_dir

    def clean_project(self) -> None:
        """Clean build artifacts and package files using strategy pattern."""
        logger.info("Cleaning build artifacts using strategy pattern...")

        entries_to_clean = [entry for entry in self.root_dir.iterdir() if self.cleaning_strategy.should_clean(entry)]

        if not entries_to_clean:
            logger.info("No build artifacts found to clean")
            return

        # Track cleaning results
        cleaned_dirs: list[str] = []
        cleaned_files: list[str] = []
        failed_operations: list[str] = []

        for entry in entries_to_clean:
            success, message = self.cleaning_strategy.clean_entry(entry)
            if success:
                if entry.is_dir():
                    cleaned_dirs.append(str(entry))
                else:
                    cleaned_files.append(str(entry))
                logger.debug(message)
            else:
                failed_operations.append(message)
                logger.warning(message)

        # Summary logging
        logger.info(
            f"Cleaned {len(cleaned_dirs)} directories and {len(cleaned_files)} file(s)",
        )

        if failed_operations:
            logger.error(f"Failed operations: {len(failed_operations)}")

    @staticmethod
    async def _run_sync_task(name: str, setup_func: Any) -> None:
        """Run a synchronous task in thread pool executor.

        Args:
            name: Name of the task for logging
            setup_func: Function that returns the task to execute
        """
        logger.info(LOG_SEPARATOR)
        logger.info(f"Packing {name}...")

        loop = asyncio.get_running_loop()
        await loop.run_in_executor(None, setup_func)
        logger.info(f"{name.capitalize()} packed.")

    async def pack_embed_python(self) -> None:
        """Pack embed python."""

        def _run() -> None:
            installer = EmbedInstaller(
                root_dir=self.root_dir,
                cache_dir=self.config.cache_dir,
                offline=self.config.offline,
            )
            installer.run()

        await self._run_sync_task("embed python", _run)

    async def pack_loaders(self) -> None:
        """Pack loaders for all projects concurrently."""

        def _run() -> None:
            generator = PyLoaderGenerator(root_dir=self.root_dir)
            generator.run()

        await self._run_sync_task("loaders", _run)

    async def pack_libraries(self) -> None:
        """Pack libraries for all projects concurrently."""

        def _run() -> None:
            libpacker = PyLibPacker(
                working_dir=self.root_dir,
                config=PyLibPackerConfig(self.config.cache_dir),
            )
            libpacker.run()

        await self._run_sync_task("libraries", _run)

    async def pack_source(self) -> None:
        """Pack source code for all projects concurrently."""

        def _run() -> None:
            source_packer = PySourcePacker(root_dir=self.root_dir)
            source_packer.run()

        await self._run_sync_task("source code", _run)

    async def pack_archive(self, archive_format: str) -> None:
        """Create archive for all projects.

        Args:
            archive_format: Archive format (zip, tar, gztar, bztar, xztar, 7z, nsis)
        """

        def _run() -> None:
            config = PyArchiveConfig(verbose=self.config.debug)
            archiver = PyArchiver(root_dir=self.root_dir, config=config)
            archiver.archive_projects(format=archive_format)

        await self._run_sync_task(f"{archive_format} archives", _run)

    async def build(self) -> dict[str, Any]:
        """Execute the packaging workflow with concurrent optimization.

        Workflow stages:
        1. Pack embed python (must be first)
        2. Pack loaders, libraries, and source in parallel
        3. Create archive (optional, if archive_type is set)

        Returns
        -------
            Dict with results and summary including output_dir and metadata

        Raises
        ------
            FileNotFoundError: If required directories don't exist
            RuntimeError: If any packaging step fails
        """
        logger.info("Starting packaging workflow execution")
        start_time = time.perf_counter()

        try:
            # Stage 1: Pack embed python (prerequisite for other tasks)
            await self.pack_embed_python()

            # Stage 2: Pack loaders, libraries, and source with optimized concurrency
            logger.info(LOG_SEPARATOR)
            logger.info("Running parallel tasks with performance optimization...")

            # Use performance optimizer for better resource utilization
            async with PerformanceOptimizer(
                max_workers=self.config.max_concurrent,
                use_processes=False,  # I/O bound tasks
            ) as optimizer:
                # Run I/O bound tasks concurrently
                io_tasks = [
                    self.pack_loaders,
                    self.pack_libraries,
                    self.pack_source,
                ]
                await optimizer.run_io_bound_tasks(io_tasks)

            # Stage 3: Create archive (optional)
            if self.config.archive_type:
                await self.pack_archive(self.config.archive_type)

        except (FileNotFoundError, RuntimeError):
            raise
        except Exception as e:
            logger.exception("Workflow execution failed")
            msg = f"Packaging workflow failed: {e}"
            raise RuntimeError(msg) from e

        elapsed = time.perf_counter() - start_time
        logger.info(LOG_SEPARATOR)
        logger.info(f"Packaging workflow completed in {elapsed:.2f}s")
        return {"output_dir": str(self.dist_dir), "metadata": {}}

    def list_projects(self) -> None:
        """List all available projects."""
        logger.info(f"Listing projects in {self.root_dir}")
        logger.debug(f"Listing operation started for directory: {self.root_dir}")
        logger.debug(f"Found {len(self.sorted_projects)} projects:")
        logger.debug(f"Root directory: {self.root_dir}")
        logger.debug(f"Projects: {list(self.sorted_projects.keys())}")

        for project in self.sorted_projects.values():
            logger.info(f"  - {project}")
            logger.debug(f"    Details: {project}")

    def list_executables(self) -> None:
        """List all executables in dist directory."""
        executables = self._scan_executables()
        if not executables:
            logger.info("No executables found in dist directory")
            return
        logger.info(f"Available executables in {self.dist_dir}:")
        for exe in executables:
            logger.info(f"  - {exe.stem}")

    def _scan_executables(self) -> list[Path]:
        """Scan dist directory for executable files.

        Returns
        -------
            List of executable file paths found in dist directory.
        """
        dist_dir = self.dist_dir
        if not dist_dir.exists():
            return []
        return [f for f in dist_dir.glob(f"*{ext}") if f.is_file()]

    def _resolve_executable(self, match_name: str | None) -> Path | None:
        """Resolve executable by scanning dist directory and matching name.

        Args:
            match_name: Executable name or partial name to match

        Returns
        -------
            Path to matched executable, or None if not found/ambiguous.
        """
        executables = self._scan_executables()
        if not executables:
            return None

        # Auto-select if only one executable and no name specified
        if not match_name:
            return executables[0] if len(executables) == 1 else None

        lower_name = match_name.lower()

        # Try exact match (without extension)
        for exe in executables:
            if exe.stem.lower() == lower_name:
                return exe

        # Try fuzzy match (case-insensitive substring)
        matches = [exe for exe in executables if lower_name in exe.stem.lower()]
        return matches[0] if len(matches) == 1 else None

    def run_project(
        self,
        match_name: str | None,
        project_args: list[str] | None = None,
    ) -> None:
        """Run an executable with fuzzy name matching support.

        Args:
            match_name: Executable name or partial name to match
            project_args: Additional arguments to pass to the executable
        """
        exe_path = self._resolve_executable(match_name)

        # Handle executable not found cases
        if not exe_path:
            self._handle_executable_not_found(match_name)
            return

        # Enhanced pre-execution checks
        if not self._pre_execution_checks(exe_path):
            return

        # Build and execute command
        cmd = self._build_executable_command(exe_path, project_args)

        try:
            logger.info(f"正在启动应用程序: {exe_path.name}")
            logger.info(f"工作目录: {self.dist_dir}")
            if project_args:
                logger.info(f"传递参数: {' '.join(project_args)}")

            # Run with enhanced error capture
            result = subprocess.run(
                cmd,
                cwd=self.dist_dir,
                capture_output=True,
                text=True,
                encoding="utf-8",
                errors="replace",
                check=False,
            )

            if result.returncode == 0:
                logger.info(f"✓ {exe_path.stem} 运行成功")
                if result.stdout.strip():
                    logger.info(f"程序输出:\n{result.stdout}")
            else:
                self._handle_execution_error(exe_path, result)

        except FileNotFoundError:
            logger.exception(f"ERROR: 可执行文件未找到: {exe_path}")
            logger.exception("请检查文件是否存在或重新构建项目")
        except PermissionError:
            logger.exception(f"ERROR: 权限不足: 无法执行 {exe_path}")
            logger.exception("请检查文件权限或以管理员身份运行")
        except Exception as e:
            logger.exception(f"ERROR: 启动 {exe_path.name} 时发生未知错误: {e}")
            logger.exception("建议检查系统环境和依赖项")

    def _handle_executable_not_found(self, match_name: str | None) -> None:
        """Handle cases where executable cannot be found with enhanced messaging.

        Args:
            match_name: Executable name that was being searched for
        """
        executables = self._scan_executables()

        logger.error("==================================================")
        if not match_name:
            if len(executables) == 0:
                logger.error("ERROR: 未找到可执行文件")
                logger.error("请先构建项目: pypack build")
            elif len(executables) > 1:
                logger.error("ERROR: 找到多个可执行文件, 请指定要运行的程序:")
                self.list_executables()
            else:
                logger.error("ERROR: 无法自动选择可执行文件")
        else:
            logger.error(f"ERROR: 未找到可执行文件: '{match_name}'")
            if executables:
                logger.error("可用的程序:")
                self.list_executables()
            else:
                logger.error("请先构建项目: pypack build")
        logger.error("==================================================")

    def _pre_execution_checks(self, exe_path: Path) -> bool:
        """Perform pre-execution checks and dependency validation.

        Args:
            exe_path: Path to the executable to be run

        Returns
        -------
            bool: True if all checks pass, False otherwise
        """
        logger.info("==================================================")
        logger.info("执行前检查 - 依赖验证")
        logger.info("==================================================")

        # Check if executable exists
        if not exe_path.exists():
            logger.error(f"ERROR: 可执行文件不存在: {exe_path}")
            return False

        # Check file permissions
        if not os.access(exe_path, os.X_OK):
            logger.error(f"ERROR: 可执行文件没有执行权限: {exe_path}")
            logger.error("请检查文件权限或以管理员身份运行")
            return False

        # Check runtime directory existence
        runtime_dir = self.dist_dir / "runtime"
        if not runtime_dir.exists():
            logger.error(f"ERROR: 运行时目录不存在: {runtime_dir}")
            logger.error("请重新构建项目以确保运行时环境完整")
            return False

        # Check Python interpreter in runtime
        python_exe = runtime_dir / ("python.exe" if is_windows else "bin/python3")
        if not python_exe.exists():
            logger.error(f"ERROR: Python 解释器未找到: {python_exe}")
            logger.error("运行时环境可能不完整, 请重新构建项目")
            return False

        # Check site-packages directory
        site_packages = self.dist_dir / "site-packages"
        if site_packages.exists():
            package_count = len(list(site_packages.iterdir()))
            logger.info(f"✓ 发现 {package_count} 个依赖包")
        else:
            logger.warning("WARNING: 未找到 site-packages 目录, 可能缺少依赖")

        # Check for common GUI dependencies
        if self._is_gui_application(exe_path):
            if not self._check_gui_dependencies():
                logger.warning("WARNING: GUI 依赖可能不完整, 应用程序可能无法正常显示界面")

        logger.info("✓ 所有预执行检查通过")
        logger.info("==================================================")
        return True

    @staticmethod
    def _is_gui_application(exe_path: Path) -> bool:
        """Determine if the executable is a GUI application.

        Args:
            exe_path: Path to executable

        Returns
        -------
            bool: True if it's likely a GUI application
        """
        # Check filename for GUI indicators
        name_lower = exe_path.stem.lower()
        gui_indicators = ["gui", "app", "interface", "window"]
        return any(indicator in name_lower for indicator in gui_indicators)

    def _check_gui_dependencies(self) -> bool:
        """Check for common GUI framework dependencies.

        Returns
        -------
            bool: True if GUI dependencies appear to be present
        """
        site_packages = self.dist_dir / "site-packages"
        if not site_packages.exists():
            return False

        # Check for common GUI frameworks
        gui_frameworks = [
            "PySide2",
            "PyQt5",
            "PyQt6",
            "tkinter",
            "wx",
            "kivy",
            "pygame",
        ]

        found_frameworks = [framework for framework in gui_frameworks if (site_packages / framework).exists()]

        if found_frameworks:
            logger.info(f"✓ 检测到 GUI 框架: {', '.join(found_frameworks)}")
            return True
        logger.warning("WARNING: 未检测到常见的 GUI 框架")
        return False

    @staticmethod
    def _build_executable_command(
        exe_path: Path,
        project_args: list[str] | None,
    ) -> list[str]:
        """Build command list for executable execution.

        Args:
            exe_path: Path to executable
            project_args: Additional arguments to pass

        Returns
        -------
            List of command arguments
        """
        cmd = [str(exe_path.resolve())]
        if project_args:
            cmd.extend(project_args)
        return cmd

    @staticmethod
    def _is_normal_exit_code(exit_code: int) -> bool:
        """判断退出码是否表示正常退出。.

        Args:
            exit_code: 程序退出码

        Returns
        -------
            True if exit code represents normal termination, False otherwise
        """
        # 0: 正常退出
        # 2: 参数错误(argparse.SystemExit)- 这是正常的
        return exit_code in {0, 2}

    @staticmethod
    def _is_serious_error(exit_code: int) -> bool:
        """判断退出码是否表示严重错误。.

        Args:
            exit_code: 程序退出码

        Returns
        -------
            True if exit code represents serious error, False otherwise
        """
        # 1: 一般错误
        # 其他非0且非2的退出码: 严重错误
        return exit_code == 1 or (exit_code != 0 and exit_code != 2)

    def _handle_execution_error(
        self,
        exe_path: Path,
        result: subprocess.CompletedProcess,
    ) -> None:
        """Handle execution errors with detailed error reporting.

        Args:
            exe_path: Path to the executable that failed
            result: Completed process result with error information
        """
        # 只对严重错误显示错误信息
        if PackageWorkflow._is_serious_error(result.returncode):
            logger.error("==================================================")
            logger.error(f"ERROR: 应用程序运行失败: {exe_path.name}")
            logger.error(f"退出代码: {result.returncode}")
        else:
            # 正常退出码(0, 2)只记录info级别日志
            logger.info(f"应用程序正常退出: {exe_path.name} (退出代码: {result.returncode})")
            if result.stderr.strip():
                logger.info(f"程序输出:\n{result.stderr}")
            return

        if result.stderr.strip():
            logger.error("\n错误详情:")
            # 分行显示错误信息, 提高可读性
            error_lines = result.stderr.strip().split("\n")
            max_display_lines = 60  # 显示行数限制到60行以满足测试要求
            for _, line in enumerate(error_lines[:max_display_lines]):
                if line.strip():
                    logger.error(f"  {line}")
            if len(error_lines) > max_display_lines:
                logger.error(
                    f"  ... (还有 {len(error_lines) - max_display_lines} 行错误信息)",
                )
                # 提供完整错误信息的保存选项
                error_file = self.dist_dir / f"error_details_{exe_path.stem}.txt"
                try:
                    error_file.write_text(result.stderr, encoding="utf-8")
                    logger.error(f"  完整错误信息已保存到: {error_file}")
                except Exception as save_error:
                    logger.exception(f"  无法保存完整错误信息: {save_error}")

        # 根据错误类型提供具体建议
        error_output = result.stderr.lower()
        if "modulenotfounderror" in error_output or "importerror" in error_output:
            logger.error("\n💡 可能的解决方案:")
            logger.error("  • 缺少必要的 Python 包, 请重新运行: pypack build")
            logger.error("  • 检查依赖包版本兼容性")
            logger.error("  • 确认所有必需的库都已正确打包")
        elif "filenotfounderror" in error_output:
            logger.error("\n💡 可能的解决方案:")
            logger.error("  • 检查程序文件完整性")
            logger.error("  • 确认运行时文件存在")
            logger.error("  • 重新构建项目")
        elif result.returncode == WINDOWS_ACCESS_VIOLATION_CODE:
            logger.error("\n💡 可能的解决方案:")
            logger.error("  • 程序可能存在内存访问错误")
            logger.error("  • 尝试以管理员身份运行")
            logger.error("  • 检查系统兼容性")
        else:
            logger.error("\nSOLUTION: 通用解决方案:")
            logger.error("  • 检查程序依赖和环境配置")
            logger.error("  • 查看完整的错误日志获取更多信息")
            logger.error("  • 重新构建项目: pypack build")

        logger.error("==================================================")
