"""Libdownloader module for models."""

from __future__ import annotations

import shutil
import subprocess
import tempfile
from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import dataclass, field
from functools import cached_property
from pathlib import Path
from typing import Any, Final

from pytola.core.logger import get_logger

from ..models import Dependency, Project
from ..models.libcache import LibraryCache

logger = get_logger(__name__)

PYPI_MIRRORS: Final[dict[str, str]] = {
    "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
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(frozen=True)
class LibraryDownloader:
    """Download Python packages from PyPI."""

    parent: Any
    cache: LibraryCache
    _mirror: str = "pypi"

    @cached_property
    def mirror_url(self) -> str:
        """URL of the package index mirror.

        Returns
        -------
            Mirror URL
        """
        return PYPI_MIRRORS.get(self._mirror, PYPI_MIRRORS["pypi"])

    @cached_property
    def pip_executable(self) -> str | None:
        """Path to the pip executable.

        Returns
        -------
            Path to pip executable or None if not found
        """
        return self._find_pip_executable()

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

    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 (typically cache_dir)

        Returns
        -------
            Path to downloaded package file (wheel or sdist) 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

            # Prefer wheel files over sdist files
            downloaded_file = None
            for file_path in Path(temp_dir).glob("*.whl"):
                downloaded_file = file_path
                break

            # If no wheel file, look for sdist files (.tar.gz or .zip)
            if not downloaded_file:
                for file_path in Path(temp_dir).glob("*.tar.gz"):
                    downloaded_file = file_path
                    break
                for file_path in Path(temp_dir).glob("*.zip"):
                    downloaded_file = file_path
                    break

            if downloaded_file:
                self.cache.add_package(dep.name, downloaded_file, dep.version)
                shutil.copy2(downloaded_file, dest_dir / downloaded_file.name)
                logger.info(f"Downloaded: {downloaded_file.name}")
                return dest_dir / downloaded_file.name

        return None

    def download_packages(self, project: Project) -> DownloadResult:
        """Download multiple packages concurrently.

        Args:
            project: Project containing dependencies to download

        Returns
        -------
            DownloadResult containing download statistics
        """
        results_list: list[tuple[str, bool]] = []
        cached_count = 0
        cached_packages: set[str] = set()  # Track cached package names efficiently

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

        # Check cache and mark cached packages (single-threaded, safe)
        for dep in dependencies:
            if self.cache.get_package_path(dep.name, dep.version):
                results_list.append((dep.name, True))
                cached_packages.add(dep.name)
                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 cached_packages]
        downloaded_count = 0

        if remaining_deps:
            with ThreadPoolExecutor(
                max_workers=self.parent.config.max_workers,
            ) as executor:
                future_to_dep = {
                    executor.submit(
                        self._download_package,
                        dep,
                        self.cache.cache_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_list.append((dep.name, wheel_file is not None))
                        if wheel_file:
                            downloaded_count += 1
                    except Exception as e:
                        logger.exception(f"Error processing {dep.name}: {e}")
                        results_list.append((dep.name, False))

        # Convert to dictionary for final result
        results = dict(results_list)
        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,
        )
