"""Package Workflow - Advanced Python project packaging tool with workflow orchestration.

This module provides a comprehensive packaging solution that integrates projectparse,
pysourcepack, embedinstall, and pyloadergen tools through a workflow engine to achieve
mixed serial and parallel execution for optimal efficiency.
"""

from __future__ import annotations

import argparse
import asyncio
import json
import logging
import platform
import shutil
from pathlib import Path
from typing import Any

from sfi.workflowengine.workflowengine import (
    CPUTask,
    IOTask,
    SerialTask,
    WorkflowEngine,
)

logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)

__version__ = "1.0.0"
__build__ = "20260120"


class ParseProjectTask(IOTask):
    """Task to parse project configuration using projectparse."""

    def __init__(self, directory: Path, recursive: bool = False, timeout: float = 60.0):
        super().__init__("parse_project", 2.0, [], timeout)
        self.directory = directory
        self.recursive = recursive

    async def execute(self, context: dict[str, Any]) -> Any:
        """Execute project parsing."""
        logger.info(f"Starting project parsing: {self.directory}")

        try:
            # Import projectparse module
            from sfi.projectparse.projectparse import parse_project_data

            # Parse project
            projects = parse_project_data(self.directory, recursive=self.recursive)

            # Save to projects.json
            output_file = self.directory / "projects.json"
            with open(output_file, "w", encoding="utf-8") as f:
                json.dump(projects, f, indent=2, ensure_ascii=False)

            logger.info(f"Found {len(projects)} project(s)")
            for project_name, project_info in projects.items():
                version = project_info.get("version", "N/A") if isinstance(project_info, dict) else "N/A"
                logger.info(f"  - {project_name}: {version}")

            return {"projects": projects, "projects_file": str(output_file)}

        except Exception as e:
            logger.error(f"Failed to parse projects: {e}")
            raise


class PackSourceTask(IOTask):
    """Task to pack source code using pysourcepack."""

    def __init__(self, directory: Path, projects_file: Path, project_name: str | None = None, timeout: float = 120.0):
        super().__init__("pack_source", 5.0, ["parse_project"], timeout)
        self.directory = directory
        self.projects_file = projects_file
        self.project_name = project_name

    async def execute(self, context: dict[str, Any]) -> Any:
        """Execute source packing."""
        logger.info(f"Starting source packing: {self.directory}")

        try:
            # Import pysourcepack module
            from sfi.pysourcepack.pysourcepack import load_projects, pack_project

            # Load projects
            projects = load_projects(self.projects_file)

            # Pack specified project or all projects
            # projects is a dict: {project_name: project_info}
            project_names = [self.project_name] if self.project_name else list(projects.keys())

            packed_projects = []
            for name in project_names:
                logger.info(f"Packing project: {name}")
                pack_project(name, projects, self.directory, output_dir=Path("dist/src"))
                packed_projects.append(name)

            logger.info(f"Packed {len(packed_projects)} project(s)")

            return {"packed_projects": packed_projects, "output_dir": "dist/src"}

        except Exception as e:
            logger.error(f"Failed to pack source: {e}")
            raise


class InstallPythonTask(IOTask):
    """Task to install embedded Python using embedinstall."""

    def __init__(
        self,
        version: str,
        target_dir: Path,
        offline: bool = False,
        timeout: float = 300.0,
    ):
        super().__init__("install_python", 10.0, ["parse_project"], timeout)
        self.version = version
        self.target_dir = target_dir
        self.offline = offline

    async def execute(self, context: dict[str, Any]) -> Any:
        """Execute Python installation."""
        logger.info(f"Starting Python installation: {self.version}")

        try:
            # Import embedinstall module
            from sfi.embedinstall.embedinstall import DEFAULT_CACHE_DIR, install_embed_python

            # Setup cache directory (use default cache directory)
            cache_dir = DEFAULT_CACHE_DIR
            cache_dir.mkdir(parents=True, exist_ok=True)

            # Determine architecture
            arch = platform.machine().lower()
            if arch in ("amd64", "x86_64"):
                arch = "amd64"
            elif arch in ("arm64", "aarch64"):
                arch = "arm64"
            else:
                arch = "amd64"

            # Install embedded Python
            install_result = install_embed_python(
                target_dir=self.target_dir,
                version=self.version,
                cache_dir=cache_dir,
                offline=self.offline,
                keep_cache=True,  # Keep cache for future use
                skip_speed_test=False,
                arch=arch,
            )

            logger.info(f"Python {self.version} installed to {self.target_dir}")

            return {
                "version": self.version,
                "target_dir": str(self.target_dir),
                "install_result": install_result,
            }

        except Exception as e:
            logger.error(f"Failed to install Python: {e}")
            raise


class PackLibrariesTask(IOTask):
    """Task to pack dependencies using pylibpack."""

    def __init__(
        self,
        project_dir: Path,
        output_dir: Path,
        cache_dir: Path | None = None,
        python_version: str | None = None,
        max_workers: int = 4,
        archive_format: str = "zip",
        mirror: str = "aliyun",
        timeout: float = 300.0,
    ):
        super().__init__("pack_libraries", 10.0, ["parse_project"], timeout)
        self.project_dir = project_dir
        self.output_dir = output_dir
        self.cache_dir = cache_dir
        self.python_version = python_version
        self.max_workers = max_workers
        self.archive_format = archive_format
        self.mirror = mirror

    async def execute(self, context: dict[str, Any]) -> Any:
        """Execute library packing."""
        logger.info(f"Starting library packing: {self.project_dir}")

        try:
            # Import pylibpack module
            from sfi.pylibpack.pylibpack import PyLibPack

            # Initialize packer with mirror support
            packer = PyLibPack(cache_dir=self.cache_dir, python_version=self.python_version, mirror=self.mirror)

            # Pack dependencies
            pack_result = packer.pack(
                base_dir=self.project_dir,
                output_dir=self.output_dir,
                max_workers=self.max_workers,
            )

            logger.info(f"Library packing completed: {pack_result.successful}/{pack_result.total}")

            return {
                "pack_result": pack_result,
                "packages_dir": pack_result.output_dir,
            }

        except Exception as e:
            logger.error(f"Failed to pack libraries: {e}")
            raise


class GenerateLoaderTask(CPUTask):
    """Task to generate loader using pyloadergen."""

    def __init__(
        self,
        project_name: str | None,
        is_debug: bool = False,
        compiler: str | None = None,
        timeout: float = 60.0,
        project_dir: Path | None = None,  # Added for backward compatibility with tests
        output_dir: Path | None = None,  # Added for backward compatibility with tests
    ):
        super().__init__("generate_loader", 100000, ["parse_project", "pack_source"], timeout)
        self.project_name = project_name
        self.is_debug = is_debug
        self.compiler = compiler

    async def execute(self, inputs: dict[str, Any]) -> Any:
        """Execute loader generation."""
        try:
            # Import pyloadergen module
            from sfi.pyloadergen.pyloadergen import find_compiler, generate_loader

            # Get projects.json path from parse_project task
            parse_result = inputs["parse_project"].data
            if not isinstance(parse_result, dict):
                logger.error("Parse project result is not a dictionary")
                raise ValueError("Invalid parse result format")

            projects_file = parse_result.get("projects_file")
            if not projects_file:
                logger.error("projects.json path not found in parse result")
                raise ValueError("Missing projects_file in parse result")

            projects_file = Path(projects_file)
            projects = parse_result.get("projects", {})

            # Determine project name
            if self.project_name and self.project_name in projects:
                project_name = self.project_name
            elif len(projects) == 1:
                project_name = next(iter(projects.keys()))
            elif self.project_name:
                logger.error(f"Project '{self.project_name}' not found in parsed projects")
                raise ValueError(f"Project not found: {self.project_name}")
            else:
                logger.error("Multiple projects found but no project name specified")
                raise ValueError("Please specify project name when multiple projects exist")

            logger.info(f"Starting loader generation for project: {project_name}")

            # Find compiler if not specified
            compiler = self.compiler
            if compiler is None:
                compiler = find_compiler()
                if compiler is None:
                    logger.warning("No compiler found, loader generation may fail")

            # Generate loader using simplified generate_loader function
            success = generate_loader(
                project_name=project_name,
                projects_json_path=projects_file,
                is_debug=self.is_debug,
                compiler=compiler,
            )

            if success:
                # Output executable path
                project_dir = projects_file.parent
                output_dir = project_dir / "dist"
                output_exe = output_dir / f"{project_name}.exe"
                output_ent = output_dir / f"{project_name}.ent"

                logger.info(f"Loader generated successfully: {output_exe}")
                logger.info(f"Entry file: {output_ent}")

                return {
                    "project_name": project_name,
                    "output_exe": str(output_exe),
                    "entry_file": str(output_ent),
                    "success": True,
                }
            else:
                logger.error(f"Failed to generate loader for {project_name}")
                return {
                    "project_name": project_name,
                    "success": False,
                }

        except Exception as e:
            logger.error(f"Failed to generate loader: {e}")
            raise


class AssemblePackageTask(SerialTask):
    """Task to assemble final package."""

    def __init__(self, output_dir: Path, dependencies: list[str], timeout: float = 60.0):
        self.output_dir = output_dir

        def assemble(inputs: dict[str, Any], state: dict[str, Any]) -> Any:
            """Assemble final package."""
            logger.info("Starting package assembly")

            try:
                # Determine project directory from the first input if available
                project_dir = None
                if "parse_project" in inputs and hasattr(inputs["parse_project"], "data"):
                    parse_result = inputs["parse_project"].data
                    if isinstance(parse_result, dict) and "projects_file" in parse_result:
                        project_dir = Path(parse_result["projects_file"]).parent
                if not project_dir:
                    project_dir = Path.cwd()

                # Create dist directory
                dist_dir = self.output_dir.parent if self.output_dir.is_file() else self.output_dir
                dist_dir = dist_dir if dist_dir.name == "dist" else dist_dir.parent / "dist"
                dist_dir.mkdir(parents=True, exist_ok=True)

                # Python runtime is already in dist/runtime from InstallPythonTask
                # No need to copy

                # Copy loaders and entry files to dist (already in dist, just log)
                loader_dir = project_dir / "dist"
                if loader_dir.exists():
                    loaders_count = 0
                    for loader in loader_dir.glob("*.ent"):
                        logger.info(f"Entry file: {loader.name}")
                        loaders_count += 1
                    for loader in loader_dir.glob("*.exe"):
                        logger.info(f"Executable: {loader.name}")
                        # Copy loader to dist
                        shutil.copy2(loader, dist_dir / loader.name)
                        loaders_count += 1
                    logger.info(f"Found and copied {loaders_count} loader(s) and entry file(s)")

                # Copy libraries to dist/site-packages
                libs_dir = project_dir / "dist" / "libs"
                if libs_dir.exists():
                    dest_libs_dir = dist_dir / "site-packages"
                    if dest_libs_dir.exists():
                        shutil.rmtree(dest_libs_dir)
                    shutil.copytree(libs_dir, dest_libs_dir)
                    logger.info(f"Libraries copied to {dest_libs_dir}")

                # Create package metadata
                metadata = {
                    "version": __version__,
                    "build": __build__,
                    "assembled_at": asyncio.get_event_loop().time(),
                }

                metadata_file = dist_dir / "metadata.json"
                with open(metadata_file, "w", encoding="utf-8") as f:
                    json.dump(metadata, f, indent=2)

                logger.info(f"Package assembled: {dist_dir}")

                return {"output_dir": str(dist_dir), "metadata": metadata}

            except Exception as e:
                logger.error(f"Failed to assemble package: {e}")
                raise

        super().__init__("assemble_package", assemble, dependencies, timeout)


class PackageWorkflow:
    """Package workflow orchestrator."""

    def __init__(self, directory: Path, config: dict[str, Any]):
        self.directory = directory
        self.config = config
        self.engine = WorkflowEngine(max_concurrent=config.get("max_concurrent", 4))

    def build_workflow(self) -> WorkflowEngine:
        """Build the packaging workflow."""
        logger.info("Building packaging workflow")

        # Phase 1: Parse project
        parse_task = ParseProjectTask(
            directory=self.directory,
            recursive=self.config.get("recursive", False),
        )
        self.engine.add_task(parse_task)

        # Phase 2: Pack source, install Python, and pack libraries (parallel)
        pack_task = PackSourceTask(
            directory=self.directory,
            projects_file=self.directory / "projects.json",
            project_name=self.config.get("project_name"),
        )
        self.engine.add_task(pack_task)

        install_task = InstallPythonTask(
            version=self.config.get("python_version", "3.8.10"),
            target_dir=self.directory / "dist" / "runtime",
            offline=self.config.get("offline", False),
        )
        self.engine.add_task(install_task)

        # Pack libraries task (always enabled unless explicitly skipped)
        if not self.config.get("skip_pack_libraries", False):
            cache_dir_value = self.config.get("cache_dir")
            cache_dir = Path(cache_dir_value) if cache_dir_value else None
            lib_pack_task = PackLibrariesTask(
                project_dir=self.directory,
                output_dir=self.directory / "dist/site-packages",
                cache_dir=cache_dir,
                python_version=self.config.get("python_version", "3.8.10"),
                max_workers=self.config.get("max_concurrent", 4),
                archive_format=self.config.get("archive_format", "zip"),
                mirror=self.config.get("mirror", "aliyun"),
            )
            self.engine.add_task(lib_pack_task)

        # Phase 3: Generate loader
        if self.config.get("generate_loader", False):
            # Create loader task - project name will be determined from parse_project results
            loader_task = GenerateLoaderTask(
                project_name=self.config.get("project_name"),
                is_debug=self.config.get("debug", False),
            )
            self.engine.add_task(loader_task)
            assembly_deps = ["pack_source", "install_python", "generate_loader"]
        else:
            assembly_deps = ["pack_source", "install_python"]

        # Add pack_libraries to assembly dependencies (always added unless skipped)
        if not self.config.get("skip_pack_libraries", False):
            assembly_deps.append("pack_libraries")

        # Phase 4: Assemble final package
        assemble_task = AssemblePackageTask(
            output_dir=self.directory / "dist",
            dependencies=assembly_deps,
        )
        self.engine.add_task(assemble_task)

        return self.engine

    async def execute(self) -> dict[str, Any]:
        """Execute the packaging workflow."""
        logger.info("Starting packaging workflow execution")

        # Build workflow
        self.build_workflow()

        # Execute workflow
        try:
            results = await self.engine.execute_workflow()

            # Get summary
            summary = self.engine.get_execution_summary()

            logger.info("=" * 50)
            logger.info("Workflow execution summary:")
            logger.info(f"  Total tasks: {summary['total_tasks']}")
            logger.info(f"  Completed: {summary['completed']}")
            logger.info(f"  Failed: {summary['failed']}")
            logger.info(f"  Success rate: {summary['success_rate'] * 100:.1f}%")
            logger.info(f"  Total time: {summary['total_execution_time']:.2f}s")
            logger.info("=" * 50)

            return {"results": results, "summary": summary}

        except Exception as e:
            logger.error(f"Workflow execution failed: {e}")
            raise


def list_projects(directory: Path) -> None:
    """List projects from projects.json.

    Args:
        directory: Project directory containing projects.json
    """
    projects_file = directory / "projects.json"

    if not projects_file.exists():
        logger.warning(f"No projects.json found in {directory}")
        logger.info("Run 'pypack build' first to create projects.json")
        return

    try:
        with open(projects_file, encoding="utf-8") as f:
            projects_data = json.load(f)

        # projects_data is a dict: {project_name: project_info}
        if not projects_data:
            logger.info("No projects found in projects.json")
            return

        logger.info(f"Found {len(projects_data)} project(s):")
        logger.info("=" * 60)
        for i, (project_name, project_info) in enumerate(projects_data.items(), 1):
            # Handle both dict and str (in case of malformed data)
            if isinstance(project_info, str):
                version = "N/A"
                entry = "N/A"
                description = ""
            else:
                version = project_info.get("version", "N/A") if isinstance(project_info, dict) else "N/A"
                entry = "main.py"  # Default entry file
                description = project_info.get("description", "") if isinstance(project_info, dict) else ""

            logger.info(f"\n[{i}] {project_name}")
            logger.info(f"    Version: {version}")
            logger.info(f"    Entry: {entry}")
            if description:
                logger.info(f"    Description: {description}")

        logger.info("=" * 60)

    except Exception as e:
        logger.error(f"Failed to load projects.json: {e}")


def run_project(project_name: str | None, directory: Path) -> None:
    """Run a built project.

    Args:
        project_name: Project name or executable name to run (e.g., 'docscan' or 'docscan-gui')
        directory: Project directory
    """
    # Load projects
    projects_file = directory / "projects.json"

    if not projects_file.exists():
        logger.error(f"No projects.json found in {directory}")
        logger.info("Run 'pypack build' first to create projects.json")
        return

    try:
        with open(projects_file, encoding="utf-8") as f:
            projects_data = json.load(f)

        # projects_data is a dict: {project_name: project_info}
        if not projects_data:
            logger.error("No projects found in projects.json")
            return

        # Find all available executables in dist directory
        dist_dir = directory / "dist"
        if not dist_dir.exists():
            logger.error(f"Dist directory not found: {dist_dir}")
            logger.info("Run 'pypack build' first to build the project")
            return

        available_exes = [exe.stem for exe in dist_dir.glob("*.exe")]
        if not available_exes:
            logger.error("No executables found in dist directory")
            logger.info("Run 'pypack build' first to build the project")
            return

        # Determine which executable to run
        target_exe_name = None
        if project_name:
            # Check if project_name is an exact executable name (e.g., 'docscan-gui')
            if project_name in available_exes:
                target_exe_name = project_name
            # Check if project_name matches a project in projects.json
            elif project_name in projects_data:
                # Look for matching executables
                matching_exes = [
                    exe for exe in available_exes if exe == project_name or exe.startswith(f"{project_name}-")
                ]
                if len(matching_exes) == 1:
                    target_exe_name = matching_exes[0]
                elif len(matching_exes) > 1:
                    logger.error(f"Multiple executables found for project '{project_name}':")
                    for exe in matching_exes:
                        logger.info(f"  - {exe}")
                    logger.info("Please specify the full executable name (e.g., 'docscan-gui')")
                    return
                else:
                    logger.error(f"No executable found for project '{project_name}'")
                    return
            else:
                logger.error(f"Project or executable '{project_name}' not found")
                logger.info("Available executables:")
                for exe in available_exes:
                    logger.info(f"** {exe} **")
                return
        else:
            # No project_name specified
            if len(available_exes) == 1:
                target_exe_name = available_exes[0]
                logger.info(f"Running single executable: {target_exe_name}")
            else:
                logger.error("Multiple executables found. Please specify executable name with --project")
                logger.info("Available executables:")
                for exe in available_exes:
                    logger.info(f"** {exe} **")
                return

        # Find the executable
        exe_path = dist_dir / f"{target_exe_name}.exe"

        if not exe_path.exists():
            logger.error(f"Executable not found: {exe_path}")
            logger.info("Run 'pypack build' first to build the project")
            return

        logger.info(f"Starting: {target_exe_name}")
        logger.info(f"Executable: {exe_path}")

        import subprocess

        try:
            # Run from dist directory so the executable can find runtime and entry files
            subprocess.run([str(exe_path)], cwd=str(dist_dir), check=True)
        except subprocess.CalledProcessError as e:
            logger.error(f"Project execution failed with exit code {e.returncode}")
        except KeyboardInterrupt:
            logger.info("Project execution interrupted")
        except FileNotFoundError:
            logger.error(f"Failed to execute {exe_path}")

    except Exception as e:
        logger.error(f"Failed to run project: {e}")


def clean_project(directory: Path) -> None:
    """Clean build artifacts and package files.

    Args:
        directory: Project directory to clean
    """

    # Directories to clean
    def should_clean_entries(dir_path: Path) -> bool:
        return any([
            (dir_path.name.startswith(".") and dir_path.is_dir())
            or (dir_path.name.startswith("__") and dir_path.is_dir())
            or (dir_path.name == "build" and dir_path.is_dir())
            or (dir_path.name == "dist" and dir_path.is_dir())
            or (dir_path.name == "pysfi_build" and dir_path.is_dir())
            or (dir_path.name == "cbuild" and dir_path.is_dir())
            or (dir_path.name == "benchmarks" and dir_path.is_dir())
            or (dir_path.name == "projects.json" and dir_path.is_file())
        ])

    logger.info("Cleaning build artifacts...")
    cleaned_dirs = []
    cleaned_files = []
    entries_to_clean = [f for f in directory.iterdir() if should_clean_entries(f)]
    for entry in entries_to_clean:
        if entry.exists():
            if entry.is_dir():
                logger.info(f"Removing directory: {entry}")
                try:
                    shutil.rmtree(entry)
                    cleaned_dirs.append(str(entry))
                    logger.info(f"  Removed directory: {entry}")
                except Exception as e:
                    logger.warning(f"  Failed to remove {entry}: {e}")
            elif entry.is_file():
                logger.info(f"Removing file: {entry}")
                try:
                    entry.unlink()
                    cleaned_files.append(str(entry))
                    logger.info(f"  Removed file: {entry}")
                except Exception as e:
                    logger.warning(f"  Failed to remove {entry}: {e}")

    if not cleaned_dirs and not cleaned_files:
        logger.info("No build artifacts found")
    else:
        logger.info(f"Cleaned {len(cleaned_dirs)} directorie(s) and {len(cleaned_files)} file(s)")


def main() -> None:
    """Main entry point for package workflow tool."""
    parser = argparse.ArgumentParser(prog="pypack", description="Python packaging tool with workflow orchestration")

    parser.add_argument(
        "action",
        choices=("build", "b", "list", "l", "run", "r", "clean", "c", "version", "v"),
        help="Action to perform",
    )
    parser.add_argument(
        "-p", "--project", type=str, default=None, help="Project or executable name (e.g., 'docscan' or 'docscan-gui')"
    )
    parser.add_argument("--debug", "-d", action="store_true", help="Debug mode")
    parser.add_argument("--python-version", type=str, default="3.8.10", help="Python version to install")
    parser.add_argument("--loader-type", type=str, choices=("console", "gui"), default="console", help="Loader type")
    parser.add_argument(
        "--entry-suffix", type=str, default=".ent", help="Entry file suffix (default: .ent, alternatives: .py)"
    )
    parser.add_argument("--no-loader", action="store_true", help="Skip loader generation")
    parser.add_argument("-r", "--recursive", action="store_true", help="Parse projects recursively")
    parser.add_argument("-o", "--offline", action="store_true", help="Offline mode")
    parser.add_argument("-j", "--jobs", type=int, default=4, help="Maximum concurrent tasks")

    # Library packing arguments
    parser.add_argument("--no-pack-libs", action="store_true", help="Skip packing project dependencies")
    parser.add_argument("--cache-dir", type=str, default=None, help="Custom cache directory for dependencies")
    parser.add_argument(
        "--archive-format",
        type=str,
        choices=("zip", "tar", "gztar", "bztar", "xztar"),
        default="zip",
        help="Archive format for dependencies",
    )
    parser.add_argument(
        "--mirror",
        type=str,
        default="aliyun",
        choices=("pypi", "tsinghua", "aliyun", "ustc", "douban", "tencent"),
        help="PyPI mirror source for faster downloads",
    )

    args = parser.parse_args()
    working_dir = Path.cwd()

    # Setup logging
    if args.debug:
        logging.getLogger().setLevel(logging.DEBUG)

    # Version command
    if args.action in {"version", "v"}:
        logger.info(f"pypack {__version__} (build {__build__})")
        return

    # List command
    if args.action in {"list", "l"}:
        list_projects(working_dir)
        return

    # Run command
    if args.action in {"run", "r"}:
        run_project(args.project, working_dir)
        return

    # Clean command
    if args.action in {"clean", "c"}:
        clean_project(working_dir)
        return

    # Build configuration
    config = {
        "directory": working_dir,
        "project_name": args.project,
        "python_version": args.python_version,
        "loader_type": args.loader_type,
        "entry_suffix": args.entry_suffix,
        "generate_loader": not args.no_loader,
        "recursive": args.recursive,
        "offline": args.offline,
        "max_concurrent": args.jobs,
        "debug": args.debug,
        "skip_pack_libraries": args.no_pack_libs,
        "cache_dir": args.cache_dir,
        "archive_format": args.archive_format,
        "mirror": args.mirror,
    }

    # Create and execute workflow
    workflow = PackageWorkflow(directory=working_dir, config=config)

    try:
        asyncio.run(workflow.execute())
        logger.info("Packaging completed successfully!")
    except Exception as e:
        logger.error(f"Packaging failed: {e}")
        raise


if __name__ == "__main__":
    main()
