"""
Audio route — switch sound between the built-in speaker and Bluetooth.

On dArkOS the actual routing is owned by the csr-fix audio daemon
(`csrfix-bt-audio-route`): a PulseAudio loop that, every second, reads a tiny
MODE FILE and moves the default sink accordingly —

    speaker → the built-in sink (builtin_out)
    bt      → the BT sink if present, else falls back to the speaker
    auto    → speaker first, else BT

So AAhead doesn't need the heavyweight `aactl` job server (which isn't even
deployed to the AAhead port) to route audio — it just WRITES that mode file and
the daemon does the rest. We also nudge `pactl` directly so the change is
instant instead of waiting up to a second. Same thin, tolerant pattern as
netops / powerops / volume: no daemon / no pulse → tolerant no-op.

(The mode file lives under /tmp, which the daemon creates 0777 and which clears
on reboot → the daemon's own default is speaker-first. This module just lets the
user force it from the Audio screen.)
"""

from __future__ import annotations

import os
import subprocess
from pathlib import Path
from typing import Optional

# csr-fix audio daemon's runtime contract (see csrfix-bt-audio-route).
_PULSE_DIR = Path("/tmp/csrfix-pulse")
_ROUTE_MODE_FILE = _PULSE_DIR / "route.mode"
_PULSE_SOCKET = _PULSE_DIR / "native"
_BUILTIN_SINK = "builtin_out"
# systemd units the csr-fix BT install lays down (pulse first, route daemon after).
_SERVICES = ("csrfix-pulseaudio", "csrfix-bt-audio-route")
_SUDO = [] if os.environ.get("AAHEAD_SUDO") == "0" else ["sudo", "-n"]

# UI target → daemon mode token.
_MODES = {"speaker": "speaker", "bluetooth": "bt", "bt": "bt", "auto": "auto"}


# ── pure ──────────────────────────────────────────────────────────────

def mode_for(target: str) -> Optional[str]:
    """UI route target → the daemon's mode token, or None if unknown."""
    return _MODES.get((target or "").lower())


def route_from_sink(default_sink: str) -> Optional[str]:
    """A PulseAudio default-sink name → 'bluetooth' / 'speaker' / None."""
    if not default_sink:
        return None
    return "bluetooth" if "bluez" in default_sink.lower() else "speaker"


# ── tolerant live layer (device; never raises) ───────────────────────

def _pulse_env() -> dict:
    return {**os.environ, "PULSE_SERVER": f"unix:{_PULSE_SOCKET}"}


def _pactl(*args: str, timeout: float = 3.0) -> None:
    """Best-effort pactl against the csr-fix pulse socket (ignore failure)."""
    try:
        subprocess.run(["pactl", *args], env=_pulse_env(),
                       capture_output=True, text=True, timeout=timeout)
    except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
        pass


def ensure_services() -> bool:
    """Best-effort: make sure the csr-fix audio daemon + pulse are running AND
    start at boot. `enable --now` both enables (so sound survives a reboot —
    the real 'speaker by default' fix) and starts now; it's idempotent on an
    already-healthy stack. ark has passwordless sudo on dArkOS; tolerant if it
    doesn't (returns False, caller still writes the route mode)."""
    try:
        r = subprocess.run([*_SUDO, "systemctl", "enable", "--now", *_SERVICES],
                           capture_output=True, text=True, timeout=20)
        return r.returncode == 0
    except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
        return False


def set_route(target: str) -> bool:
    """Force the audio route. Brings the audio daemon up if it's down, writes
    the daemon's mode file (the reliable lever — the daemon reconciles within
    ~1s) and nudges pactl now for instant feedback. True if the mode file was
    written, False otherwise."""
    mode = mode_for(target)
    if mode is None:
        return False
    ensure_services()                     # revive a stopped pulse/route daemon
    try:
        _PULSE_DIR.mkdir(parents=True, exist_ok=True)
        _ROUTE_MODE_FILE.write_text(mode + "\n")
    except OSError:
        return False
    # Instant nudge: speaker has a known sink name; for BT/auto the daemon
    # picks the right (dynamic) bluez sink itself within a second.
    if mode == "speaker":
        _pactl("set-default-sink", _BUILTIN_SINK)
        _pactl("set-sink-mute", _BUILTIN_SINK, "0")
    return True


def _tone_bytes(seconds: float = 1.5, rate: int = 8000) -> bytes:
    """A short, QUIET unsigned-8-bit mono tone (440→660 Hz). Same gentle shape
    the csr-fix backend uses — civilized on headphones."""
    import math
    total = int(seconds * rate)
    buf = bytearray()
    for i in range(total):
        hz = 440 if i < total // 2 else 660
        buf.append(max(0, min(255, int(128 + 22 * math.sin(2 * math.pi * hz * i / rate)))))
    return bytes(buf)


def test_tone() -> bool:
    """Play a short test tone through the CURRENT route — the built-in speaker
    when routed there. Works without aactl or Bluetooth: pipe raw PCM into
    `pacat` against the csr-fix pulse default sink. Brings the daemon up first
    and unmutes the sink (so a muted output can't look like a dead speaker).
    Blocking (~1.5s) — call off the render thread. True on success."""
    ensure_services()
    _pactl("set-sink-mute", "@DEFAULT_SINK@", "0")   # don't change volume, just unmute
    try:
        proc = subprocess.run(
            ["pacat", "--rate=8000", "--channels=1", "--format=u8"],
            input=_tone_bytes(), env=_pulse_env(),
            capture_output=True, timeout=8)
        return proc.returncode == 0
    except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
        return False


def current_route() -> Optional[str]:
    """The live route ('bluetooth' / 'speaker'), or None when pulse is absent."""
    try:
        r = subprocess.run(["pactl", "info"], env=_pulse_env(),
                           capture_output=True, text=True, timeout=2)
    except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
        return None
    if r.returncode != 0:
        return None
    for line in r.stdout.splitlines():
        if line.startswith("Default Sink:"):
            return route_from_sink(line.split(":", 1)[1].strip())
    return None
