"""Small, dependency-free platform boundary for atcwatch.

The module deliberately contains no Qt imports.  Headless commands and unit tests can
therefore use it without PySide6 being installed.
"""

from __future__ import annotations

import logging
import os
from pathlib import Path
import subprocess
import sys
from typing import Literal
from urllib.parse import urlsplit


Platform = Literal["linux", "macos"]

_NOTIFY_TIMEOUT_SECONDS = 10
_OPEN_URL_WAIT_SECONDS = 2.0

#: Basename (without ".desktop") of the desktop entry installed under
#: ``$XDG_DATA_HOME/applications``.  Plasma only files a notification in the
#: bell/history — and only offers per-app settings for it — when the message
#: carries the ``desktop-entry`` hint and a matching .desktop file exists.
#: ``autostart/atcwatch.desktop`` and install.sh must keep this name; the
#: notification would otherwise silently fall back to "unassigned" again.
DESKTOP_ENTRY_NAME = "atcwatch"


def _logger() -> logging.Logger:
    """The shared atcwatch logger, fetched lazily to keep this module Qt- and
    import-cycle-free (``atcwatch.py`` configures the handlers)."""

    return logging.getLogger("atcwatch")


_APPLE_SCRIPT = (
    "on run {notificationTitle, notificationBody}",
    "display notification notificationBody with title notificationTitle",
    "end run",
)


def detect_platform(platform_name: str | None = None) -> Platform:
    """Return the supported platform name, or fail explicitly.

    ``platform_name`` is an injection point for tests.  Production callers leave it
    unset so this remains the sole place that reads ``sys.platform``.
    """

    candidate = sys.platform if platform_name is None else platform_name
    if candidate == "linux":
        return "linux"
    if candidate == "darwin":
        return "macos"
    raise RuntimeError(
        f"atcwatch supports Linux and macOS only (got {candidate!r})"
    )


def supports_sticky(platform_name: str | None = None) -> bool:
    """Can this platform keep a notification up until it is dismissed?

    Linux can: ``notify-send -t 0`` asks the notification server for a popup
    without an expire timeout.  macOS cannot — ``display notification`` via
    osascript has no persistence knob at all, so the sticky flag is silently
    dropped there (see ``notify``).  Offering the choice anyway would promise
    something the backend cannot deliver.
    """

    return detect_platform(platform_name) == "linux"


def _command(env_name: str, default: str) -> str:
    """Return one executable name, never a shell command line."""

    value = os.environ.get(env_name, default).strip()
    if not value:
        return default
    return value


def notify(
    title: str,
    body: str,
    sticky: bool = False,
    *,
    warning: bool = False,
    platform_name: str | None = None,
) -> bool:
    """Send a desktop notification and report whether the command succeeded.

    Two separate decisions, deliberately two parameters:

    *sticky* keeps the popup on screen until the user dismisses it, via an
    expire timeout of 0.  Notably NOT via urgency ``critical``: in Plasma that
    additionally overrides do-not-disturb and full screen, which is a different
    promise than "waits for me" — and the tool already has ``paused_until`` for
    silence.

    *warning* says the message is about a fault, and only it may pick the
    warning icon and the critical urgency.  Before, one flag drove both, so a
    sticky "airport is live" would have worn a warning triangle: good news
    dressed as a failure.

    User-controlled strings are always separate argv entries.  In particular, they
    are never interpolated into AppleScript source or evaluated by a shell.
    """

    platform = detect_platform(platform_name)
    if platform == "linux":
        icon = (
            "dialog-warning"
            if warning
            else str(
                Path(__file__).resolve().parent
                / "icons"
                / "png"
                / "linux"
                # The app icon, not the monochrome tray asset: a tray icon gets
                # its colour from the panel palette at load time
                # (tray.py::tint_pixmap), but notify-send only takes a path.
                # A pure black silhouette is invisible on a dark notification.
                / "atcwatch-app.png"
            )
        )
        argv = [
            _command("ATCWATCH_NOTIFY_CMD", "notify-send"),
            "-a",
            "ATCMatch",
            "-i",
            icon,
            "-u",
            "critical" if warning else "normal",
            # Without this hint Plasma cannot tell which application sent the
            # message: no bell, no entry in the notification history, and no
            # per-application settings the user could adjust.
            f"--hint=string:desktop-entry:{DESKTOP_ENTRY_NAME}",
        ]
        if sticky:
            argv.extend(("-t", "0"))
        argv.extend((title, body))
    else:
        argv = [_command("ATCWATCH_NOTIFY_CMD", "osascript")]
        for statement in _APPLE_SCRIPT:
            argv.extend(("-e", statement))
        argv.extend(("--", title, body))

    try:
        completed = subprocess.run(
            argv,
            check=False,
            shell=False,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.PIPE,
            timeout=_NOTIFY_TIMEOUT_SECONDS,
        )
    except (OSError, subprocess.TimeoutExpired, ValueError):
        return False
    return completed.returncode == 0


def open_url(url: str, *, platform_name: str | None = None) -> bool:
    """Open *url* with the platform default handler, without invoking a shell.

    Returns ``True`` when the handler did not fail within
    ``_OPEN_URL_WAIT_SECONDS``.  A handler that is still running by then counts
    as success: ``xdg-open`` may stay alive as long as the browser it started.
    """

    parsed = urlsplit(url)
    if parsed.scheme not in {"http", "https"} or not parsed.netloc:
        raise ValueError("open_url accepts only absolute HTTP(S) URLs")
    platform = detect_platform(platform_name)
    executable = _command(
        "ATCWATCH_OPEN_CMD", "xdg-open" if platform == "linux" else "open"
    )
    # No "--" terminator on Linux: xdg-open is a shell script that does not
    # implement the end-of-options convention and aborts with
    # "unexpected option '--'".  It is unnecessary anyway — the URL was just
    # validated as an absolute http(s) URL and can never look like an option.
    # macOS' open(1) does understand "--", so the terminator stays there as a
    # belt-and-braces guard.
    argv = [executable, url] if platform == "linux" else [executable, "--", url]
    try:
        process = subprocess.Popen(  # noqa: S603 - argv only; executable is intentionally configurable
            argv,
            shell=False,
            stdin=subprocess.DEVNULL,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.PIPE,
            start_new_session=True,
        )
    except (OSError, ValueError) as exc:
        _logger().warning("cannot start %s: %s", executable, exc)
        return False

    # Short, bounded wait so a GUI caller is never blocked noticeably, but an
    # immediate failure (unknown option, missing handler) still becomes visible
    # instead of vanishing into DEVNULL.
    try:
        _stdout, stderr = process.communicate(timeout=_OPEN_URL_WAIT_SECONDS)
    except subprocess.TimeoutExpired:
        return True  # still running: the handler took over, nothing to report
    if process.returncode != 0:
        message = (stderr or b"").decode("utf-8", "replace").strip()
        _logger().warning(
            "%s failed with exit code %s: %s",
            executable, process.returncode, message or "(no output)")
        return False
    return True


def icon_is_template(*, platform_name: str | None = None) -> bool:
    """Whether tray icons should be treated as monochrome template images."""

    return detect_platform(platform_name) == "macos"


def autostart_hint(*, platform_name: str | None = None) -> str:
    """Return the most useful platform-specific autostart diagnostic command."""

    if detect_platform(platform_name) == "linux":
        return "systemctl --user status atcwatch.service"
    return "launchctl print gui/$(id -u)/de.emvisio.atcwatch"
