"""Shared device inventory filters and app classification (0.2.2.23)."""

from __future__ import annotations

import re
import shutil
from pathlib import Path
from typing import Any, Callable

_LIBREOFFICE_TEMPLATE_NAME_RE = re.compile(
    r"^new\s+(document|drawing|formula|presentation|spreadsheet)\b",
    re.IGNORECASE,
)
_JUNK_INVENTORY_ACTION_PREFIXES = (
    "quit ",
    "open profile manager",
    "log out",
    "logout ",
)
_JUNK_INVENTORY_STANDALONE_NAMES = frozenset(
    {
        "math",
        "new document",
        "new drawing",
        "new formula",
        "new presentation",
        "new spreadsheet",
    }
)
_AI_PROVIDER_APP_PATTERNS = (
    "claude",
    "anthropic",
    "grok",
    "x.ai",
    "chatgpt",
    "openai",
    "whatsapp web",
    "gemini",
    "copilot",
    "perplexity",
    "deepseek",
    "midjourney",
)
_OFFICE_APP_KEYWORDS = (
    "libreoffice",
    "onlyoffice",
    "openoffice",
    "wps office",
    "abiword",
    "gnumeric",
)
_MESSENGER_APP_KEYWORDS = (
    "telegram",
    "viber",
    "slack",
    "discord",
    "signal",
    "whatsapp",
    "zoom",
    "teams",
    "skype",
)
_EDITOR_CATEGORY_TOKENS = frozenset(
    {
        "development",
        "ide",
        "texteditor",
        "editor",
    }
)
_BROWSER_CATEGORY_TOKENS = frozenset({"webbrowser"})
_KNOWN_BROWSER_DESKTOP_STEMS = frozenset(
    {
        "firefox",
        "firefox-esr",
        "org.mozilla.firefox",
        "firefox_firefox",
        "google-chrome",
        "google-chrome-stable",
        "com.google.chrome",
        "chromium",
        "chromium-browser",
        "brave-browser",
        "microsoft-edge",
        "microsoft-edge-dev",
        "opera",
        "vivaldi",
        "vivaldi-stable",
    }
)
_KNOWN_EDITOR_DESKTOP_STEMS = frozenset(
    {
        "cursor",
        "code",
        "code-oss",
        "codium",
        "vscodium",
        "visual-studio-code",
        "com.visualstudio.code",
    }
)
_LIBREOFFICE_MAIN_COMPONENT_STEMS = frozenset(
    {
        "libreoffice-writer",
        "libreoffice-calc",
        "libreoffice-impress",
        "libreoffice-draw",
        "libreoffice-base",
        "libreoffice-startcenter",
        "libreoffice",
    }
)
_BROWSER_APP_KEYWORDS = (
    "firefox",
    "mozilla-firefox",
    "google-chrome",
    "google chrome",
    "chromium",
    "chromium-browser",
    "brave",
    "brave-browser",
    "opera",
    "vivaldi",
    "microsoft-edge",
    "microsoft edge",
    "msedge",
    "waterfox",
    "librewolf",
)
_CANONICAL_DESKTOP_STEM_MAP: dict[str, str] = {
    "firefox_firefox": "firefox",
    "org.mozilla.firefox": "firefox",
    "firefox-esr": "firefox",
    "google-chrome-stable": "google-chrome",
    "com.google.chrome": "google-chrome",
    "chromium-browser": "chromium",
    "brave-browser": "brave",
    "microsoft-edge-dev": "microsoft-edge",
    "visual-studio-code": "code",
    "com.visualstudio.code": "code",
    "code-oss": "code",
    "vscodium": "code",
    "codium": "code",
}
_PRIORITY_KNOWN_APPS: tuple[dict[str, Any], ...] = (
    {
        "canonical": "firefox",
        "display_name": "Firefox",
        "binaries": ("firefox", "firefox-esr"),
        "desktop_patterns": ("firefox.desktop", "firefox-esr.desktop", "firefox_*.desktop"),
        "categories": "Network;WebBrowser;",
    },
    {
        "canonical": "google-chrome",
        "display_name": "Google Chrome",
        "binaries": ("google-chrome-stable", "google-chrome", "chrome"),
        "desktop_patterns": ("google-chrome.desktop", "google-chrome-stable.desktop", "chrome-*.desktop"),
        "categories": "Network;WebBrowser;",
    },
    {
        "canonical": "cursor",
        "display_name": "Cursor",
        "binaries": ("cursor",),
        "desktop_patterns": ("cursor.desktop", "*cursor*.desktop"),
        "categories": "Development;IDE;",
    },
    {
        "canonical": "code",
        "display_name": "Visual Studio Code",
        "binaries": ("code", "code-oss", "codium"),
        "desktop_patterns": ("code.desktop", "code-oss.desktop", "codium.desktop"),
        "categories": "Development;TextEditor;",
    },
)


def canonical_desktop_stem(app: dict[str, Any]) -> str:
    """Map a desktop_id stem to a canonical app id (firefox, google-chrome, cursor, code)."""
    for token in _desktop_stem_tokens(app):
        stem = token.removesuffix(".desktop")
        if stem in _CANONICAL_DESKTOP_STEM_MAP:
            return _CANONICAL_DESKTOP_STEM_MAP[stem]
        if stem in {spec["canonical"] for spec in _PRIORITY_KNOWN_APPS}:
            return stem
    return ""


def _desktop_stem_tokens(app: dict[str, Any]) -> set[str]:
    desktop_file = str(app.get("desktop_file") or "").strip()
    desktop_id = str(app.get("desktop_id") or Path(desktop_file).name if desktop_file else "").lower()
    stem = Path(desktop_file).stem.lower() if desktop_file else ""
    tokens = {item for item in (desktop_id, stem) if item}
    for item in (desktop_id, stem):
        if item.endswith(".desktop"):
            tokens.add(item[: -len(".desktop")])
    return tokens


def _desktop_app_match_blob(app: dict[str, Any]) -> str:
    desktop_file = str(app.get("desktop_file") or "").strip()
    desktop_path = Path(desktop_file) if desktop_file else None
    desktop_id = str(app.get("desktop_id") or (desktop_path.name if desktop_path else "")).lower()
    desktop_stem = desktop_path.stem.lower() if desktop_path else ""
    canonical = canonical_desktop_stem(app)
    return (
        f"{app.get('name', '')} {app.get('exec', '')} {desktop_id} {desktop_stem} {canonical}"
    ).lower()


def _app_category_tokens(app: dict[str, Any]) -> set[str]:
    return {item.strip().lower() for item in str(app.get("categories") or "").split(";") if item.strip()}


def _is_ai_provider_app(app: dict[str, Any]) -> bool:
    blob = f"{app.get('name', '')} {app.get('exec', '')} {app.get('desktop_file', '')}".lower()
    return any(pattern in blob for pattern in _AI_PROVIDER_APP_PATTERNS)


def _is_whatsapp_web_app(app: dict[str, Any]) -> bool:
    blob = f"{app.get('name', '')} {app.get('exec', '')} {app.get('desktop_file', '')}".lower()
    return "whatsapp web" in blob or ("whatsapp" in blob and "--app=" in blob and "web.whatsapp" in blob)


def _is_libreoffice_template_app(app: dict[str, Any]) -> bool:
    name = str(app.get("name") or "").strip().lower()
    if _LIBREOFFICE_TEMPLATE_NAME_RE.match(name):
        return True
    if name in _JUNK_INVENTORY_STANDALONE_NAMES and "libreoffice" in _desktop_app_match_blob(app):
        return True
    desktop_stem = Path(str(app.get("desktop_file") or "")).stem.lower()
    if desktop_stem.startswith("libreoffice") and any(
        token in desktop_stem for token in ("template", "startcenter", "math")
    ):
        return True
    if name == "math" and "libreoffice" in _desktop_app_match_blob(app):
        return True
    return False


def _is_action_inventory_app(app: dict[str, Any]) -> bool:
    name = str(app.get("name") or "").strip().lower()
    if not name:
        return True
    if any(name.startswith(prefix) for prefix in _JUNK_INVENTORY_ACTION_PREFIXES):
        return True
    if name.startswith("open ") and "profile" in name:
        return True
    return False


def is_junk_inventory_app(app: dict[str, Any], *, browsers_question: bool = False) -> bool:
    if _is_ai_provider_app(app):
        return True
    if not browsers_question and _is_whatsapp_web_app(app):
        return True
    if _is_libreoffice_template_app(app):
        return True
    if _is_action_inventory_app(app):
        return True
    name = str(app.get("name") or "").strip().lower()
    exec_line = str(app.get("exec") or "").strip().lower()
    if name in _JUNK_INVENTORY_STANDALONE_NAMES and "libreoffice" in f"{name} {exec_line}":
        return True
    return False


def _is_libreoffice_main_app(app: dict[str, Any]) -> bool:
    if _is_libreoffice_template_app(app):
        return False
    stems = _desktop_stem_tokens(app)
    if stems & _LIBREOFFICE_MAIN_COMPONENT_STEMS:
        return True
    name = str(app.get("name") or "").strip().lower()
    return any(token in name for token in ("libreoffice writer", "libreoffice calc", "libreoffice impress"))


def matches_browser_app(app: dict[str, Any]) -> bool:
    if is_junk_inventory_app(app, browsers_question=True):
        return False
    if _is_ai_provider_app(app):
        return False
    canonical = canonical_desktop_stem(app)
    if canonical in {"firefox", "google-chrome", "chromium", "brave", "microsoft-edge"}:
        return True
    categories = _app_category_tokens(app)
    if categories & _BROWSER_CATEGORY_TOKENS:
        return True
    stems = _desktop_stem_tokens(app)
    if stems & _KNOWN_BROWSER_DESKTOP_STEMS:
        return True
    name = str(app.get("name") or "").strip().lower()
    if any(name == keyword or name.startswith(f"{keyword} ") for keyword in _BROWSER_APP_KEYWORDS):
        return True
    blob = _desktop_app_match_blob(app)
    return any(keyword in blob for keyword in _BROWSER_APP_KEYWORDS)


def matches_editor_app(app: dict[str, Any]) -> bool:
    if is_junk_inventory_app(app):
        return False
    if _is_ai_provider_app(app):
        return False
    canonical = canonical_desktop_stem(app)
    if canonical in {"cursor", "code"}:
        return True
    categories = _app_category_tokens(app)
    if categories & _EDITOR_CATEGORY_TOKENS:
        if _is_libreoffice_template_app(app):
            return False
        return True
    stems = _desktop_stem_tokens(app)
    if stems & _KNOWN_EDITOR_DESKTOP_STEMS:
        return True
    if _is_libreoffice_main_app(app):
        return True
    blob = _desktop_app_match_blob(app)
    if "visual studio code" in blob or "vscode" in blob:
        return True
    if "cursor" in blob:
        return True
    if any(token in blob for token in ("gedit", "kate", "sublime", "atom", "neovim", "emacs")):
        return True
    if re.search(r"\bvim\b", blob) or blob.endswith(" vi") or blob.startswith("vi "):
        return True
    if re.search(r"(?:^|[\s/])code(?:$|[\s/])", blob):
        return True
    return False


def matches_office_app(app: dict[str, Any]) -> bool:
    if is_junk_inventory_app(app):
        return False
    blob = _desktop_app_match_blob(app)
    return any(keyword in blob for keyword in _OFFICE_APP_KEYWORDS)


def matches_messenger_app(app: dict[str, Any]) -> bool:
    if is_junk_inventory_app(app) or _is_action_inventory_app(app):
        return False
    blob = _desktop_app_match_blob(app)
    if "quit" in str(app.get("name") or "").lower():
        return False
    return any(keyword in blob for keyword in _MESSENGER_APP_KEYWORDS)


def _normalize_inventory_display_name(name: str) -> str:
    cleaned = re.sub(r"\([^)]*\)", "", str(name or ""))
    return re.sub(r"[^a-z0-9а-яё]+", " ", str(cleaned or "").lower()).strip()


def _inventory_app_key(app: dict[str, Any]) -> str:
    desktop_file = str(app.get("desktop_file") or "").strip().lower()
    if desktop_file:
        return desktop_file
    name = str(app.get("name") or "").strip().lower()
    exec_line = str(app.get("exec") or "").strip().lower()
    return f"{name}|{exec_line}"


def dedupe_inventory_apps_by_name(apps: list[dict[str, Any]]) -> list[dict[str, Any]]:
    deduped: list[dict[str, Any]] = []
    seen_names: set[str] = set()
    seen_keys: set[str] = set()
    for app in apps:
        if not isinstance(app, dict):
            continue
        key = _inventory_app_key(app)
        normalized = _normalize_inventory_display_name(str(app.get("name") or ""))
        if not normalized or normalized in seen_names:
            continue
        if key and key in seen_keys:
            continue
        seen_names.add(normalized)
        if key:
            seen_keys.add(key)
        deduped.append(app)
    return deduped


def filter_meaningful_inventory_apps(
    apps: list[dict[str, Any]],
    *,
    browsers_question: bool = False,
    priority_score_fn: Callable[[dict[str, str]], int] | None = None,
) -> list[dict[str, Any]]:
    meaningful: list[dict[str, Any]] = []
    for app in apps:
        if not isinstance(app, dict):
            continue
        if is_junk_inventory_app(app, browsers_question=browsers_question):
            continue
        meaningful.append(app)
    meaningful = dedupe_inventory_apps_by_name(meaningful)

    def _sort_key(item: dict[str, Any]) -> tuple[int, str]:
        priority = 0
        if matches_browser_app(item):
            priority += 500
        if matches_editor_app(item):
            priority += 400
        if matches_office_app(item):
            priority += 300
        if matches_messenger_app(item):
            priority += 200
        if priority_score_fn is not None:
            priority += priority_score_fn(item)
        return (-priority, str(item.get("name") or "").lower())

    meaningful.sort(key=_sort_key)
    return meaningful


def dedupe_inventory_apps(apps: list[dict[str, Any]]) -> list[dict[str, Any]]:
    deduped: list[dict[str, Any]] = []
    seen: set[str] = set()
    for app in apps:
        if not isinstance(app, dict):
            continue
        key = _inventory_app_key(app)
        if not key or key in seen:
            continue
        seen.add(key)
        deduped.append(app)
    deduped.sort(key=lambda item: str(item.get("name") or "").lower())
    return deduped


def browsers_from_apps(apps: list[dict[str, Any]]) -> list[dict[str, Any]]:
    browsers: list[dict[str, Any]] = []
    for app in apps:
        if isinstance(app, dict) and matches_browser_app(app):
            browsers.append(app)
    return dedupe_inventory_apps(browsers)


def editors_from_apps(apps: list[dict[str, Any]]) -> list[dict[str, Any]]:
    editors: list[dict[str, Any]] = []
    for app in apps:
        if isinstance(app, dict) and matches_editor_app(app):
            editors.append(app)
    return dedupe_inventory_apps(editors)


def _priority_app_present(apps: list[dict[str, str]], canonical: str) -> bool:
    for app in apps:
        if canonical_desktop_stem(app) == canonical:
            return True
        blob = _desktop_app_match_blob(app)
        if canonical == "firefox" and "firefox" in blob:
            return True
        if canonical == "google-chrome" and ("google-chrome" in blob or "chrome" in blob):
            return True
        if canonical == "cursor" and "cursor" in blob:
            return True
        if canonical == "code" and ("visual studio code" in blob or re.search(r"\bcode\b", blob)):
            return True
    return False


def _find_desktop_for_priority(
    spec: dict[str, Any],
    search_dirs: list[Path],
    parse_desktop_file: Callable[[Path], dict[str, str] | None],
) -> dict[str, str] | None:
    patterns = tuple(spec.get("desktop_patterns") or ())
    for directory in search_dirs:
        if not directory.is_dir():
            continue
        for pattern in patterns:
            try:
                matches = sorted(directory.glob(pattern))
            except OSError:
                continue
            for path in matches:
                if not path.is_file():
                    continue
                parsed = parse_desktop_file(path)
                if parsed is not None:
                    return parsed
    return None


def inject_priority_known_apps(
    apps: list[dict[str, str]],
    search_dirs: list[Path],
    parse_desktop_file: Callable[[Path], dict[str, str] | None],
) -> list[dict[str, str]]:
    """Ensure known apps appear in inventory even when .desktop metadata is sparse."""
    merged = list(apps)
    seen_keys = {
        str(app.get("desktop_id") or Path(str(app.get("desktop_file") or "")).name).lower()
        for app in merged
        if isinstance(app, dict)
    }
    for spec in _PRIORITY_KNOWN_APPS:
        canonical = str(spec["canonical"])
        if _priority_app_present(merged, canonical):
            continue
        discovered = _find_desktop_for_priority(spec, search_dirs, parse_desktop_file)
        if discovered is not None:
            desktop_id = str(discovered.get("desktop_id") or "").lower()
            if desktop_id and desktop_id not in seen_keys:
                merged.append(discovered)
                seen_keys.add(desktop_id)
            continue
        binary = next((item for item in spec.get("binaries", ()) if shutil.which(str(item))), "")
        if not binary:
            continue
        synthetic_id = f"{canonical}.desktop"
        if synthetic_id in seen_keys:
            continue
        merged.append(
            {
                "name": str(spec.get("display_name") or canonical),
                "exec": binary,
                "desktop_file": "",
                "desktop_id": synthetic_id,
                "categories": str(spec.get("categories") or ""),
                "canonical_id": canonical,
                "synthetic": "true",
            }
        )
        seen_keys.add(synthetic_id)
    return merged
