"""Edubuntu Menu Administration backend."""

from __future__ import annotations

import grp
import os
import pwd
import subprocess
from dataclasses import dataclass
from pathlib import Path

from i18n import _

APP_TITLE = _("Edubuntu Menu Administration")

ICON_PATH = "/usr/share/icons/hicolor/scalable/apps/edubuntu-menu-admin.svg"

HELPER = "/usr/sbin/edubuntu-menu-admin-helper"

_SYSTEM_APPS_DIR = Path("/usr/share/applications")
_SNAP_APPS_DIR = Path("/var/lib/snapd/desktop/applications")
_OVERRIDE_DIR = Path("/usr/share/edubuntu/applications")
_SKIP_FILES = frozenset({"defaults.list", "mimeinfo.cache", "kde-mimeapps.list"})


@dataclass
class DesktopEntry:
    filename: str
    display_name: str
    hidden: bool


def _adm_members() -> frozenset[str]:
    try:
        return frozenset(grp.getgrnam("adm").gr_mem)
    except KeyError:
        return frozenset()


_NOLOGIN_SHELLS = frozenset({
    "/usr/sbin/nologin",
    "/sbin/nologin",
    "/bin/false",
    "/bin/sync",
    "/usr/bin/false",
})


def list_non_admin_users() -> list[str]:
    adm = _adm_members()
    users: list[str] = []
    for pw in pwd.getpwall():
        if pw.pw_uid < 1000:
            continue
        if pw.pw_name in ("nobody", "nfsnobody"):
            continue
        if pw.pw_name in adm:
            continue
        if pw.pw_shell in _NOLOGIN_SHELLS:
            continue
        users.append(pw.pw_name)
    return sorted(users)


def _user_apps_dir(username: str) -> Path:
    pw = pwd.getpwnam(username)
    return Path(pw.pw_dir) / ".local" / "share" / "applications"


def _has_no_display(filepath: Path) -> bool:
    try:
        with open(filepath, encoding="utf-8", errors="replace") as fh:
            for line in fh:
                if line.strip().lower() == "nodisplay=true":
                    return True
    except OSError:
        pass
    return False


def _get_name(filepath: Path) -> str:
    try:
        with open(filepath, encoding="utf-8", errors="replace") as fh:
            for line in fh:
                if line.startswith("Name="):
                    return line.split("=", 1)[1].strip()
    except OSError:
        pass
    return ""


def _scan_directory(directory: Path) -> list[DesktopEntry]:
    entries: list[DesktopEntry] = []
    if not directory.is_dir():
        return entries

    for name in sorted(os.listdir(directory)):
        if name in _SKIP_FILES or not name.endswith(".desktop"):
            continue
        filepath = directory / name
        if not filepath.is_file():
            continue
        if _has_no_display(filepath):
            continue

        hidden = (_OVERRIDE_DIR / name).exists()
        display_name = _get_name(filepath)
        entries.append(DesktopEntry(
            filename=name,
            display_name=display_name,
            hidden=hidden,
        ))

    return entries


def build_desktop_table() -> list[DesktopEntry]:
    entries = _scan_directory(_SYSTEM_APPS_DIR)
    entries.extend(_scan_directory(_SNAP_APPS_DIR))
    return entries


def query_user_hidden(usernames: list[str]) -> dict[str, set[str]]:
    """Ask the privileged helper which desktop files have NoDisplay=true
    in each user's ~/.local/share/applications/.

    Returns a dict mapping each username to a set of hidden filenames.
    Requires pkexec (single password prompt for all users).
    """
    hidden: dict[str, set[str]] = {u: set() for u in usernames}
    if not usernames:
        return hidden
    try:
        result = subprocess.run(
            ["pkexec", HELPER, "query"] + usernames,
            capture_output=True, text=True,
        )
        if result.returncode == 0:
            for line in result.stdout.splitlines():
                parts = line.split("\t", 1)
                if len(parts) == 2 and parts[0] in hidden:
                    hidden[parts[0]].add(parts[1])
    except FileNotFoundError:
        pass
    return hidden


def build_user_desktop_table(
    username: str,
    hidden_files: set[str] | None = None,
) -> list[DesktopEntry]:
    """An entry is marked hidden if the user has a NoDisplay=true override
    in ~/.local/share/applications/.

    If *hidden_files* is provided (from :func:`query_user_hidden`) it is
    used directly; otherwise a direct filesystem check is attempted.
    """
    base_entries = build_desktop_table()
    result: list[DesktopEntry] = []

    if hidden_files is not None:
        for entry in base_entries:
            result.append(DesktopEntry(
                filename=entry.filename,
                display_name=entry.display_name,
                hidden=entry.filename in hidden_files,
            ))
        return result

    # Fallback: try direct access (works when running as the target user).
    user_dir = _user_apps_dir(username)
    for entry in base_entries:
        user_override = user_dir / entry.filename
        try:
            hidden = user_override.exists() and _has_no_display(user_override)
        except PermissionError:
            hidden = False
        result.append(DesktopEntry(
            filename=entry.filename,
            display_name=entry.display_name,
            hidden=hidden,
        ))
    return result


def apply_global_hidden(filenames: list[str]) -> bool:
    try:
        cmd = ["pkexec", HELPER, "global"] + filenames
        return subprocess.run(cmd).returncode == 0
    except FileNotFoundError:
        return False


def apply_user_hidden(username: str, filenames: list[str]) -> bool:
    try:
        cmd = ["pkexec", HELPER, "user", username] + filenames
        return subprocess.run(cmd).returncode == 0
    except FileNotFoundError:
        return False


def is_gnome_session() -> bool:
    desktop = os.environ.get("XDG_CURRENT_DESKTOP", "").lower()
    session = os.environ.get("DESKTOP_SESSION", "").lower()
    return "gnome" in desktop or session == "ubuntu"


def reset_gnome_apps() -> bool:
    try:
        subprocess.run(
            ["dconf", "reset", "-f", "/org/gnome/desktop/app-folders/"],
            check=True,
        )
        subprocess.run(
            ["dconf", "reset", "-f", "/org/gnome/shell/favorite-apps"],
            check=True,
        )
        return True
    except (subprocess.CalledProcessError, FileNotFoundError):
        return False
