from __future__ import annotations

import importlib
import os
import platform
import subprocess
import types
from typing import TYPE_CHECKING

import structlog
from qgis.PyQt.QtCore import QThread, pyqtSignal
from qgis.core import Qgis, QgsSettings, QgsApplication

if TYPE_CHECKING:
    from qgis.gui import QgisInterface
    from qgis.core import QgsRasterLayer

logger = structlog.get_logger(__name__)


def get_qgis_python_path() -> str:
    """Gets the QGIS Python path from settings or tries to determine it
    automatically."""
    settings = QgsSettings()
    logger.debug(f"QGIS Python path: '{settings}', '{type(settings)}'")
    python_path = settings.value("pip_manager/python_path")
    logger.debug(f"QGIS Python path: '{python_path}'")
    if python_path:
        return python_path

    # Attempt to automatically find the path
    qgis_prefix_path = QgsApplication.prefixPath()
    logger.debug(f"QGIS prefix path: '{qgis_prefix_path}'")

    _PY_VERSIONS = ("37", "38", "39", "310", "311", "312")

    possible_paths = []
    if platform.system() == "Windows":
        _EXE = "python.exe"
        possible_paths.append(os.path.join(qgis_prefix_path, "python3.exe"))
        for ver in _PY_VERSIONS:
            possible_paths.append(
                os.path.join(qgis_prefix_path, "apps", f"Python{ver}", _EXE)
            )
            possible_paths.append(os.path.join(qgis_prefix_path, f"Python{ver}", _EXE))
        # OSGeo4W installations
        possible_paths.append(os.path.join("C:\\OSGeo4W64", "bin", "python3.exe"))
        possible_paths.append(os.path.join("C:\\OSGeo4W", "bin", "python3.exe"))

    else:  # Linux or macOS
        possible_paths.append(os.path.join(qgis_prefix_path, "bin", "python3"))
        possible_paths.append(os.path.join(qgis_prefix_path, "python3"))
        possible_paths.append(
            os.path.join(qgis_prefix_path, "Contents", "MacOS", "python")
        )
        possible_paths.append(
            os.path.join(qgis_prefix_path, "Contents", "MacOS", "python3.12")
        )
        possible_paths.append(os.path.join(qgis_prefix_path, "bin", "python"))
        possible_paths.append(os.path.join(qgis_prefix_path, "python"))
        for ver in ("3.9", "3.10", "3.11", "3.12"):
            possible_paths.append(os.path.join(qgis_prefix_path, "bin", f"python{ver}"))
            possible_paths.append(os.path.join(qgis_prefix_path, f"python{ver}"))

    logger.debug(f"Found {len(possible_paths)} Possible QGIS Python paths.")
    logger.debug(f"Possible paths: '{possible_paths}'")

    for path in possible_paths:
        if os.path.exists(path):
            set_qgis_python_path(path)
            logger.debug(f"Found QGIS Python path: '{path}'")
            return path

    raise FileNotFoundError(
        "Could not find QGIS Python executable. "
        "Please report this at https://gitlab.com/aletrn/samgis-qgis-plugin/-/issues "
        f"with your QGIS version and OS. Searched prefix: {qgis_prefix_path}"
    )


def set_qgis_python_path(path: str) -> None:
    """Saves the QGIS Python path to QGIS settings."""
    settings = QgsSettings()
    settings.setValue("pip_manager/python_path", path)


def get_subprocess_flags() -> int:
    """Returns the appropriate subprocess flags based on the operating system."""
    if platform.system() == "Windows":
        return 0x08000000
    return 0


def pip_package_installer(
    package_name: str, target_dir: str | None = None
) -> tuple[bool, str]:
    """Install a package via pip.

    Args:
        package_name: Name of the pip package to install
        target_dir: Optional directory to install to (--target)

    Returns:
        tuple: (success: bool, message: str)
    """
    try:
        qgis_python_path = get_qgis_python_path()
        subprocess_flags = get_subprocess_flags()

        cmd = [qgis_python_path, "-m", "pip", "install", package_name]
        if target_dir is not None:
            os.makedirs(target_dir, exist_ok=True)
            cmd.extend(["--target", target_dir])

        process = subprocess.run(  # noqa: S603
            cmd, capture_output=True, text=True, creationflags=subprocess_flags
        )

        if process.returncode == 0:
            msg = f"Package '{package_name}' installed successfully."
            logger.info(msg)
            return True, msg
        else:
            msg = f"Failed to install '{package_name}': {process.stderr}"
            logger.error(msg)
            return False, msg
    except Exception as e:
        msg = f"Error installing '{package_name}': {e}"
        logger.error(msg)
        return False, msg


def try_import_module(module_name: str) -> types.ModuleType | None:
    """Dynamically import a module by name.

    Args:
        module_name: Name of the module to import

    Returns:
        Module if importable, None otherwise
    """
    try:
        return importlib.import_module(module_name)
    except ImportError:
        return None


def ensure_package(
    package_name: str,
    import_name: str | None = None,
    iface: QgisInterface | None = None,
    target_dir: str | None = None,
) -> types.ModuleType | None:
    """Ensure a package is installed, install via pip if missing.

    Args:
        package_name: Name of the pip package to install
        import_name: Name of the module to import (defaults to package_name)
        iface: QGIS interface for message bar errors (optional)
        target_dir: Optional directory to install to (--target)

    Returns:
        Module if successfully imported, None otherwise
    """
    import sys

    if import_name is None:
        import_name = package_name

    if target_dir is not None and target_dir not in sys.path:
        sys.path.insert(0, target_dir)

    module = try_import_module(import_name)
    if module:
        logger.debug(f"'{import_name}' is already installed.")
        return module

    success, msg = pip_package_installer(package_name, target_dir)
    if not success:
        if iface is not None:
            iface.messageBar().pushCritical("Package Install Error", msg)
        return None

    importlib.invalidate_caches()

    module = try_import_module(import_name)
    if module:
        logger.info(f"Successfully installed and imported '{import_name}'")
    else:
        error_msg = f"Installed '{package_name}' but failed to import '{import_name}'"
        logger.error(error_msg)
        if iface is not None:
            iface.messageBar().pushCritical("Import Error", error_msg)

    return module


class PipInstallWorker(QThread):
    """Non-blocking pip package installer running in a QThread."""

    finished = pyqtSignal(bool, str)  # (success, message)

    def __init__(
        self,
        package_name: str,
        target_dir: str | None = None,
        request_id: str | None = None,
    ) -> None:
        super().__init__()
        self._package_name = package_name
        self._target_dir = target_dir
        self._request_id = request_id

    def run(self) -> None:
        if self._request_id:
            from samgis_core.utilities.request_context import bind_request_id

            bind_request_id(self._request_id)
        success, msg = pip_package_installer(self._package_name, self._target_dir)
        if success:
            importlib.invalidate_caches()
        self.finished.emit(success, msg)


def get_raster_statistics(layer: QgsRasterLayer) -> dict[int, dict[str, float]]:
    """Calculate statistics for all bands of a raster layer.

    Args:
        layer: QgsRasterLayer to analyze

    Returns:
        dict: {band_num: {min, max, mean, stdDev}}
    """
    stats_dict = {}
    band_count = layer.bandCount()

    for band in range(1, band_count + 1):
        data_provider = layer.dataProvider()
        input_stats = Qgis.RasterBandStatistics()
        stats = data_provider.bandStatistics(band, input_stats)
        band_stats = {
            "min": stats.minimumValue,
            "max": stats.maximumValue,
            "mean": stats.mean,
            "stdDev": stats.stdDev,
        }
        stats_dict[band] = band_stats
        logger.debug(
            f"# Band {band}: min={stats.minimumValue}, max={stats.maximumValue}, "
            f"mean={stats.mean}, stdDev={stats.stdDev} #"
        )

    return stats_dict
