import sys
import importlib
import zipfile
import tarfile
from pathlib import Path
from typing import Optional
from .path.utils import __compress__, __decompress__, ZIP_TYPES

LIST_ZIP_TYPES = list(ZIP_TYPES)
LIST_ZIP_TYPES.sort(key=lambda x: x[0])  # Sort zip types


class ZipModule:
    """
    A utility class for compressing and decompressing Python modules or packages.

    Features
    --------
    - Compress Python modules/packages into ZIP or TAR formats.
    - Decompress existing archives.
    - Provide stats and metadata about target modules or archives.

    Parameters
    ----------
    module_name : str
                    Name of the module or path to an archive file.
    error : bool, default=True
                    If True, raises exceptions for invalid inputs.
    strict : bool, default=True
                    If True, ensures module_name is importable if not a file.
    config : dict, optional
                    Optional configuration:
                            - "output_dir": Directory for compressed files.
                            - "overwrite": Whether to overwrite existing archives.
    """

    def __init__(
        self,
        module_name: str,
        *,
        error: bool = True,
        strict: bool = True,
        config: Optional[dict] = None,
    ) -> None:
        if not isinstance(module_name, str):
            raise TypeError("Module name must be a string.")

        self.error = error
        self.strict = strict
        self.config = config or {}
        self.config.setdefault("output_dir", Path.cwd() / "archives")
        self.config.setdefault("overwrite", False)
        self.ziptypes = LIST_ZIP_TYPES

        self.config["output_dir"] = Path(self.config["output_dir"])
        self.config["output_dir"].mkdir(parents=True, exist_ok=True)

        self.module_name = module_name
        self.is_archive = any(
            module_name.endswith(ext)
            for ext in (".zip", ".tar", ".tar.gz", ".tar.xz", ".tar.bz2")
        )

        if self.is_archive:
            # Archive mode
            self.zipath = Path(module_name).resolve()
            self.module_path = self.zipath.parent
            self.iszip = zipfile.is_zipfile(self.zipath)
            self.zipname = self.zipath.name
        else:
            # Module mode
            try:
                if module_name not in sys.modules:
                    importlib.import_module(module_name)
            except ModuleNotFoundError:
                if strict:
                    raise ImportError(
                        f"'{module_name}' is not a valid importable module name."
                    )
                else:
                    module = None
            else:
                module = sys.modules.get(module_name)

            if not module or not hasattr(module, "__file__"):
                raise TypeError(
                    f"Cannot locate source for built-in module '{module_name}'."
                )

            self.module_path = Path(module.__file__).resolve().parent
            self.zipname = f"{self.module_name}.zip"
            self.zipath = self.config["output_dir"] / self.zipname
            self.iszip = self.zipath.exists() and zipfile.is_zipfile(self.zipath)

        # Try reading archive contents
        if self.is_archive and self.zipath.exists():
            try:
                if zipfile.is_zipfile(self.zipath):
                    with zipfile.ZipFile(self.zipath) as archive:
                        self.read_archive = archive.namelist()
                elif tarfile.is_tarfile(self.zipath):
                    with tarfile.open(self.zipath) as archive:
                        self.read_archive = archive.getnames()
                else:
                    self.read_archive = []
            except Exception:
                self.read_archive = []
        else:
            self.read_archive = []

    # ============================================================
    # == ZIP MODULE ==
    # ============================================================

    def zipmodule(self, zipmode: str = "zip:def") -> Path:
        """
        Compress the target Python module or package into an archive.

        Parameters
        ----------
        zipmode : str, default='zip:def'
        Compression mode:
                - "zip:def", "zip:lzma", "zip:bz2", "zip:std"
                - "tar", "tar:gz", "tar:bz2", "tar:xz"

        Returns
        -------
        Path
                Path to the generated archive.
        """
        if self.is_archive:
            return self.zipath  # Already an archive

        module_path = self.module_path
        if not module_path.exists():
            raise FileNotFoundError(f"Module path not found: {module_path}")

        self.config["output_dir"].mkdir(exist_ok=True)
        final_archive_path = self.config["output_dir"] / (
            self.module_name + self._extension_for_mode(zipmode)
        )

        if not self.config["overwrite"] and final_archive_path.exists():
            return final_archive_path

        archive_path = __compress__(module_path, zipmode)
        final_archive_path.write_bytes(Path(archive_path).read_bytes())
        Path(archive_path).unlink(missing_ok=True)

        return final_archive_path.resolve()

    # ============================================================
    # == UNZIP MODULE ==
    # ============================================================

    def unzipmodule(
        self, extract_to: Optional[str | Path] = None, mode: Optional[str] = None
    ) -> Path:
        """
        Decompress a module archive created by `zipmodule()`.

        Parameters
        ----------
        extract_to : str | Path | None
                Target directory for extraction.
                If None, extracts under ./extracted/<archive_name>/
        mode : str | None
                Optional explicit decompression mode ("zip", "tar:gz", etc.).

        Returns
        -------
        Path
                Path to the extracted directory.
        """
        if not self.zipath.exists():
            raise FileNotFoundError(f"Archive not found: {self.zipath}")

        base_extract_dir = Path(extract_to) if extract_to else Path.cwd() / "extracted"
        base_extract_dir.mkdir(parents=True, exist_ok=True)

        result = __decompress__(self.zipath, base_extract_dir, mode)
        return Path(result).resolve()

    # ============================================================
    # == STATS ==
    # ============================================================

    def stats(self) -> dict:
        """
        Return metadata and statistics about the module or archive.

        Returns
        -------
        dict
                Information including name, path, archive path, size, and contents.
        """
        return {
            "name": self.module_name,
            "path": str(self.module_path),
            "zip_path": str(self.zipath),
            "is_archive": self.is_archive,
            "exists": self.zipath.exists(),
            "size": self.zipath.stat().st_size if self.zipath.exists() else None,
            "contents": self.read_archive,
        }

    # ============================================================
    # == INTERNAL UTILITIES ==
    # ============================================================

    def _extension_for_mode(self, mode: str) -> str:
        """Return the appropriate file extension for a given compression mode."""
        ext_map = {
            "zip:def": ".zip",
            "zip:lzma": ".zip",
            "zip:bz2": ".zip",
            "zip:std": ".zip",
            "tar": ".tar",
            "tar:gz": ".tar.gz",
            "tar:bz2": ".tar.bz2",
            "tar:xz": ".tar.xz",
        }
        return ext_map.get(mode, ".zip")
