from __future__ import annotations

import logging
from pathlib import Path
import subprocess
import sys

logger = logging.getLogger(__name__)

# ── Standard-library modules that must never be sent to pip ──
# Built from sys.stdlib_module_names (3.10+) with a hardcoded fallback.
def _build_stdlib_set() -> frozenset[str]:
    if hasattr(sys, "stdlib_module_names"):
        return frozenset(sys.stdlib_module_names)
    return frozenset({
        "__future__", "abc", "aifc", "argparse", "array", "ast", "asynchat",
        "asyncio", "asyncore", "atexit", "audioop", "base64", "bdb",
        "binascii", "binhex", "bisect", "builtins", "bz2", "calendar",
        "cgi", "cgitb", "chunk", "cmath", "cmd", "code", "codecs",
        "codeop", "collections", "colorsys", "compileall", "concurrent",
        "configparser", "contextlib", "contextvars", "copy", "copyreg",
        "cProfile", "crypt", "csv", "ctypes", "curses", "dataclasses",
        "datetime", "dbm", "decimal", "difflib", "dis", "distutils",
        "doctest", "email", "encodings", "enum", "errno", "faulthandler",
        "fcntl", "filecmp", "fileinput", "fnmatch", "fractions",
        "ftplib", "functools", "gc", "getopt", "getpass", "gettext",
        "glob", "grp", "gzip", "hashlib", "heapq", "hmac", "html",
        "http", "idlelib", "imaplib", "imghdr", "imp", "importlib",
        "inspect", "io", "ipaddress", "itertools", "json", "keyword",
        "lib2to3", "linecache", "locale", "logging", "lzma", "mailbox",
        "mailcap", "marshal", "math", "mimetypes", "mmap", "modulefinder",
        "multiprocessing", "netrc", "nis", "nntplib", "numbers",
        "operator", "optparse", "os", "ossaudiodev", "pathlib", "pdb",
        "pickle", "pickletools", "pipes", "pkgutil", "platform",
        "plistlib", "poplib", "posix", "posixpath", "pprint",
        "profile", "pstats", "pty", "pwd", "py_compile", "pyclbr",
        "pydoc", "queue", "quopri", "random", "re", "readline",
        "reprlib", "resource", "rlcompleter", "runpy", "sched",
        "secrets", "select", "selectors", "shelve", "shlex", "shutil",
        "signal", "site", "smtpd", "smtplib", "sndhdr", "socket",
        "socketserver", "spwd", "sqlite3", "ssl", "stat", "statistics",
        "string", "stringprep", "struct", "subprocess", "sunau",
        "symtable", "sys", "sysconfig", "syslog", "tabnanny",
        "tarfile", "telnetlib", "tempfile", "termios", "test", "textwrap",
        "threading", "time", "timeit", "tkinter", "token", "tokenize",
        "trace", "traceback", "tracemalloc", "tty", "turtle",
        "turtledemo", "types", "typing", "unicodedata", "unittest",
        "urllib", "uu", "uuid", "venv", "warnings", "wave", "weakref",
        "webbrowser", "winreg", "winsound", "wsgiref", "xdrlib", "xml",
        "xmlrpc", "zipapp", "zipfile", "zipimport", "zlib",
        "_thread", "typing_extensions",
    })

_STDLIB_MODULES: frozenset[str] = _build_stdlib_set()


def is_stdlib(name: str) -> bool:
    top = name.split(".")[0]
    return top in _STDLIB_MODULES


IMPORT_TO_PIP: dict[str, str] = {
    "sklearn": "scikit-learn",
    "cv2": "opencv-python",
    "PIL": "pillow",
    "yaml": "pyyaml",
    "bs4": "beautifulsoup4",
    "attr": "attrs",
    "dateutil": "python-dateutil",
    "dotenv": "python-dotenv",
    "skimage": "scikit-image",
    "serial": "pyserial",
    "usb": "pyusb",
    "Bio": "biopython",
    "Crypto": "pycryptodome",
    "gi": "PyGObject",
    "wx": "wxPython",
    "lxml": "lxml",
}


def resolve_pip_name(import_name: str) -> str:
    return IMPORT_TO_PIP.get(import_name, import_name)


def _python_cmd(python_executable: str | None) -> list[str]:
    return [python_executable or sys.executable]


def is_installed(package_name: str, python_executable: str | None = None) -> bool:
    try:
        proc = subprocess.run(
            [*_python_cmd(python_executable), "-m", "pip", "show", package_name],
            capture_output=True,
            text=True,
            timeout=30,
        )
        return proc.returncode == 0
    except subprocess.SubprocessError:
        return False


def freeze_requirements(
    python_executable: str | None = None,
    lock_path: Path | None = None,
) -> str:
    try:
        proc = subprocess.run(
            [*_python_cmd(python_executable), "-m", "pip", "freeze", "--exclude-editable"],
            capture_output=True,
            text=True,
            timeout=60,
        )
        output = proc.stdout if proc.returncode == 0 else ""
        if lock_path is not None:
            lock_path.parent.mkdir(parents=True, exist_ok=True)
            lock_path.write_text(output, encoding="utf-8")
        return output
    except subprocess.SubprocessError:
        return ""


def install_missing(
    packages: list[str],
    *,
    python_executable: str | None = None,
    lock_path: Path | None = None,
) -> dict[str, str]:
    results: dict[str, str] = {}
    to_install: list[str] = []

    for pkg in packages:
        import_name = pkg.split(">=")[0].split("==")[0].split("[")[0].strip()
        if is_stdlib(import_name):
            results[pkg] = "stdlib"
            logger.debug("Skipping stdlib module: %s", import_name)
            continue
        pip_name = resolve_pip_name(import_name)
        check_name = pip_name if pip_name != import_name else import_name
        if is_installed(check_name, python_executable) or is_installed(import_name, python_executable):
            results[pkg] = "already_installed"
            logger.info("Package already installed: %s (pip: %s)", import_name, pip_name)
        else:
            to_install.append(pip_name)

    if to_install:
        logger.info("Installing missing packages: %s", to_install)
        try:
            proc = subprocess.run(
                [*_python_cmd(python_executable), "-m", "pip", "install", *to_install],
                capture_output=True,
                text=True,
                timeout=600,
            )
            if proc.returncode == 0:
                for pkg in to_install:
                    results[pkg] = "installed"
            else:
                error = proc.stderr[:400] or proc.stdout[:400] or "unknown install error"
                for pkg in to_install:
                    results[pkg] = f"error: {error}"
        except subprocess.TimeoutExpired:
            for pkg in to_install:
                results[pkg] = "error: install timed out"

    freeze_requirements(python_executable, lock_path)
    return results
