"""Gather relevant system information."""

import functools
import logging
import os
import re
import shutil
import subprocess
import sys
from pathlib import Path
from platform import python_version

from PySide6 import QtCore, QtGui, QtWidgets
from PySide6 import __version__ as pyside_version

from normcap import __version__, app_id
from normcap.system.models import DesktopEnvironment, Screen

logger = logging.getLogger(__name__)


@functools.cache
def config_directory() -> Path:
    """Retrieve platform specific configuration directory."""
    postfix = "normcap"

    # Windows
    if sys.platform == "win32":
        if is_portable_windows_package():
            data_path = get_package_root() / "data"
            data_path.mkdir(exist_ok=True)
            return data_path
        if local_appdata := os.getenv("LOCALAPPDATA"):
            return Path(local_appdata) / postfix
        if appdata := os.getenv("APPDATA"):
            return Path(appdata) / postfix
        raise ValueError("Could not determine the appdata directory.")

    # Linux and Mac
    if xdg_config_home := os.getenv("XDG_CONFIG_HOME"):
        return Path(xdg_config_home) / postfix

    return Path.home() / ".config" / postfix


def desktop_dir() -> Path:
    """Return path the User's Desktop directory.

    Raises:
        NotImplementedError: When called on other systems than Windows.

    Returns:
        Path to desktop directory.
    """
    if sys.platform != "win32":
        raise NotImplementedError()

    try:
        import winreg

        reg_key = winreg.OpenKey(
            winreg.HKEY_CURRENT_USER,
            r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders",
        )
        desktop = Path(winreg.QueryValueEx(reg_key, "Desktop")[0]).resolve()
        winreg.CloseKey(reg_key)
    except Exception:
        desktop = Path().home() / "Desktop"

    if not desktop.is_dir() or not desktop.exists():
        raise RuntimeError(f"Detected desktop directory '{desktop}' does not exist!")

    return desktop


def get_resources_path() -> Path:
    return Path(__file__).resolve().parents[1] / "resources"


def get_package_root() -> Path:
    return Path(__file__).resolve().parents[3]


@functools.cache
def is_portable_windows_package() -> bool:
    if sys.platform != "win32":
        return False
    portable_file = get_package_root() / "portable"
    return portable_file.is_file()


@functools.cache
def is_briefcase_package() -> bool:
    app_path = Path(__file__).resolve().parents[2]
    return app_path.is_dir() and (app_path.parent / "app_packages").is_dir()


@functools.cache
def is_appimage_package() -> bool:
    return os.getenv("APPIMAGE") is not None


@functools.cache
def is_packaged() -> bool:
    return is_briefcase_package() or is_flatpak()


@functools.cache
def display_manager_is_wayland() -> bool:
    xdg_session_type = os.environ.get("XDG_SESSION_TYPE", "").lower()
    has_wayland_display_env = bool(os.environ.get("WAYLAND_DISPLAY", ""))
    return "wayland" in xdg_session_type or has_wayland_display_env


@functools.cache
def desktop_environment() -> DesktopEnvironment:  # noqa: PLR0911 # too many returns
    """Detect used desktop environment (Linux)."""
    kde_full_session = os.environ.get("KDE_FULL_SESSION", "").lower()
    xdg_current_desktop = os.environ.get("XDG_CURRENT_DESKTOP", "").lower()
    desktop_session = os.environ.get("DESKTOP_SESSION", "").lower()
    gnome_desktop_session_id = os.environ.get("GNOME_DESKTOP_SESSION_ID", "")
    hyprland_instance_signature = os.environ.get("HYPRLAND_INSTANCE_SIGNATURE", "")

    if gnome_desktop_session_id == "this-is-deprecated":
        gnome_desktop_session_id = ""

    if gnome_desktop_session_id or "gnome" in xdg_current_desktop:
        return DesktopEnvironment.GNOME
    if kde_full_session or "kde-plasma" in desktop_session:
        return DesktopEnvironment.KDE
    if "sway" in xdg_current_desktop or "sway" in desktop_session:
        return DesktopEnvironment.SWAY
    if "unity" in xdg_current_desktop:
        return DesktopEnvironment.UNITY
    if hyprland_instance_signature:
        return DesktopEnvironment.HYPRLAND
    if "awesome" in xdg_current_desktop:
        return DesktopEnvironment.AWESOME

    return DesktopEnvironment.OTHER


def screens() -> list[Screen]:
    """Get information about available monitors."""
    return [
        Screen(
            device_pixel_ratio=QtGui.QScreen.devicePixelRatio(screen),
            left=screen.geometry().left(),
            top=screen.geometry().top(),
            right=screen.geometry().right(),
            bottom=screen.geometry().bottom(),
            index=idx,
        )
        for idx, screen in enumerate(QtWidgets.QApplication.screens())
    ]


@functools.cache
def get_tesseract_bin_path(is_briefcase_package: bool) -> Path:
    """Get the path to the Tesseract binary.

    Returns:
        Path: The path to the Tesseract binary.

    Raises:
        ValueError: If the platform is not supported.
        RuntimeError: If the Tesseract binary cannot be located.
    """
    if is_briefcase_package:
        if sys.platform == "linux" or "bsd" in sys.platform:
            bin_path = Path(__file__).resolve().parents[3] / "bin"
        elif sys.platform == "win32":
            bin_path = Path(__file__).resolve().parents[1] / "resources" / "tesseract"
        elif sys.platform == "darwin":
            bin_path = Path(__file__).resolve().parents[3] / "app_packages" / "bin"
        else:
            raise ValueError(f"Platform {sys.platform} is not supported")
        extension = ".exe" if sys.platform == "win32" else ""
        tesseract_path = bin_path / f"tesseract{extension}"
        if not tesseract_path.exists():
            raise RuntimeError(f"Could not locate Tesseract binary {tesseract_path}!")
        return tesseract_path

    # Then try to find tesseract on system
    if tesseract_bin := shutil.which("tesseract"):
        tesseract_path = Path(tesseract_bin)
        if tesseract_path.exists():
            return tesseract_path

    raise RuntimeError(
        "No Tesseract binary found! Tesseract has to be installed and added "
        "to PATH environment variable."
    )


@functools.cache
def get_tessdata_path(config_directory: Path, is_packaged: bool) -> Path | None:
    """Decide which path for tesseract language files to use."""
    if is_packaged:
        tessdata_path = config_directory / "tessdata"
        return tessdata_path.resolve()

    if prefix := os.environ.get("TESSDATA_PREFIX", None):
        tessdata_path = Path(prefix) / "tessdata"
        if tessdata_path.is_dir() and list(tessdata_path.glob("*.traineddata")):
            return tessdata_path.resolve()

    if sys.platform == "win32":
        logger.warning("Missing tessdata directory. (Is TESSDATA_PREFIX variable set?)")

    return None


@functools.cache
def is_gnome() -> bool:
    if sys.platform != "linux" and "bsd" not in sys.platform:
        return False

    xdg_current_desktop = os.environ.get("XDG_CURRENT_DESKTOP", "").lower()
    gnome_desktop_session_id = os.environ.get("GNOME_DESKTOP_SESSION_ID", "")

    if gnome_desktop_session_id == "this-is-deprecated":
        gnome_desktop_session_id = ""

    return bool(gnome_desktop_session_id) or ("gnome" in xdg_current_desktop)


@functools.cache
def is_kde() -> bool:
    if sys.platform != "linux" and "bsd" not in sys.platform:
        return False

    desktop_session = os.environ.get("DESKTOP_SESSION", "").lower()
    kde_full_session = os.environ.get("KDE_FULL_SESSION", "").lower()
    return bool(kde_full_session) or ("kde-plasma" in desktop_session)


@functools.cache
def is_flatpak() -> bool:
    if sys.platform != "linux" and "bsd" not in sys.platform:
        return False

    return os.getenv("FLATPAK_ID") is not None


@functools.cache
def has_awesome_wm() -> bool:
    if sys.platform != "linux" and "bsd" not in sys.platform:
        return False

    return "awesome" in os.environ.get("XDG_CURRENT_DESKTOP", "").lower()


@functools.cache
def has_wlroots_compositor() -> bool:
    """Check if wlroots compositor is running, as grim only supports wlroots.

    Certainly not wlroots based are: KDE, GNOME and Unity.
    Others are likely wlroots based.
    """
    if not has_wayland_display_manager():
        return False

    kde_full_session = os.environ.get("KDE_FULL_SESSION", "").lower()
    xdg_current_desktop = os.environ.get("XDG_CURRENT_DESKTOP", "").lower()
    desktop_session = os.environ.get("DESKTOP_SESSION", "").lower()
    gnome_desktop_session_id = os.environ.get("GNOME_DESKTOP_SESSION_ID", "")
    if gnome_desktop_session_id == "this-is-deprecated":
        gnome_desktop_session_id = ""

    return not (
        gnome_desktop_session_id
        or "gnome" in xdg_current_desktop
        or kde_full_session
        or "kde-plasma" in desktop_session
        or "unity" in xdg_current_desktop
    )


@functools.cache
def has_wayland_display_manager() -> bool:
    """Identify relevant display managers (Linux)."""
    if sys.platform != "linux" and "bsd" not in sys.platform:
        return False

    xdg_session_type = os.environ.get("XDG_SESSION_TYPE", "").lower()
    has_wayland_display_env = bool(os.environ.get("WAYLAND_DISPLAY", ""))
    return "wayland" in xdg_session_type or has_wayland_display_env


def is_dbus_service_running() -> bool:
    # TODO: Improve dbus service check.
    # E.g. try to call the following async (to still be able to process the request)
    # dbus-send --session --print-reply --dest=com.github.dynobo.normcap_dev \
    #    /com/github/dynobo/normcap_dev org.freedesktop.DBus.Introspectable.Introspect
    app = QtGui.QGuiApplication.instance()
    if not app:
        return False

    service = getattr(app, "dbus_service", None)
    return getattr(service, "_registered", False)


@functools.cache
def get_gnome_version() -> str:
    """Detect Gnome version of current session.

    Returns:
        Version string or empty string if not detected.
    """
    if sys.platform != "linux" and "bsd" not in sys.platform:
        return ""

    if (
        not os.environ.get("GNOME_DESKTOP_SESSION_ID", "")
        and "gnome" not in os.environ.get("XDG_CURRENT_DESKTOP", "").lower()
    ):
        return ""

    if not shutil.which("gnome-shell"):
        return ""

    try:
        output = subprocess.check_output(
            ["gnome-shell", "--version"],  # noqa: S607
            shell=False,
            text=True,
        )
        if result := re.search(r"\s+([\d\.]+)", output.strip()):
            gnome_version = result.groups()[0]
    except Exception as e:
        logger.warning("Exception when trying to get gnome version from cli %s", e)
        return ""
    else:
        return gnome_version


def to_dict() -> dict:
    """Cast all system infos to string for logging."""
    return {
        "normcap_version": __version__,
        "python_version": python_version(),
        "app_id": app_id,
        "cli_args": " ".join(sys.argv),
        "is_briefcase_package": is_briefcase_package(),
        "is_flatpak_package": is_flatpak(),
        "is_appimage_package": is_appimage_package(),
        "platform": sys.platform,
        "desktop_environment": desktop_environment(),
        "display_manager_is_wayland": display_manager_is_wayland(),
        "pyside6_version": pyside_version,
        "qt_version": QtCore.qVersion(),
        "qt_library_path": ", ".join(QtCore.QCoreApplication.libraryPaths()),
        "config_directory": config_directory(),
        "resources_path": get_resources_path(),
        "tesseract_path": get_tesseract_bin_path(
            is_briefcase_package=is_briefcase_package()
        ),
        "tessdata_path": get_tessdata_path(
            config_directory=config_directory(),
            is_packaged=is_packaged(),
        ),
        "envs": {
            "TESSDATA_PREFIX": os.environ.get("TESSDATA_PREFIX", None),
            "LD_LIBRARY_PATH": os.environ.get("LD_LIBRARY_PATH", None),
            "APP_ID": os.environ.get("APP_ID"),
            "DESKTOP_STARTUP_ID": os.environ.get("DESKTOP_STARTUP_ID"),
        },
        "screens": screens(),
    }
