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

from __future__ import annotations

import argparse
import json
import logging
import shutil
import sys
from pathlib import Path

__version__ = "0.1.0"

cwd = Path.cwd()
logging.basicConfig(level=logging.INFO, format="%(message)s")
logger = logging.getLogger(__name__)

# Default directories and files to exclude during packaging
DEFAULT_EXCLUDE = {
    "__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 = {
    "*.py",
    "README.md",
    "LICENSE",
    "pyproject.toml",
}


def load_projects(projects_file: Path) -> dict:
    """Load projects from JSON file.

    Args:
        projects_file: Path to projects.json file

    Returns:
        Dictionary containing project information
    """
    if not projects_file.exists():
        logger.error(f"Projects file {projects_file} does not exist")
        return {}

    try:
        with projects_file.open("r", encoding="utf-8") as f:
            return json.load(f)
    except Exception as e:
        logger.error(f"Error loading projects from {projects_file}: {e}")
        return {}


def list_projects(projects: dict) -> None:
    """List all available projects.

    Args:
        projects: Dictionary containing project information
    """
    if not projects:
        logger.warning("No projects found")
        return

    logger.info("Available projects:")
    for name, info in sorted(projects.items()):
        version = info.get("version", "N/A")
        description = info.get("description", "No description")
        logger.info(f"  - {name} (v{version}): {description}")


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


def pack_project(
    project_name: str,
    projects: dict,
    base_dir: Path,
    output_dir: Path | None = None,
    include_patterns: set[str] | None = None,
    exclude_patterns: set[str] | None = None,
) -> bool:
    """Pack project source code and resources to dist/src directory.

    Args:
        project_name: Name of the project to pack
        projects: Dictionary containing project information
        base_dir: Base directory containing project folders
        output_dir: Output directory (default: project's dist/src)
        include_patterns: File patterns to include (default: DEFAULT_INCLUDE)
        exclude_patterns: File patterns to exclude (default: DEFAULT_EXCLUDE)

    Returns:
        True if packing succeeded, False otherwise
    """
    if project_name not in projects:
        logger.error(f"Project '{project_name}' not found in projects.json")
        return False

    project_path = base_dir / project_name

    # If project directory doesn't exist in base_dir, try to search for it
    if not project_path.exists():
        logger.debug(f"Project directory {project_path} does not exist in base_dir")
        # Search in parent directories
        search_dirs = [base_dir, base_dir.parent, base_dir.parent.parent]
        for search_dir in search_dirs:
            candidate = search_dir / project_name
            if candidate.exists() and candidate.is_dir():
                logger.info(f"Found project at {candidate}")
                project_path = candidate
                break

    if not project_path.exists():
        logger.error(f"Project directory for '{project_name}' not found")
        logger.debug(f"Searched in: {base_dir}, {base_dir.parent}, {base_dir.parent.parent}")
        return False

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

    # Determine output directory
    output_dir = project_path / "dist" / "src" if output_dir is None else Path(output_dir) / project_name

    # Create output directory
    try:
        output_dir.mkdir(parents=True, exist_ok=True)
    except Exception as e:
        logger.error(f"Error creating output directory {output_dir}: {e}")
        return False

    # Use default patterns if not specified
    if include_patterns is None:
        include_patterns = DEFAULT_INCLUDE
    if exclude_patterns is None:
        exclude_patterns = DEFAULT_EXCLUDE

    # Copy files
    copied_files = 0
    copied_dirs = 0

    logger.info(f"Packing project '{project_name}' to {output_dir}")

    try:
        for item in project_path.iterdir():
            if item.is_file():
                # For files, check if should be included
                if should_include(item, include_patterns, exclude_patterns):
                    dest_file = output_dir / item.name
                    shutil.copy2(item, dest_file)
                    logger.debug(f"Copied file: {item.name}")
                    copied_files += 1
            elif item.is_dir():
                # For directories, exclude only if in exclude patterns
                if not should_exclude(item, exclude_patterns):
                    # Copy directory recursively with exclude patterns
                    dest_dir = 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 {project_name}: {copied_files} files, {copied_dirs} directories")
        return True

    except Exception as e:
        logger.error(f"Error packing project {project_name}: {e}")
        return False


def main():
    parser = argparse.ArgumentParser(
        description="PySourcePack - Source code packaging tool",
        epilog="Pack source code and resources for projects defined in projects.json",
    )
    parser.add_argument(
        "-i",
        "--input",
        default="projects.json",
        help="Input projects.json file path (default: projects.json)",
    )
    parser.add_argument(
        "-d",
        "--directory",
        default=str(cwd),
        help="Base directory containing project folders (default: current directory)",
    )
    parser.add_argument(
        "-o",
        "--output-dir",
        default=None,
        help="Output directory for packed files (default: <project>/dist/src)",
    )
    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",
        action="store_true",
        help="Enable debug mode",
    )

    args = parser.parse_args()

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

    # Load projects file
    projects_file = Path(args.input)
    if not projects_file.is_absolute():
        # Relative to base directory
        base_dir = Path(args.directory)
        projects_file = base_dir / args.input
    else:
        base_dir = Path(args.directory)

    # If projects_file is relative and not found, try to find it in current directory
    if not projects_file.exists():
        logger.warning(f"Projects file {projects_file} not found, trying current directory...")
        alt_file = Path.cwd() / args.input
        if alt_file.exists():
            projects_file = alt_file
            base_dir = alt_file.parent
            logger.info(f"Found projects file at {projects_file}")

    # Check if user explicitly specified the -d parameter
    explicit_dir = any(arg in ["-d", "--directory"] for arg in sys.argv)
    if not explicit_dir and projects_file.exists():
        # If -d was not explicitly specified, set base_dir to projects_file's parent
        base_dir = projects_file.parent
        logger.debug(f"Auto-set base_dir to {base_dir} (parent of projects.json)")

    projects = load_projects(projects_file)
    if not projects:
        return

    # List projects if requested
    if args.list:
        list_projects(projects)
        return

    # Process include and exclude patterns
    include_patterns = set(args.include) if args.include else None
    exclude_patterns = set(args.exclude) if args.exclude else None

    # Pack specified project or all projects
    if args.project:
        success = pack_project(
            args.project,
            projects,
            base_dir,
            output_dir=Path(args.output_dir) if args.output_dir else None,
            include_patterns=include_patterns,
            exclude_patterns=exclude_patterns,
        )
        if success:
            logger.info(f"Packed project '{args.project}' successfully")
        else:
            logger.error(f"Failed to pack project '{args.project}'")
    else:
        # Pack all projects
        logger.info("Packing all projects...")
        success_count = 0
        for project_name in projects:
            if pack_project(
                project_name,
                projects,
                base_dir,
                output_dir=Path(args.output_dir) if args.output_dir else None,
                include_patterns=include_patterns,
                exclude_patterns=exclude_patterns,
            ):
                success_count += 1

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