#!/usr/bin/env python3

"""Read and update freedesktop defaults for Panama's settings page."""

from __future__ import annotations

import ast
import json
import os
from pathlib import Path
import re
import subprocess
import sys
import tempfile


ROLE_TARGETS = {
    "browser": ("settings", "default-web-browser"),
    "mail": ("mime", "x-scheme-handler/mailto"),
    "files": ("mime", "inode/directory"),
    "terminal": ("mime", "x-scheme-handler/terminal"),
    "music": ("mime", "audio/mpeg"),
    "images": ("mime", "image/png"),
    "video": ("mime", "video/mp4"),
}
DESKTOP_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._+-]*\.desktop$")
EXEC_CMD = re.compile(r"hl\.exec_cmd\(\s*(\"(?:\\.|[^\"\\])*\")\s*\)")


class BoundaryError(RuntimeError):
    """A user-visible validation or command failure."""


def xdg_data_roots() -> list[Path]:
    data_home = Path(os.environ.get("XDG_DATA_HOME", Path.home() / ".local/share"))
    data_dirs = os.environ.get("XDG_DATA_DIRS", "/usr/local/share:/usr/share")
    return [data_home, *(Path(item) for item in data_dirs.split(":") if item)]


def discovered_desktop_ids() -> set[str]:
    desktop_ids: set[str] = set()
    for root in xdg_data_roots():
        applications = root / "applications"
        if not applications.is_dir():
            continue
        for path in applications.rglob("*.desktop"):
            if not path.is_file():
                continue
            relative = path.relative_to(applications)
            desktop_ids.add("-".join(relative.parts))
    return desktop_ids


def require_desktop_id(desktop_id: str, *, discovered: set[str]) -> None:
    if not DESKTOP_ID.fullmatch(desktop_id) or desktop_id not in discovered:
        raise BoundaryError("That application is not available.")


def run(command: list[str]) -> str:
    completed = subprocess.run(command, check=False, capture_output=True, text=True)
    if completed.returncode != 0:
        detail = completed.stderr.strip()
        raise BoundaryError(detail or "The system default could not be updated.")
    return completed.stdout.strip()


def query_handlers() -> dict[str, str]:
    handlers: dict[str, str] = {}
    for role, (kind, target) in ROLE_TARGETS.items():
        command = (
            ["xdg-settings", "get", target]
            if kind == "settings"
            else ["xdg-mime", "query", "default", target]
        )
        output = run(command)
        handlers[role] = output.splitlines()[0] if output else ""
    return handlers


def parse_desktop_entry(path: Path) -> dict[str, str]:
    values: dict[str, str] = {}
    section = ""
    try:
        lines = path.read_text(encoding="utf-8").splitlines()
    except (OSError, UnicodeError) as error:
        raise BoundaryError(f"Could not read {path.name}.") from error
    for line in lines:
        stripped = line.strip()
        if stripped.startswith("[") and stripped.endswith("]"):
            section = stripped[1:-1]
            continue
        if section != "Desktop Entry" or "=" not in line or stripped.startswith("#"):
            continue
        key, value = line.split("=", 1)
        values.setdefault(key.strip(), value.strip())
    return values


def autostart_directory() -> Path:
    config_home = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config"))
    return config_home / "autostart"


def user_autostart_entries() -> list[dict[str, object]]:
    directory = autostart_directory()
    if not directory.is_dir():
        return []
    entries: list[dict[str, object]] = []
    for path in directory.glob("*.desktop"):
        if path.is_symlink() or not path.is_file():
            continue
        values = parse_desktop_entry(path)
        entries.append(
            {
                "id": path.name,
                "name": values.get("Name", path.stem),
                "enabled": values.get("Hidden", "false").lower() != "true",
            }
        )
    return sorted(entries, key=lambda entry: (str(entry["name"]).casefold(), str(entry["id"])))


def hypr_autostart_path() -> Path:
    override = os.environ.get("PANAMA_HYPR_AUTOSTART")
    if override:
        return Path(override)
    return Path(__file__).resolve().parents[2] / "hypr" / "autostart.lua"


def lua_autostart_entries() -> list[dict[str, object]]:
    path = hypr_autostart_path()
    try:
        lines = path.read_text(encoding="utf-8").splitlines()
    except (OSError, UnicodeError):
        return []

    commands: list[str] = []
    in_start_handler = False
    for line in lines:
        if not in_start_handler:
            in_start_handler = bool(re.search(r'hl\.on\(\s*"hyprland\.start"', line))
            continue
        if line.strip() == "end)":
            break
        match = EXEC_CMD.search(line)
        if match:
            try:
                commands.append(ast.literal_eval(match.group(1)))
            except (SyntaxError, ValueError):
                continue

    return [
        {
            "id": f"hyprland:{index}",
            "name": command.split()[0].rsplit("/", 1)[-1],
            "command": command,
            "enabled": True,
            "readOnly": True,
            "source": "config/dot/hypr/autostart.lua",
        }
        for index, command in enumerate(commands, start=1)
    ]


def snapshot() -> dict[str, object]:
    return {
        "handlers": query_handlers(),
        "autostartEntries": user_autostart_entries(),
        "luaAutostartEntries": lua_autostart_entries(),
    }


def set_default(role: str, desktop_id: str) -> None:
    target = ROLE_TARGETS.get(role)
    if target is None:
        raise BoundaryError("That default application role is not supported.")
    require_desktop_id(desktop_id, discovered=discovered_desktop_ids())
    kind, setting = target
    command = (
        ["xdg-settings", "set", setting, desktop_id]
        if kind == "settings"
        else ["xdg-mime", "default", desktop_id, setting]
    )
    run(command)


def update_hidden(path: Path, *, hidden: bool) -> None:
    try:
        original = path.read_text(encoding="utf-8")
    except (OSError, UnicodeError) as error:
        raise BoundaryError("That autostart entry could not be read.") from error

    lines = original.splitlines()
    output: list[str] = []
    section = ""
    found_section = False
    wrote_hidden = False
    for line in lines:
        stripped = line.strip()
        if stripped.startswith("[") and stripped.endswith("]"):
            if section == "Desktop Entry" and not wrote_hidden:
                output.append(f"Hidden={'true' if hidden else 'false'}")
                wrote_hidden = True
            section = stripped[1:-1]
            found_section = found_section or section == "Desktop Entry"
            output.append(line)
            continue
        if section == "Desktop Entry" and line.split("=", 1)[0].strip() == "Hidden":
            if not wrote_hidden:
                output.append(f"Hidden={'true' if hidden else 'false'}")
                wrote_hidden = True
            continue
        output.append(line)

    if not found_section:
        raise BoundaryError("That autostart entry is not a desktop file.")
    if not wrote_hidden:
        output.append(f"Hidden={'true' if hidden else 'false'}")

    mode = path.stat().st_mode
    try:
        with tempfile.NamedTemporaryFile(
            "w", encoding="utf-8", dir=path.parent, prefix=f".{path.name}.", delete=False
        ) as temporary:
            temporary.write("\n".join(output) + "\n")
            temporary.flush()
            os.fsync(temporary.fileno())
            temporary_path = Path(temporary.name)
        temporary_path.chmod(mode)
        os.replace(temporary_path, path)
    except OSError as error:
        if "temporary_path" in locals():
            temporary_path.unlink(missing_ok=True)
        raise BoundaryError("That autostart entry could not be updated.") from error


def set_autostart(desktop_id: str, enabled_text: str) -> None:
    if enabled_text not in {"true", "false"}:
        raise BoundaryError("Autostart state must be true or false.")
    if not DESKTOP_ID.fullmatch(desktop_id):
        raise BoundaryError("That autostart entry is not available.")

    directory = autostart_directory()
    path = directory / desktop_id
    try:
        resolved_directory = directory.resolve(strict=True)
        resolved_path = path.resolve(strict=True)
    except OSError as error:
        raise BoundaryError("That autostart entry is not available.") from error
    if path.is_symlink() or resolved_path.parent != resolved_directory or not resolved_path.is_file():
        raise BoundaryError("That autostart entry is not available.")
    update_hidden(resolved_path, hidden=enabled_text == "false")


def main(arguments: list[str]) -> int:
    try:
        if arguments == ["snapshot"]:
            print(json.dumps(snapshot(), separators=(",", ":")))
        elif len(arguments) == 3 and arguments[0] == "set-default":
            set_default(arguments[1], arguments[2])
        elif len(arguments) == 3 and arguments[0] == "set-autostart":
            set_autostart(arguments[1], arguments[2])
        else:
            raise BoundaryError(
                "Usage: panama-default-apps snapshot | set-default ROLE DESKTOP_ID | "
                "set-autostart DESKTOP_ID true|false"
            )
    except BoundaryError as error:
        print(str(error), file=sys.stderr)
        return 2
    return 0


if __name__ == "__main__":
    raise SystemExit(main(sys.argv[1:]))
