"""
Robust Dependency Installer for QGIS Plugins
Correctly handles Python executable detection on Windows (OSGeo4W)
Supports Numba for accelerated hydrological algorithms

Author: MAS Tools
"""

import sys
import os
import subprocess # nosec B404

from qgis.core import Qgis, QgsMessageLog
from qgis.PyQt.QtWidgets import (
    QMessageBox, QProgressDialog, QApplication, QDialog,
    QVBoxLayout, QLabel, QPushButton, QTextEdit, QHBoxLayout
)
from qgis.PyQt.QtCore import Qt


class DependencyInstaller:
    """Handle installation of Python dependencies for QGIS plugins"""

    PLUGIN_NAME = "MAS Spatial Analysis Tool"

    # Minimum Numba version for Python 3.9+ compatibility
    # Numba 0.50 doesn't support LIST_EXTEND opcode, need >= 0.53.0
    MIN_NUMBA_VERSION = "0.53.0"

    def __init__(self, iface, required_packages: dict):
        """
        Initialize the installer.

        Args:
            iface: QGIS interface
            required_packages: Dict of {pip_package_name: import_module_name}
                e.g., {'numba': 'numba'} or {'numba>=0.53.0': 'numba'}
        """
        self.iface = iface
        self.required_packages = required_packages
        self._python_exe = None

    def get_python_executable(self) -> str:
        """
        Get the actual Python executable path.

        On Windows QGIS, sys.executable points to qgis.exe, not python.exe.
        This method finds the correct Python interpreter.
        """
        if self._python_exe:
            return self._python_exe

        if os.name == 'nt':  # Windows
            self._python_exe = self._find_windows_python()
        else:  # Linux/macOS
            self._python_exe = sys.executable

        self._log(f"Python executable: {self._python_exe}")
        return self._python_exe

    def _find_windows_python(self) -> str:
        """Find Python executable on Windows QGIS installation"""

        self._log(f"sys.executable = {sys.executable}")
        self._log(
            f"OSGEO4W_ROOT = {
                os.environ.get(
                    'OSGEO4W_ROOT',
                    'NOT SET')}")

        # Method 1: Check OSGEO4W_ROOT environment variable
        osgeo4w_root = os.environ.get('OSGEO4W_ROOT')
        if osgeo4w_root:
            self._log(f"Checking OSGEO4W_ROOT: {osgeo4w_root}")
            # Try different Python versions (newest first)
            for py_ver in ['Python312', 'Python311',
                           'Python310', 'Python39', 'Python38', 'Python37']:
                python_path = os.path.join(
                    osgeo4w_root, 'apps', py_ver, 'python.exe')
                self._log(
                    f"  Checking {python_path}: {
                        os.path.exists(python_path)}")
                if os.path.exists(python_path):
                    self._log(f"Found Python via OSGEO4W_ROOT: {python_path}")
                    return python_path

        # Method 2: Check PATH for QGIS Python
        self._log("Checking PATH for QGIS Python...")
        for path_dir in os.environ.get('PATH', '').split(os.pathsep):
            if 'QGIS' in path_dir.upper() or 'OSGEO4W' in path_dir.upper():
                python_path = os.path.join(path_dir, 'python.exe')
                if os.path.exists(python_path):
                    self._log(f"Found Python in PATH: {python_path}")
                    return python_path

        # Method 3: Try to find from QGIS installation directory
        qgis_dir = os.path.dirname(sys.executable)
        self._log(f"QGIS directory: {qgis_dir}")

        possible_paths = [
            os.path.join(qgis_dir, '..', 'apps', 'Python312', 'python.exe'),
            os.path.join(qgis_dir, '..', 'apps', 'Python311', 'python.exe'),
            os.path.join(qgis_dir, '..', 'apps', 'Python310', 'python.exe'),
            os.path.join(qgis_dir, '..', 'apps', 'Python39', 'python.exe'),
            os.path.join(qgis_dir, '..', 'apps', 'Python38', 'python.exe'),
            os.path.join(qgis_dir, 'python.exe'),
        ]

        for path in possible_paths:
            normalized = os.path.normpath(path)
            self._log(f"  Checking {normalized}: {os.path.exists(normalized)}")
            if os.path.exists(normalized):
                self._log(f"Found Python via QGIS dir: {normalized}")
                return normalized

        # Method 4: Use python-qgis.bat wrapper if available
        python_qgis_bat = os.path.join(qgis_dir, 'python-qgis.bat')
        if os.path.exists(python_qgis_bat):
            self._log(f"Found python-qgis.bat: {python_qgis_bat}")
            return python_qgis_bat

        # Method 5: Look for python-qgis-ltr.bat
        python_qgis_ltr_bat = os.path.join(qgis_dir, 'python-qgis-ltr.bat')
        if os.path.exists(python_qgis_ltr_bat):
            self._log(f"Found python-qgis-ltr.bat: {python_qgis_ltr_bat}")
            return python_qgis_ltr_bat

        # Fallback
        self._log(
            "WARNING: Could not find Python executable, falling back to sys.executable",
            Qgis.MessageLevel.Warning)
        return sys.executable

    def _log(self, message: str, level=Qgis.MessageLevel.Info):
        """Log message to QGIS message log"""
        QgsMessageLog.logMessage(message, self.PLUGIN_NAME, level)
        print(f"[{self.PLUGIN_NAME}] {message}")

    def check_dependencies(self) -> list:
        """
        Check which required packages are missing or need upgrade.

        Returns:
            List of missing/outdated package names (pip names with version spec)
        """
        missing = []

        for package_name, import_name in self.required_packages.items():
            try:
                module = __import__(import_name)

                # Special check for numba version
                if import_name == 'numba':
                    if hasattr(module, '__version__'):
                        installed_version = module.__version__
                        if self._version_lt(
                                installed_version, self.MIN_NUMBA_VERSION):
                            # Numba is installed but too old
                            missing.append(f'numba>={self.MIN_NUMBA_VERSION}')
                            self._log(
                                f"⚠ numba {installed_version} is outdated, need >= {
                                    self.MIN_NUMBA_VERSION}",
                                Qgis.MessageLevel.Warning
                            )
                            continue

                self._log(f"✓ {package_name} is installed")
            except ImportError:
                # For numba, always add version requirement
                if import_name == 'numba':
                    missing.append(f'numba>={self.MIN_NUMBA_VERSION}')
                else:
                    missing.append(package_name)
                self._log(f"✗ {package_name} is missing (optional)", Qgis.MessageLevel.Info)

        return missing

    def _version_lt(self, version1: str, version2: str) -> bool:
        """Check if version1 < version2 (simple version comparison)"""
        try:
            from packaging import version as pkg_version
            return pkg_version.parse(version1) < pkg_version.parse(version2)
        except ImportError:
            # Fallback: simple tuple comparison
            def parse_version(v):
                return tuple(int(x) for x in v.split('.')[:3] if x.isdigit())
            try:
                return parse_version(version1) < parse_version(version2)
            except BaseException:
                return False  # Can't compare, assume OK

    def check_and_install(self, silent_if_ok: bool = True) -> bool:
        """
        Check dependencies and prompt to install if missing.

        Args:
            silent_if_ok: If True, don't show message if all deps are installed

        Returns:
            True if all dependencies are available, False otherwise
        """
        missing = self.check_dependencies()

        if not missing:
            if not silent_if_ok:
                self._log("All dependencies are installed", Qgis.MessageLevel.Success)
            return True

        # Show installation dialog
        return self._show_install_dialog(missing)

    def _show_install_dialog(self, missing_packages: list) -> bool:
        """Show dialog to install missing packages"""

        dialog = DependencyInstallDialog(
            self.iface.mainWindow() if self.iface else None,
            missing_packages,
            self.PLUGIN_NAME
        )

        if dialog.exec() != QDialog.DialogCode.Accepted:
            return False

        # Perform installation
        return self._install_packages(missing_packages)

    def _install_packages(self, packages: list) -> bool:
        """Install packages using pip"""

        python_exe = self.get_python_executable()

        # Verify Python executable exists
        if not os.path.exists(python_exe):
            QMessageBox.critical(
                self.iface.mainWindow() if self.iface else None,
                "Installation Error",
                f"Python executable not found at:\n{python_exe}\n\n"
                "Please install dependencies manually using OSGeo4W Shell:\n"
                f"pip install {' '.join(packages)}"
            )
            return False

        # Create progress dialog
        progress = QProgressDialog(
            "Installing dependencies...",
            "Cancel",
            0, len(packages),
            self.iface.mainWindow() if self.iface else None
        )
        progress.setWindowTitle("Installing Dependencies")
        progress.setWindowModality(Qt.WindowModality.WindowModal)
        progress.setAutoClose(False)
        progress.show()

        success_count = 0
        failed_packages = []
        error_messages = []

        for i, package in enumerate(packages):
            if progress.wasCanceled():
                break

            progress.setValue(i)
            progress.setLabelText(
                f"Installing {package}...\n(This may take a few minutes)")
            QApplication.processEvents()

            success, error = self._install_single_package(python_exe, package)
            if success:
                success_count += 1
                self._log(f"✓ Successfully installed {package}", Qgis.MessageLevel.Success)
            else:
                failed_packages.append(package)
                error_messages.append(f"{package}: {error}")
                self._log(
                    f"✗ Failed to install {package}: {error}",
                    Qgis.MessageLevel.Critical)

        progress.setValue(len(packages))
        progress.close()

        # Show results
        if failed_packages:
            self._show_failure_dialog(failed_packages, error_messages)
            return False
        else:
            self._show_success_dialog(success_count)
            return True

    def _install_single_package(self, python_exe: str, package: str) -> tuple:
        """
        Install a single package.

        Returns:
            Tuple of (success: bool, error_message: str)
        """
        try:
            cmd = [
                python_exe,
                '-m', 'pip',
                'install',
                '--user',
                '--upgrade',
                package
            ]

            self._log(f"Running: {' '.join(cmd)}")

            # Use CREATE_NO_WINDOW on Windows to prevent console popup
            creationflags = 0
            if os.name == 'nt':
                creationflags = subprocess.CREATE_NO_WINDOW

            result = subprocess.run( # nosec B603
                cmd,
                capture_output=True,
                text=True,
                timeout=600,  # 10 minute timeout for numba (it's large)
                creationflags=creationflags
            )

            if result.returncode == 0:
                return True, ""
            else:
                return False, result.stderr or result.stdout or "Unknown error"

        except subprocess.TimeoutExpired:
            return False, "Installation timed out (10 minutes)"
        except FileNotFoundError:
            return False, f"Python executable not found: {python_exe}"
        except Exception as e:
            return False, str(e)

    def _show_success_dialog(self, count: int):
        """Show success message"""
        QMessageBox.information(
            self.iface.mainWindow() if self.iface else None,
            "Installation Complete",
            f"Successfully installed {count} package(s)!\n\n"
            "✓ Numba is now available for accelerated processing.\n\n"
            "You can continue using the plugin. The accelerated algorithms \n"
            "will be used automatically for improved performance."
        )

    def _show_failure_dialog(self, failed: list, errors: list):
        """Show detailed failure dialog"""
        dialog = QDialog(self.iface.mainWindow() if self.iface else None)
        dialog.setWindowTitle("Installation Failed")
        dialog.setMinimumWidth(500)

        layout = QVBoxLayout(dialog)

        # Header
        header = QLabel(f"Failed to install {len(failed)} package(s):")
        header.setStyleSheet("font-weight: bold; color: #c0392b;")
        layout.addWidget(header)

        # Error details
        error_text = QTextEdit()
        error_text.setReadOnly(True)
        error_text.setPlainText("\n\n".join(errors))
        error_text.setMaximumHeight(150)
        layout.addWidget(error_text)

        # Manual install instructions
        manual_label = QLabel(
            "<b>To install manually:</b><br>"
            "1. Open <b>OSGeo4W Shell</b> from Start Menu<br>"
            "2. Run the following command:"
        )
        layout.addWidget(manual_label)

        cmd_text = QTextEdit()
        cmd_text.setReadOnly(True)
        cmd_text.setPlainText(f"pip install --user {' '.join(failed)}")
        cmd_text.setMaximumHeight(40)
        cmd_text.setStyleSheet(
            "font-family: Consolas, monospace; background: #f5f5f5;")
        layout.addWidget(cmd_text)

        # OK button
        btn_layout = QHBoxLayout()
        btn_layout.addStretch()
        ok_btn = QPushButton("OK")
        ok_btn.clicked.connect(dialog.accept)
        btn_layout.addWidget(ok_btn)
        layout.addLayout(btn_layout)

        dialog.exec()


class DependencyInstallDialog(QDialog):
    """Dialog to confirm dependency installation or upgrade"""

    def __init__(self, parent, packages: list, plugin_name: str):
        super().__init__(parent)

        # Check if this is an upgrade scenario
        is_upgrade = any('>=' in pkg for pkg in packages)

        if is_upgrade:
            self.setWindowTitle(f"{plugin_name} - Upgrade Required")
        else:
            self.setWindowTitle(f"{plugin_name} - Optional Dependencies")
        self.setMinimumWidth(480)

        layout = QVBoxLayout(self)

        # Info icon and message
        if is_upgrade:
            header = QLabel(
                f"<h3>⚠️ Numba Upgrade Required</h3>"
                f"<p>Your installed <b>Numba</b> version is too old and doesn't support "
                f"Python 3.9+ properly.</p>"
                f"<p>This causes the <b>LIST_EXTEND opcode</b> error when running "
                f"flow algorithms.</p>"
            )
        else:
            header = QLabel(
                f"<h3>⚡ Optional Performance Boost Available</h3>"
                f"<p>The <b>{plugin_name}</b> plugin can use <b>Numba</b> for "
                f"significantly faster hydrological processing (10-100x speedup).</p>"
            )
        layout.addWidget(header)

        # Package list
        packages_html = "<ul>" + \
            "".join(
                f"<li><code>{pkg}</code></li>" for pkg in packages) + "</ul>"
        pkg_label = QLabel(packages_html)
        pkg_label.setStyleSheet("margin-left: 20px;")
        layout.addWidget(pkg_label)

        # Performance benefits
        benefits = QLabel(
            "<p><b>Benefits of Numba:</b></p>"
            "<ul>"
            "<li>Flow Direction: ~20x faster</li>"
            "<li>Flow Accumulation: ~50x faster</li>"
            "<li>Watershed Delineation: ~30x faster</li>"
            "</ul>"
        )
        layout.addWidget(benefits)

        # Note
        if is_upgrade:
            note = QLabel(
                "<p><small>This will upgrade Numba to a compatible version using pip.<br>"
                "A QGIS restart may be required after the upgrade.<br>"
                "Installation may take a few minutes.</small></p>"
            )
        else:
            note = QLabel(
                "<p><small>This will install packages to your user directory using pip.<br>"
                "The plugin will continue to work without Numba but will be slower.<br>"
                "Installation may take a few minutes.</small></p>"
            )
        note.setStyleSheet("color: #666;")
        layout.addWidget(note)

        # Buttons
        btn_layout = QHBoxLayout()
        btn_layout.addStretch()

        skip_btn = QPushButton("Skip (Use slower mode)")
        skip_btn.clicked.connect(self.reject)
        btn_layout.addWidget(skip_btn)

        if is_upgrade:
            install_btn = QPushButton("Upgrade Numba (Recommended)")
        else:
            install_btn = QPushButton("Install Numba (Recommended)")
        install_btn.setDefault(True)
        install_btn.setStyleSheet(
            "QPushButton { background-color: #27ae60; color: white; padding: 8px 16px; }"
            "QPushButton:hover { background-color: #229954; }"
        )
        install_btn.clicked.connect(self.accept)
        btn_layout.addWidget(install_btn)

        layout.addLayout(btn_layout)


def check_numba_available() -> bool:
    """Check if Numba is available and meets minimum version requirements."""
    try:
        import numba
        # Check minimum version for Python 3.9+ compatibility
        min_version = "0.53.0"
        if hasattr(numba, '__version__'):
            installed = numba.__version__
            # Simple version comparison

            def parse_version(v):
                return tuple(int(x) for x in v.split('.')[:3] if x.isdigit())
            try:
                if parse_version(installed) < parse_version(min_version):
                    return False  # Numba too old
            except BaseException:
                pass  # Can't parse, assume OK
        return True
    except ImportError:
        return False


def check_and_install_numba(iface) -> bool:
    """
    Convenience function to check and offer Numba installation/upgrade.

    Args:
        iface: QGIS interface

    Returns:
        True if Numba is available (or installed/upgraded), False if skipped
    """
    required_packages = {'numba': 'numba'}

    installer = DependencyInstaller(iface, required_packages)
    return installer.check_and_install(silent_if_ok=True)
