#!/usr/bin/env python3
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Copyright (c) 2026 CyberMind — Gerald Kerma <devel@cybermind.fr>
#
# secubox-fmrelay-icy — RDS-to-ICY metadata pusher.
#
# Runs alongside secubox-fmrelay-runner. Spawns ITS OWN rtl_fm (at the
# 171 kHz sample rate redsea needs for the RDS subcarrier) and pipes
# it through redsea -o json. Each PS/RT update is POSTed to icecast2's
# /admin/metadata endpoint so HTTP listeners see ICY StreamTitle =
# "Artist - Title" (or "PS — RT" when RT carries song info, which is
# the common case for French national radio).
#
# Two rtl_fm processes share the single RTL-SDR — that's a hard cost.
# A v0.2 plan splits a single rtl_fm stream via `sox tee` so audio +
# RDS subcarrier come from one tuner pass; v0.1.0 accepts the dual
# cost for simplicity.

from __future__ import annotations

import argparse
import json
import os
import shlex
import signal
import subprocess
import sys
import time
import urllib.parse
import urllib.request


STATE_DIR = "/var/lib/secubox/fmrelay"
NOW_PLAYING = os.path.join(STATE_DIR, "now-playing.json")
CONFIG_FILE = os.environ.get("SECUBOX_FMRELAY_CONFIG", "/etc/secubox/fmrelay.toml")


def toml_get(key: str, default: str) -> str:
    """Same TOML scalar reader as fmrelayctl / runner."""
    try:
        with open(CONFIG_FILE) as f:
            for line in f:
                s = line.strip()
                if not s or s.startswith("#") or "=" not in s:
                    continue
                k, _, v = s.partition("=")
                if k.strip() == key:
                    v = v.split("#", 1)[0].strip().strip('"').strip("'")
                    return v or default
    except FileNotFoundError:
        pass
    return default


def push_icy(host: str, port: int, admin_pass: str, mount: str, song: str) -> bool:
    """POST to icecast2 /admin/metadata?mount=/<mount>.mp3&mode=updinfo&song=…"""
    qs = urllib.parse.urlencode({
        "mount": f"/{mount}.mp3",
        "mode":  "updinfo",
        "song":  song,
    })
    url = f"http://{host}:{port}/admin/metadata?{qs}"
    req = urllib.request.Request(url, method="GET")
    # Basic auth — icecast admin endpoint
    import base64
    auth = base64.b64encode(f"admin:{admin_pass}".encode()).decode()
    req.add_header("Authorization", f"Basic {auth}")
    try:
        with urllib.request.urlopen(req, timeout=3) as r:
            return 200 <= r.status < 300
    except (urllib.error.URLError, OSError):
        return False


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("freq", help="FM frequency, e.g. 103.9M")
    ap.add_argument("mount", help="Icecast mount name (without .mp3)")
    ap.add_argument("ppm", nargs="?", default="0")
    args = ap.parse_args()

    host = toml_get("icecast_host", "127.0.0.1")
    port = int(toml_get("icecast_port", "8000"))
    admin_pass = toml_get("icecast_admin_pass", "hackme")
    redsea_bin = os.environ.get(
        "SECUBOX_REDSEA_BIN", "/usr/libexec/secubox/secubox-redsea"
    )
    rtl_fm_bin = os.environ.get("SECUBOX_RTL_FM_BIN", "/usr/bin/rtl_fm")

    os.makedirs(STATE_DIR, exist_ok=True)

    # rtl_fm @ 171 kHz mono for RDS subcarrier, pipe to redsea -o json.
    rtl_argv = [
        rtl_fm_bin, "-f", args.freq, "-M", "fm",
        "-l", "0", "-A", "std", "-s", "171k", "-F", "9", "-E", "deemp",
        "-p", str(int(args.ppm)),
        "-",
    ]
    redsea_argv = [redsea_bin, "-r", "171000"]

    print(f"[icy] rtl_fm: {' '.join(shlex.quote(a) for a in rtl_argv)}", flush=True)
    print(f"[icy] redsea: {' '.join(shlex.quote(a) for a in redsea_argv)}", flush=True)
    print(f"[icy] icecast admin: http://{host}:{port}/admin/metadata?mount=/{args.mount}.mp3",
          flush=True)

    rtl = subprocess.Popen(rtl_argv, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
    redsea = subprocess.Popen(redsea_argv, stdin=rtl.stdout, stdout=subprocess.PIPE,
                              stderr=subprocess.DEVNULL)
    if rtl.stdout:
        rtl.stdout.close()

    last_song = None
    state = {"pi": None, "ps": None, "rt": None}

    def shutdown(*_):
        print("[icy] SIGTERM — cleaning up", flush=True)
        for p in (redsea, rtl):
            try:
                p.terminate()
            except ProcessLookupError:
                pass
        sys.exit(0)

    signal.signal(signal.SIGTERM, shutdown)
    signal.signal(signal.SIGINT, shutdown)

    try:
        assert redsea.stdout is not None
        for raw in redsea.stdout:
            try:
                f = json.loads(raw.decode(errors="replace"))
            except json.JSONDecodeError:
                continue
            # PI is the station id; PS the 8-char name; RT the live title.
            if isinstance(f.get("pi"), str):
                state["pi"] = f["pi"]
            if isinstance(f.get("ps"), str):
                state["ps"] = f["ps"].strip()
            if isinstance(f.get("rt"), str):
                state["rt"] = f["rt"].strip()

            song = state["rt"] or state["ps"] or state["pi"] or ""
            if song and song != last_song:
                ok = push_icy(host, port, admin_pass, args.mount, song)
                ts = time.time()
                with open(NOW_PLAYING, "w") as fp:
                    json.dump({
                        "ts": ts,
                        "pi": state["pi"], "ps": state["ps"], "rt": state["rt"],
                        "song": song, "icy_pushed": ok,
                    }, fp)
                print(f"[icy] {'OK' if ok else 'FAIL'} push: {song!r}", flush=True)
                last_song = song
    finally:
        shutdown()


if __name__ == "__main__":
    main()
