#!/usr/bin/env python3
"""Runtime pip installer for QGIS plugin dependencies."""

from __future__ import annotations

import os
import shutil
import subprocess
import sys
from contextlib import redirect_stderr, redirect_stdout
from pathlib import Path
from typing import Callable


def _bundled_pip_root() -> Path | None:
    """Return bundled pip root (vendor/pip) if present."""
    plugin_root = Path(__file__).resolve().parent
    candidate = plugin_root / "vendor" / "pip"
    if (candidate / "pip").is_dir():
        return candidate
    return None


class _CallbackStream:
    """File-like stream that forwards complete lines to a callback."""

    def __init__(self, callback: Callable[[str], None] | None):
        self._callback = callback
        self._buffer = ""

    def write(self, data: str):
        if not self._callback or not data:
            return
        self._buffer += data
        while "\n" in self._buffer:
            line, self._buffer = self._buffer.split("\n", 1)
            if line.strip():
                self._callback(line)

    def flush(self):
        if self._callback and self._buffer.strip():
            self._callback(self._buffer.strip())
        self._buffer = ""


def _is_usable_python(executable: Path) -> bool:
    """Return True when executable behaves like a Python interpreter."""
    if not executable.is_file():
        return False
    try:
        result = subprocess.run(
            [str(executable), "-c", "import sys; print(sys.version_info[0])"],
            capture_output=True,
            text=True,
            timeout=5,
        )
        return result.returncode == 0 and result.stdout.strip() == "3"
    except Exception:
        return False


def _resolve_python_executable() -> str:
    """Resolve a Python executable usable for `-m pip` in QGIS contexts."""
    candidates: list[Path] = []

    if sys.executable:
        candidates.append(Path(sys.executable))

    if sys.prefix:
        candidates.extend(
            [
                Path(sys.prefix) / "bin" / "python3",
                Path(sys.prefix) / "bin" / "python",
                Path(sys.prefix) / "python3",
                Path(sys.prefix) / "python",
            ]
        )

    if sys.platform == "darwin" and sys.executable:
        exe_path = Path(sys.executable).resolve()
        for parent in exe_path.parents:
            if parent.name == "MacOS":
                app_contents = parent.parent
                candidates.extend(
                    [
                        parent / "bin" / "python3",
                        parent / "python3",
                        app_contents / "Resources" / "python" / "bin" / "python3",
                        app_contents / "Resources" / "python" / "python3",
                    ]
                )
                break

    for name in ("python3", "python"):
        discovered = shutil.which(name)
        if discovered:
            candidates.append(Path(discovered))

    seen: set[str] = set()
    unique_candidates: list[Path] = []
    for candidate in candidates:
        normalized = str(candidate)
        if normalized not in seen:
            seen.add(normalized)
            unique_candidates.append(candidate)

    for candidate in unique_candidates:
        if _is_usable_python(candidate):
            return str(candidate)

    return sys.executable


def _run_pip_inprocess(
    target_dir: Path,
    package_specs: list[str],
    pip_root: Path | None,
    progress_callback: Callable[[str], None] | None,
) -> bool:
    """Run pip inside the current Python process (preferred in QGIS)."""
    added_path = False
    if pip_root and str(pip_root) not in sys.path:
        sys.path.insert(0, str(pip_root))
        added_path = True

    try:
        from pip._internal.cli.main import main as pip_main
    except Exception as exc:
        if progress_callback:
            progress_callback(f"Could not import bundled pip in-process: {exc}")
        if added_path:
            try:
                sys.path.remove(str(pip_root))
            except Exception:
                pass
        return False

    pip_args = [
        "install",
        "--disable-pip-version-check",
        "--upgrade",
        "--target",
        str(target_dir),
        *package_specs,
    ]

    if progress_callback:
        progress_callback("Using in-process pip execution")

    stream = _CallbackStream(progress_callback)
    try:
        with redirect_stdout(stream), redirect_stderr(stream):
            result_code = pip_main(pip_args)
        stream.flush()
        return result_code == 0
    except Exception as exc:
        if progress_callback:
            progress_callback(f"In-process pip error: {exc}")
        return False
    finally:
        if added_path:
            try:
                sys.path.remove(str(pip_root))
            except Exception:
                pass


def install_dependencies(
    target_dir: Path | str,
    package_specs: list[str],
    verbose: bool = False,
    progress_callback: Callable[[str], None] | None = None,
) -> bool:
    """
    Install packages to target directory using bundled pip when available.
    
    Args:
        target_dir: Installation target directory
        package_specs: List of pip package specifications
        verbose: Print to stderr (legacy fallback)
        progress_callback: Optional callback to receive real-time log lines
    
    Returns:
        True if installation succeeded, False otherwise
    """
    target_dir = Path(target_dir)
    target_dir.mkdir(parents=True, exist_ok=True)

    env = os.environ.copy()
    pip_root = _bundled_pip_root()
    if pip_root:
        existing_pythonpath = env.get("PYTHONPATH", "")
        env["PYTHONPATH"] = str(pip_root) + (os.pathsep + existing_pythonpath if existing_pythonpath else "")

    if progress_callback:
        progress_callback(f"Installing: {', '.join(package_specs)}\n")

    # Preferred: run pip in-process to avoid QGIS app executable issues on macOS.
    if _run_pip_inprocess(target_dir, package_specs, pip_root, progress_callback):
        return True

    if progress_callback:
        progress_callback("In-process pip failed, trying subprocess fallback")

    python_executable = _resolve_python_executable()

    cmd = [
        python_executable,
        "-m",
        "pip",
        "install",
        "--disable-pip-version-check",
        "--upgrade",
        "--target",
        str(target_dir),
        *package_specs,
    ]

    if progress_callback:
        progress_callback(f"Using Python executable (fallback): {python_executable}")

    try:
        # Use Popen for real-time output if progress_callback provided
        if progress_callback:
            process = subprocess.Popen(
                cmd,
                stdout=subprocess.PIPE,
                stderr=subprocess.STDOUT,
                text=True,
                env=env,
            )
            
            if process.stdout:
                for line in iter(process.stdout.readline, ""):
                    if line:
                        progress_callback(line.rstrip("\n"))
            
            process.wait()
            return process.returncode == 0
        else:
            # Legacy behavior: silent or verbose to stderr
            subprocess.run(
                cmd,
                check=True,
                capture_output=not verbose,
                text=True,
                env=env,
            )
            return True
    except subprocess.CalledProcessError as exc:
        if verbose:
            print(f"pip install failed: {exc}", file=sys.stderr)
            if exc.stdout:
                print(exc.stdout, file=sys.stderr)
            if exc.stderr:
                print(exc.stderr, file=sys.stderr)
        return False
    except Exception as exc:
        if progress_callback:
            progress_callback(f"Error: {exc}")
        if verbose:
            print(f"Error running pip: {exc}", file=sys.stderr)
        return False
