"""Python Library Packager - Download and pack Python dependencies with caching support.

This module provides functionality to:
1. Read project information from projects.json or run projectparse if needed
2. Download dependencies to local .cache directory
3. Pack dependencies into a distributable format
4. Support batch processing multiple projects recursively
"""

from __future__ import annotations

import argparse
import json
import logging
import platform
import shutil
import subprocess
import sys
import tempfile
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

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

__version__ = "1.0.0"
__build__ = "20260120"

DEFAULT_CACHE_DIR = Path.home() / ".pysfi" / ".cache" / "python-libs"

PYPI_MIRRORS = {
    "pypi": "https://pypi.org/simple",
    "tsinghua": "https://pypi.tuna.tsinghua.edu.cn/simple",
    "aliyun": "https://mirrors.aliyun.com/pypi/simple/",
    "ustc": "https://pypi.mirrors.ustc.edu.cn/simple/",
    "douban": "https://pypi.douban.com/simple/",
    "tencent": "https://mirrors.cloud.tencent.com/pypi/simple",
}


@dataclass(frozen=True)
class Dependency:
    """Represents a Python package dependency."""

    name: str
    version: str | None = None
    extras: set[str] = field(default_factory=set)
    requires: set[str] = field(default_factory=set)

    def __post_init__(self):
        """Normalize package name after initialization."""
        object.__setattr__(self, "name", normalize_package_name(self.name))

    def __str__(self) -> str:
        """String representation of dependency."""
        if self.extras:
            return f"{self.name}[{','.join(sorted(self.extras))}]{self.version or ''}"
        return f"{self.name}{self.version or ''}"


@dataclass
class DownloadResult:
    """Result of downloading packages."""

    results: dict[str, bool] = field(default_factory=dict)
    total: int = 0
    successful: int = 0
    cached: int = 0
    downloaded: int = 0


@dataclass
class PackResult:
    """Result of packing project dependencies."""

    success: bool
    project: str
    total: int
    successful: int
    failed: int
    packages_dir: str
    extracted_packages: list[str] = field(default_factory=list)
    message: str = ""


@dataclass
class BatchPackResult:
    """Result of packing multiple projects."""

    success: bool
    total: int
    successful: int
    failed: int
    failed_projects: list[str] = field(default_factory=list)
    output_dir: str = ""
    total_time: float = 0.0


@dataclass
class CacheMetadata:
    """Metadata for cached package."""

    name: str
    version: str | None
    path: str
    timestamp: float


@dataclass
class ProjectInfo:
    """Project information for packing dependencies."""

    name: str
    dir: Path
    info: dict


DEV_TOOLS = frozenset({
    "sphinx",
    "sphinx_rtd_theme",
    "watchdog",
    "pytest",
    "coverage",
    "black",
    "mypy",
    "flake8",
    "pylint",
    "isort",
    "pre-commit",
    "tox",
    "nose",
    "unittest",
    "mock",
})
DEV_PATTERNS = frozenset({"dev", "test", "docs", "lint", "example"})
TYPING_PATTERNS = frozenset({"stubs", "typing", "types"})


def normalize_package_name(name: str) -> str:
    """Normalize package name to lowercase with underscores.

    Args:
        name: Package name to normalize

    Returns:
        Normalized package name
    """
    return name.lower().replace("-", "_")


def should_skip_dependency(req_name: str, has_extras: bool = False) -> bool:
    """Check if a dependency should be skipped based on common patterns.

    Args:
        req_name: Package name
        has_extras: Whether the requirement has extras

    Returns:
        True if should skip, False otherwise
    """
    req_lower = req_name.lower()

    # Skip extras
    if has_extras:
        return True

    # Skip dev/test/docs/lint/example patterns
    if any(keyword in req_lower for keyword in DEV_PATTERNS):
        return True

    # Skip typing/stubs dependencies
    if any(keyword in req_lower for keyword in TYPING_PATTERNS):
        return True

    # Skip common dev tools
    return req_lower.replace("-", "_") in DEV_TOOLS


class ProjectParser:
    """Parse project information from projects.json or run projectparse."""

    @staticmethod
    def ensure_projects_json(directory: Path) -> Path | None:
        """Ensure projects.json exists by running projectparse if needed.

        Args:
            directory: Directory to check for projects.json

        Returns:
            Path to projects.json if successful, None otherwise
        """
        projects_json = directory / "projects.json"
        if projects_json.exists():
            logger.debug(f"Found existing projects.json at {projects_json}")
            return projects_json

        logger.info("projects.json not found, running projectparse...")
        try:
            from sfi.projectparse import projectparse as pp

            # Save and restore original argv
            original_argv = sys.argv.copy()
            sys.argv = ["projectparse", "--directory", str(directory), "--output", "projects.json", "--recursive"]
            try:
                pp.main()
            finally:
                sys.argv = original_argv

            if projects_json.exists():
                logger.info("projectparse completed successfully")
                return projects_json
            else:
                logger.error("projectparse failed to generate projects.json")
                return None
        except ImportError:
            # Fallback: run projectparse as script
            sfi_dir = Path(__file__).parent.parent.parent
            projectparse_script = sfi_dir / "projectparse" / "projectparse.py"

            if not projectparse_script.exists():
                logger.error(f"Cannot find projectparse script at {projectparse_script}")
                return None

            result = subprocess.run(
                [
                    sys.executable,
                    str(projectparse_script),
                    "--directory",
                    str(directory),
                    "--output",
                    "projects.json",
                    "--recursive",
                ],
                capture_output=True,
                text=True,
                cwd=directory,
            )

            if result.returncode == 0 and projects_json.exists():
                logger.info("projectparse completed successfully")
                return projects_json

            logger.error(f"projectparse failed: {result.stderr}")
            return None
        except Exception as e:
            logger.error(f"Failed to run projectparse: {e}")
            return None

    @staticmethod
    def load_projects_json(projects_json: Path) -> dict | None:
        """Load project information from projects.json.

        Args:
            projects_json: Path to projects.json file

        Returns:
            Dictionary of project information, None if failed
        """
        try:
            with open(projects_json, encoding="utf-8") as f:
                return json.load(f)
        except Exception as e:
            logger.error(f"Failed to load projects.json: {e}")
            return None

    @staticmethod
    def parse_requirements_from_project(project_info: dict) -> list[Dependency]:
        """Parse dependencies from project info.

        Args:
            project_info: Project information dictionary from projects.json

        Returns:
            List of Dependency objects
        """
        from packaging.requirements import Requirement

        dependencies = []
        dep_list = project_info.get("dependencies", [])

        for dep_str in dep_list:
            try:
                req = Requirement(dep_str)

                if should_skip_dependency(req.name, bool(req.extras)):
                    logger.info(f"Skipping: {dep_str}")
                    continue

                dep = Dependency(name=req.name, version=str(req.specifier) if req.specifier else None)
                dependencies.append(dep)
                logger.debug(f"Parsed dependency: {dep}")
            except Exception as e:
                logger.warning(f"Failed to parse requirement '{dep_str}': {e}")

        logger.info(f"Parsed {len(dependencies)} dependencies for project")
        return dependencies


class LibraryCache:
    """Manage local cache for Python packages."""

    def __init__(self, cache_dir: Path | None = None):
        """Initialize cache manager.

        Args:
            cache_dir: Cache directory path (default: ~/.pysfi/.cache/pylibpack)
        """
        self.cache_dir = cache_dir or DEFAULT_CACHE_DIR
        self.cache_dir.mkdir(parents=True, exist_ok=True)
        self.metadata_file = self.cache_dir / "metadata.json"

    def get_package_path(self, package_name: str, version: str | None = None) -> Path | None:
        """Get cached package path if available.

        Args:
            package_name: Name of the package
            version: Version (optional)

        Returns:
            Path to cached package or None
        """
        # First try filesystem lookup (works even if metadata is missing)
        for whl_file in self.cache_dir.glob("*.whl"):
            parsed_name = self._extract_package_name_from_wheel(whl_file)
            if parsed_name == package_name:
                logger.debug(f"Cache hit (filesystem): {package_name}")
                return whl_file

        # Fallback to metadata lookup
        metadata = self._load_metadata()
        for info in metadata.values():
            if info["name"] == package_name and (version is None or info.get("version") == version):
                path = self.cache_dir / info["path"]
                if path.exists():
                    logger.debug(f"Cache hit (metadata): {package_name}")
                    return path

        logger.debug(f"Cache miss: {package_name}")
        return None

    @staticmethod
    def _extract_package_name_from_wheel(wheel_file: Path) -> str | None:
        """Extract package name from wheel file.

        Args:
            wheel_file: Path to wheel file

        Returns:
            Package name or None
        """
        try:
            filename = wheel_file.stem  # Remove .whl extension
            parts = filename.split("-")
            if parts:
                return normalize_package_name(parts[0])
        except Exception:
            pass
        return None

    @staticmethod
    def _extract_dependencies_from_wheel(wheel_file: Path) -> set[str]:
        """Extract dependencies from wheel METADATA file.

        Args:
            wheel_file: Path to wheel file

        Returns:
            Set of package names (normalized)
        """
        try:
            import re
            import zipfile

            dependencies: set[str] = set()
            with zipfile.ZipFile(wheel_file, "r") as zf:
                metadata_files = [name for name in zf.namelist() if name.endswith("METADATA")]
                if not metadata_files:
                    return dependencies

                metadata_content = zf.read(metadata_files[0]).decode("utf-8", errors="ignore")

                # Parse dependencies from METADATA
                for line in metadata_content.splitlines():
                    if line.startswith("Requires-Dist:"):
                        dep_str = line.split(":", 1)[1].strip()

                        # Skip extras dependencies
                        if re.search(r'extra\s*==\s*["\']?([^"\';\s]+)["\']?', dep_str, re.IGNORECASE):
                            logger.debug(f"Skipping extra dependency: {dep_str}")
                            continue

                        try:
                            from packaging.requirements import Requirement

                            req = Requirement(dep_str)
                            if not should_skip_dependency(req.name, bool(req.extras)):
                                dep_name = normalize_package_name(req.name)
                                dependencies.add(dep_name)
                                logger.debug(f"Found core dependency: {dep_name}")
                        except Exception:
                            pass

            return dependencies
        except Exception as e:
            logger.warning(f"Failed to extract dependencies from {wheel_file.name}: {e}")
            return set()

    def add_package(self, package_name: str, package_path: Path, version: str | None = None) -> None:
        """Add package to cache.

        Args:
            package_name: Name of the package
            package_path: Path to package files
            version: Package version
        """
        # Copy package files to cache (flat structure for wheels, nested for dirs)
        if package_path.is_dir():
            dest_dir = self.cache_dir / package_name
            if dest_dir.exists():
                shutil.rmtree(dest_dir)
            shutil.copytree(package_path, dest_dir)
            relative_path = package_name
        else:
            dest_file = self.cache_dir / package_path.name
            shutil.copy2(package_path, dest_file)
            relative_path = package_path.name

        # Update metadata using CacheMetadata dataclass
        metadata = self._load_metadata()
        metadata[str(package_path)] = CacheMetadata(
            name=package_name,
            version=version,
            path=relative_path,
            timestamp=time.time(),
        ).__dict__
        self._save_metadata(metadata)

        logger.info(f"Cached package: {package_name}")

    def _load_metadata(self) -> dict[str, Any]:
        """Load cache metadata.

        Returns:
            Metadata dictionary
        """
        if self.metadata_file.exists():
            try:
                with open(self.metadata_file, encoding="utf-8") as f:
                    return json.load(f)
            except Exception as e:
                logger.warning(f"Failed to load cache metadata: {e}")

        return {}

    def _save_metadata(self, metadata: dict[str, Any]) -> None:
        """Save cache metadata.

        Args:
            metadata: Metadata dictionary
        """
        with open(self.metadata_file, "w", encoding="utf-8") as f:
            json.dump(metadata, f, indent=2)

    def clear_cache(self) -> None:
        """Clear all cached packages."""
        if self.cache_dir.exists():
            shutil.rmtree(self.cache_dir)
            self.cache_dir.mkdir(parents=True, exist_ok=True)
        logger.info("Cache cleared")


class LibraryDownloader:
    """Download Python packages from PyPI."""

    def __init__(self, cache: LibraryCache, python_version: str | None = None, mirror: str = "pypi"):
        """Initialize downloader.

        Args:
            cache: Cache manager
            python_version: Target Python version for platform-specific packages
            mirror: PyPI mirror source (pypi, tsinghua, aliyun, ustc, douban, tencent)
        """
        self.cache = cache
        self.python_version = python_version or f"{sys.version_info.major}.{sys.version_info.minor}"
        self.platform_name = platform.system().lower() + "_" + platform.machine().lower()
        self.mirror_url = PYPI_MIRRORS.get(mirror, PYPI_MIRRORS["pypi"])
        self.pip_executable = self._find_pip_executable()

    @staticmethod
    def _find_pip_executable() -> str | None:
        """Find pip executable in the system.

        Returns:
            Path to pip executable or None
        """
        return next((shutil.which(cmd) for cmd in ("pip", "pip3")), None)

    def _download_package(self, dep: Dependency, dest_dir: Path) -> Path | None:
        """Download a single package without dependencies.

        Args:
            dep: Dependency to download
            dest_dir: Destination directory

        Returns:
            Path to downloaded wheel file or None
        """
        if not self.pip_executable:
            logger.error("pip not found. Please install pip: python -m ensurepip --upgrade")
            return None

        logger.info(f"Downloading: {dep}")

        with tempfile.TemporaryDirectory() as temp_dir:
            result = subprocess.run(
                [
                    self.pip_executable,
                    "download",
                    "--no-deps",
                    "--index-url",
                    self.mirror_url,
                    "--dest",
                    temp_dir,
                    str(dep),
                ],
                capture_output=True,
                text=True,
                check=False,
            )

            if result.returncode != 0:
                logger.warning(f"pip download failed for {dep}: {result.stderr}")
                return None

            # Find and copy the downloaded wheel file
            for file_path in Path(temp_dir).glob("*.whl"):
                self.cache.add_package(dep.name, file_path, dep.version)
                shutil.copy2(file_path, dest_dir / file_path.name)
                logger.info(f"Downloaded: {file_path.name}")
                return dest_dir / file_path.name

        return None

    def download_packages(
        self,
        dependencies: list[Dependency],
        dest_dir: Path,
        max_workers: int = 4,
    ) -> DownloadResult:
        """Download multiple packages concurrently.

        Args:
            dependencies: List of dependencies to download
            dest_dir: Destination directory
            max_workers: Maximum concurrent downloads

        Returns:
            DownloadResult containing download statistics
        """
        dest_dir.mkdir(parents=True, exist_ok=True)

        results: dict[str, bool] = {}
        cached_count = 0

        logger.info(f"Total direct dependencies: {len(dependencies)}")
        logger.info(f"Using mirror: {self.mirror_url}")

        # Check cache and mark cached packages
        for dep in dependencies:
            if self.cache.get_package_path(dep.name, dep.version):
                results[dep.name] = True
                cached_count += 1
                logger.info(f"Using cached package: {dep}")

        # Download remaining packages concurrently
        remaining_deps = [dep for dep in dependencies if dep.name not in results or not results[dep.name]]
        downloaded_count = 0

        if remaining_deps:
            with ThreadPoolExecutor(max_workers=max_workers) as executor:
                future_to_dep = {executor.submit(self._download_package, dep, dest_dir): dep for dep in remaining_deps}

                for future in as_completed(future_to_dep):
                    dep = future_to_dep[future]
                    try:
                        wheel_file = future.result()
                        results[dep.name] = wheel_file is not None
                        if wheel_file:
                            downloaded_count += 1
                    except Exception as e:
                        logger.error(f"Error processing {dep.name}: {e}")
                        results[dep.name] = False

        successful = sum(1 for v in results.values() if v)
        logger.info(
            f"Processed {successful}/{len(dependencies)} ({cached_count} cached, {downloaded_count} downloaded)"
        )

        return DownloadResult(
            results=results,
            total=len(dependencies),
            successful=successful,
            cached=cached_count,
            downloaded=downloaded_count,
        )


class PyLibPack:
    """Main library packer class."""

    def __init__(
        self,
        cache_dir: Path | None = None,
        python_version: str | None = None,
        mirror: str = "pypi",
    ):
        """Initialize library packer.

        Args:
            cache_dir: Custom cache directory
            python_version: Target Python version
            mirror: PyPI mirror source (pypi, tsinghua, aliyun, ustc, douban, tencent)
        """
        self.cache = LibraryCache(cache_dir)
        self.downloader = LibraryDownloader(self.cache, python_version, mirror)

    def pack_project(self, project: ProjectInfo, output_dir: Path, max_workers: int = 4) -> PackResult:
        """Pack dependencies for a single project.

        Args:
            project: Project information
            output_dir: Output directory
            max_workers: Maximum concurrent downloads

        Returns:
            PackResult containing packing statistics
        """
        logger.info(f"\n{'=' * 60}")
        logger.info(f"Packing dependencies for project: {project.name}")
        logger.info(f"{'=' * 60}")

        dependencies = ProjectParser.parse_requirements_from_project(project.info)

        if not dependencies:
            logger.warning(f"No dependencies found for {project.name}")
            return PackResult(
                success=False,
                message="No dependencies found",
                project=project.name,
                total=0,
                successful=0,
                failed=0,
                packages_dir=str(output_dir),
            )

        logger.info(f"Found {len(dependencies)} dependencies")

        # Download direct dependencies
        download_result = self.downloader.download_packages(
            dependencies,
            self.cache.cache_dir,
            max_workers=max_workers,
        )

        # Build wheel map and collect all required packages recursively
        wheel_map: dict[str, Path] = {
            pkg_name: wheel_file
            for wheel_file in self.cache.cache_dir.glob("*.whl")
            if (pkg_name := self.cache._extract_package_name_from_wheel(wheel_file))
        }

        # Recursively collect all dependencies
        all_packages = self._collect_all_dependencies(wheel_map, list(download_result.results))

        # Extract all required packages (keep order of dependency resolution)
        extracted_packages = []
        for pkg_name in all_packages:
            if pkg_name in wheel_map:
                # Skip if output directory already exists
                output_pkg_dir = output_dir / pkg_name
                if output_pkg_dir.exists():
                    logger.warning(f"Output directory already exists: {output_pkg_dir}")
                    continue

                wheel_file = wheel_map[pkg_name]
                logger.info(f"Extracting {wheel_file.name}...")
                self._extract_wheel(wheel_file, output_dir)
                extracted_packages.append(pkg_name)
                logger.info(f"Extracted {pkg_name}")

        logger.info(f"Pack complete for {project.name}: {download_result.successful}/{download_result.total}")

        return PackResult(
            success=download_result.successful > 0,
            project=project.name,
            total=download_result.total,
            successful=download_result.successful,
            failed=download_result.total - download_result.successful,
            packages_dir=str(output_dir),
            extracted_packages=extracted_packages,
        )

    @staticmethod
    def _collect_all_dependencies(wheel_map: dict[str, Path], root_packages: list[str]) -> set[str]:
        """Recursively collect all dependencies from wheel files.

        Args:
            wheel_map: Mapping of package names to wheel files
            root_packages: List of root package names to start from

        Returns:
            List of all required package names
        """
        all_packages: set[str] = set()
        visited: set[str] = set()

        def visit(pkg_name: str, level: int = 0) -> None:
            """Visit a package and collect its dependencies."""
            if pkg_name in visited:
                return

            visited.add(pkg_name)
            all_packages.add(pkg_name)

            if pkg_name in wheel_map:
                deps = LibraryCache._extract_dependencies_from_wheel(wheel_map[pkg_name])
                logger.debug(f"{'  ' * level}{pkg_name} -> {deps}")
                for dep in deps:
                    visit(dep, level + 1)

        for pkg_name in root_packages:
            visit(pkg_name)

        logger.info(f"Collected {len(all_packages)} packages (including recursive dependencies)")
        logger.info(f"Packages: {all_packages}")
        return all_packages

    @staticmethod
    def _extract_wheel(wheel_file: Path, dest_dir: Path) -> None:
        """Extract wheel file to destination directory.

        Args:
            wheel_file: Path to wheel file
            dest_dir: Destination directory
        """
        import zipfile

        with zipfile.ZipFile(wheel_file, "r") as zf:
            zf.extractall(dest_dir)

    def pack(
        self,
        base_dir: Path,
        output_dir: Path | None = None,
        max_workers: int = 4,
    ) -> BatchPackResult:
        """Pack project dependencies from base directory.

        Args:
            base_dir: Base directory containing projects or a single project
            output_dir: Output directory (default: base_dir/dist/site-packages)
            max_workers: Maximum concurrent downloads

        Returns:
            BatchPackResult containing batch packing statistics
        """
        output_dir = output_dir or base_dir / "dist" / "site-packages"
        logger.info(f"Starting dependency pack for: {base_dir}")

        # Ensure projects.json exists
        projects_json = ProjectParser.ensure_projects_json(base_dir)
        if not projects_json:
            logger.error("Failed to create projects.json")
            return BatchPackResult(
                success=False,
                total=0,
                successful=0,
                failed=0,
                output_dir=str(output_dir),
                total_time=0.0,
            )

        # Load project information
        projects = ProjectParser.load_projects_json(projects_json)
        if not projects:
            logger.error("Failed to load project information")
            return BatchPackResult(
                success=False,
                total=0,
                successful=0,
                failed=0,
                output_dir=str(output_dir),
                total_time=0.0,
            )

        logger.info(f"Found {len(projects)} project(s) to process")

        # Process each project
        total_start = time.perf_counter()
        success_count = 0
        failed_projects: list[str] = []
        use_current_dir = len(projects) == 1

        for project_name, project_info in projects.items():
            project_dir = base_dir if use_current_dir else base_dir / project_name

            if not project_dir.is_dir():
                logger.warning(f"Project directory not found: {project_dir}, skipping")
                failed_projects.append(project_name)
                continue

            project = ProjectInfo(name=project_name, dir=project_dir, info=project_info)
            result = self.pack_project(project, output_dir, max_workers)

            if result.success:
                success_count += 1
            else:
                failed_projects.append(project_name)

        total_time = time.perf_counter() - total_start

        # Summary
        logger.info(f"\n{'=' * 60}")
        logger.info("Summary")
        logger.info(f"{'=' * 60}")
        logger.info(f"Total projects: {len(projects)}")
        logger.info(f"Successfully packed: {success_count}")
        logger.info(f"Failed: {len(failed_projects)}")
        if failed_projects:
            logger.info(f"Failed projects: {', '.join(failed_projects)}")
        logger.info(f"Total time: {total_time:.2f}s")

        return BatchPackResult(
            success=len(failed_projects) == 0,
            total=len(projects),
            successful=success_count,
            failed=len(failed_projects),
            failed_projects=failed_projects,
            output_dir=str(output_dir),
            total_time=total_time,
        )

    def clear_cache(self) -> None:
        """Clear the package cache."""
        self.cache.clear_cache()


def main() -> None:
    """Main entry point for pylibpack tool."""
    parser = argparse.ArgumentParser(
        prog="pylibpack",
        description="Python library packer with caching support",
    )

    parser.add_argument(
        "directory", type=str, nargs="?", default=str(Path.cwd()), help="Base directory containing projects"
    )
    parser.add_argument("--cache-dir", type=str, default=None, help="Custom cache directory")
    parser.add_argument("--python-version", type=str, default=None, help="Target Python version")
    parser.add_argument("-j", "--jobs", type=int, default=4, help="Maximum concurrent downloads")
    parser.add_argument(
        "--mirror",
        type=str,
        default="aliyun",
        choices=("pypi", "tsinghua", "aliyun", "ustc", "douban", "tencent"),
        help="PyPI mirror source for faster downloads in China",
    )
    parser.add_argument("--debug", "-d", action="store_true", help="Debug mode")
    args = parser.parse_args()

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

    # Initialize packer
    cache_dir = Path(args.cache_dir) if args.cache_dir else None
    packer = PyLibPack(cache_dir=cache_dir, python_version=args.python_version, mirror=args.mirror)

    # Pack command
    base_dir = Path(args.directory)
    output_dir = base_dir / "dist" / "site-packages"
    output_dir.mkdir(parents=True, exist_ok=True)

    try:
        result = packer.pack(
            base_dir=base_dir,
            output_dir=output_dir,
            max_workers=args.jobs,
        )

        if result.success:
            logger.info("=" * 50)
            logger.info("Packing summary:")
            logger.info(f"  Total: {result.total}")
            logger.info(f"  Successful: {result.successful}")
            logger.info(f"  Failed: {result.failed}")
            logger.info(f"  Output directory: {result.output_dir}")
            logger.info(f"  Total time: {result.total_time:.2f}s")
            logger.info("=" * 50)
        else:
            logger.error("Packing failed!")
            sys.exit(1)

    except Exception as e:
        logger.error(f"Packing failed: {e}")
        sys.exit(1)


if __name__ == "__main__":
    main()
