"""Source code packaging tool for projects defined in projects.json."""

from __future__ import annotations

import argparse
import logging
import shutil
from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import dataclass, field
from functools import cached_property
from pathlib import Path
from typing import TYPE_CHECKING, Final

from pytola.core.logger import get_logger

from ..models.solution import Solution

if TYPE_CHECKING:
    from ..models.project import Project


__version__ = "0.1.0"
__all__ = ["PySourcePacker"]

logger = get_logger(__name__)

cwd = Path.cwd()


# Default directories and files to exclude during packaging
DEFAULT_EXCLUDE: Final = {
    "__pycache__",
    "*.pyc",
    "*.pyo",
    ".pytest_cache",
    ".benchmarks",
    "tests",
    ".git",
    ".gitignore",
    "dist",
    "build",
    "*.egg-info",
    "node_modules",
    ".idea",
    "*.log",
}

# Default files to include in packaging
DEFAULT_INCLUDE: Final = {
    "*.py",
    "README.md",
    "LICENSE",
    "pyproject.toml",
}


def should_exclude(path: Path, exclude_patterns: set[str]) -> bool:
    """Check if a path should be excluded based on patterns.

    Args:
        path: Path to check
        exclude_patterns: Set of patterns to exclude

    Returns
    -------
        True if path should be excluded, False otherwise
    """
    for pattern in exclude_patterns:
        if pattern.startswith("*"):
            # Wildcard pattern (e.g., "*.pyc")
            if path.match(pattern):
                return True
        else:
            # Exact name pattern
            if path.name == pattern:
                return True
            if path.parts:
                # Check if any parent directory matches
                for part in path.parts:
                    if part == pattern:
                        return True
    return False


def should_include(
    path: Path,
    include_patterns: set[str],
    exclude_patterns: set[str],
) -> bool:
    """Check if a path should be included based on include and exclude patterns.

    Args:
        path: Path to check
        include_patterns: Set of patterns to include
        exclude_patterns: Set of patterns to exclude

    Returns
    -------
        True if path should be included, False otherwise
    """
    # Check if should be excluded first
    if should_exclude(path, exclude_patterns):
        return False

    # If no include patterns, include everything not excluded
    if not include_patterns:
        return True

    # Check if matches any include pattern
    for pattern in include_patterns:
        if pattern.startswith("*"):
            # Wildcard pattern (e.g., "*.py")
            if path.match(pattern):
                return True
        else:
            # Exact name pattern
            if path.name == pattern:
                return True
            if path.is_dir() and path.name == pattern:
                # Include directory and all its contents
                return True

    return False


@dataclass
class ProjectPacker:
    """Helper class to pack individual projects."""

    parent: PySourcePacker
    project: Project
    include_patterns: set[str]
    exclude_patterns: set[str]

    @cached_property
    def is_single_project(self) -> bool:
        """Check if the project is the only one in the solution."""
        return len(self.parent.solution.projects) == 1

    @cached_property
    def project_path(self) -> Path:
        """Get project directory path."""
        # Use the actual directory from toml_path instead of project name
        return self.project.toml_path.parent

    @cached_property
    def output_dir(self) -> Path:
        """Get output directory path."""
        return self.parent.root_dir / "dist" / "src" / self.project.normalized_name

    def validate_project_path(self) -> bool:
        """Validate project path exists and is a directory."""
        if not self.project_path.exists():
            logger.error(f"Project directory {self.project_path} does not exist")
            return False

        if not self.project_path.is_dir():
            logger.error(f"{self.project_path} is not a directory")
            return False

        return True

    def pack(self) -> bool:
        """Pack project source code and resources to dist/src directory.

        Returns
        -------
            True if packing succeeded, False otherwise
        """
        logger.debug(f"Start packing project: {self.project.normalized_name}")

        if not self.project.normalized_name:
            logger.error("Project name cannot be empty")
            return False

        logger.debug(
            f"Project path: {self.project_path}, project_name: {self.project.normalized_name}",
        )
        if not self.validate_project_path():
            return False

        self.output_dir.mkdir(parents=True, exist_ok=True)

        # Copy files
        copied_files = 0
        copied_dirs = 0

        logger.info(
            f"Packing project '{self.project.normalized_name}' to {self.output_dir}",
        )

        try:
            for item in self.project_path.iterdir():
                if item.is_file():
                    # For files, check if should be included
                    if should_include(
                        item,
                        self.include_patterns,
                        self.exclude_patterns,
                    ):
                        dest_file = self.output_dir / item.name
                        shutil.copy2(item, dest_file)
                        logger.debug(f"Copied file: {item.name}")
                        copied_files += 1
                elif item.is_dir() and not should_exclude(item, self.exclude_patterns):
                    # Copy directory recursively with exclude patterns
                    dest_dir = self.output_dir / item.name
                    if dest_dir.exists():
                        shutil.rmtree(dest_dir)
                    shutil.copytree(
                        item,
                        dest_dir,
                        ignore=shutil.ignore_patterns(*DEFAULT_EXCLUDE),
                    )
                    logger.debug(f"Copied directory: {item.name}")
                    copied_dirs += 1

            logger.info(
                f"Successfully packed {self.project.normalized_name}: {copied_files} files, {copied_dirs} directories",
            )
            return True

        except Exception as e:
            logger.exception(
                f"Error packing project {self.project.normalized_name}: {e}",
            )
            return False


@dataclass
class PySourcePacker:
    """Main class for packing Python source code."""

    root_dir: Path
    include_patterns: set[str] = field(default_factory=lambda: DEFAULT_INCLUDE)
    exclude_patterns: set[str] = field(default_factory=lambda: DEFAULT_EXCLUDE)

    @cached_property
    def solution(self) -> Solution:
        """Get the solution from the target directory."""
        return Solution.from_directory(self.root_dir)

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

    def pack_project(self, project_name: str) -> bool:
        """Pack a single project.

        Args:
            project_name: Name of the project to pack

        Returns
        -------
            True if packing succeeded, False otherwise
        """
        projects = self.solution.projects

        if project_name not in projects:
            logger.error(f"Project '{project_name}' not found in projects.json")
            return False

        project = projects[project_name]
        packer = ProjectPacker(
            parent=self,
            project=project,
            include_patterns=self.include_patterns,
            exclude_patterns=self.exclude_patterns,
        )

        return packer.pack()

    def run(self, project_name: str | None = None) -> None:
        """Pack specified project or all projects concurrently.

        Args:
            project_name: Name of the project to pack, or None to pack all projects
        """
        if not self.projects:
            logger.error("No projects found in projects.json")
            return

        # Pack specific project
        if project_name:
            if self.pack_project(project_name):
                logger.info(f"Packed project '{project_name}' successfully")
            else:
                logger.error(f"Failed to pack project '{project_name}'")
            return

        # Pack all projects
        logger.info(f"Start packing, projects: {self.projects}")

        if len(self.projects) == 1:
            # Single project: process directly
            name = next(iter(self.projects))
            success_count = 1 if self.pack_project(name) else 0
        else:
            # Multiple projects: process concurrently
            logger.info(f"Packing {len(self.projects)} projects concurrently...")
            success_count = 0

            with ThreadPoolExecutor(max_workers=None) as executor:
                future_to_name = {executor.submit(self.pack_project, name): name for name in self.projects}

                for future in as_completed(future_to_name):
                    name = future_to_name[future]
                    try:
                        if future.result():
                            success_count += 1
                    except Exception as e:
                        logger.exception(f"Project {name} packing failed: {e}")

        logger.info(
            f"Packed {success_count}/{len(self.projects)} projects successfully",
        )

    def list_projects(self) -> None:
        """List all available projects."""
        for name, project in self.projects.items():
            logger.info(f"{name}: {project}")


def parse_args() -> argparse.Namespace:
    """Create and return an argument parser for the PySourcePack tool."""
    parser = argparse.ArgumentParser(
        description="PySourcePack - Source code packaging tool",
        epilog="Pack source code and resources for projects defined in projects.json",
    )
    parser.add_argument(
        "directory",
        default=str(cwd),
        nargs="?",
        help="Base directory containing project folders (default: current directory)",
    )
    parser.add_argument(
        "-p",
        "--project",
        default=None,
        help="Project name to pack (default: pack all projects)",
    )
    parser.add_argument(
        "-l",
        "--list",
        action="store_true",
        help="List all available projects",
    )
    parser.add_argument(
        "--include",
        nargs="*",
        default=None,
        help="File patterns to include (default: *.py, README.md, pyproject.toml)",
    )
    parser.add_argument(
        "--exclude",
        nargs="*",
        default=None,
        help="File patterns to exclude (default: __pycache__, *.pyc, tests, .benchmarks, etc.)",
    )
    parser.add_argument(
        "--debug",
        "-d",
        action="store_true",
        help="Enable debug mode",
    )
    return parser.parse_args()


def main() -> None:
    """Run main entry point for pysourcepack."""
    args = parse_args()

    root_dir = Path(args.directory)
    if not root_dir.exists():
        logger.error(f"Root directory {root_dir} does not exist")
        return

    if args.debug:
        logger.setLevel(logging.DEBUG)

    # Create packer instance
    include_patterns = set(args.include) if args.include else DEFAULT_INCLUDE
    exclude_patterns = set(args.exclude) if args.exclude else DEFAULT_EXCLUDE

    packer = PySourcePacker(
        root_dir=root_dir,
        include_patterns=include_patterns,
        exclude_patterns=exclude_patterns,
    )

    if args.list:
        packer.list_projects()

    packer.run(project_name=args.project)


if __name__ == "__main__":
    main()
