#!/usr/bin/env bash
# scripts/playlist.sh — playlist sync + control API for the music daemon.
#
# Usage:
#   ./playlist.sh                 → sync (default; index audio in ../playlist/)
#   ./playlist.sh sync            → explicit sync
#   ./playlist.sh skip            → next track
#   ./playlist.sh prev            → previous track
#   ./playlist.sh pause           → pause playback
#   ./playlist.sh resume          → resume playback
#   ./playlist.sh status          → show last queued command
#   ./playlist.sh help            → this help
#
# Control commands (skip/prev/pause/resume) write a tiny `cmd.js` that the
# Music Daemon polls every 250ms. The daemon picks up the new command,
# acts on it, and broadcasts the resulting state via OBS WebSocket so the
# loading overlay updates live.

set -euo pipefail

DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
OBS_DIR="$(cd "$DIR/.." && pwd)"
JSON="$OBS_DIR/loading/playlist.json"
JS="$OBS_DIR/loading/playlist.js"
CMD_JS="$OBS_DIR/loading/cmd.js"

# Audio roots — scanned recursively, in order. Add more here as the library grows.
# Missing roots are skipped with a warning, not an error.
ROOTS=(
  "$OBS_DIR/playlist"
  "$HOME/HDD/Music/Electronic/NCS Directory"
)

usage() {
  sed -n 's/^# \?//p' "$0" | head -20
}

# ─────────────── sync ───────────────
cmd_sync() {
  if ! command -v ffprobe >/dev/null 2>&1; then
    echo "  ✗ ffprobe not found (install ffmpeg: sudo pacman -S ffmpeg)" >&2
    exit 1
  fi

  echo "── Indexing audio roots ──"
  for r in "${ROOTS[@]}"; do
    if [[ -d "$r" ]]; then echo "    + $r"; else echo "    ! $r (missing — skipped)"; fi
  done

  local TMP="$JSON.tmp.$$"
  python3 - "${ROOTS[@]}" "$TMP" <<'PY'
import json, subprocess, sys, os, datetime, re, urllib.parse

roots = sys.argv[1:-1]
out   = sys.argv[-1]
exts  = ('.m4a', '.mp3', '.opus', '.ogg', '.flac', '.webm', '.aac', '.wav')

def ffprobe_meta(path):
    try:
        r = subprocess.run(
            ['ffprobe', '-v', 'error',
             '-show_entries', 'format=duration:format_tags=title,artist',
             '-of', 'json', path],
            capture_output=True, text=True, timeout=10,
        )
        if r.returncode != 0: return {}
        fmt  = (json.loads(r.stdout) or {}).get('format', {}) or {}
        tags = {k.lower(): v for k, v in (fmt.get('tags') or {}).items()}
        return {
            'title':       tags.get('title'),
            'artist':      tags.get('artist'),
            'durationSec': float(fmt['duration']) if fmt.get('duration') else None,
        }
    except Exception:
        return {}

def clean_title(t):
    if not t: return t
    # PRIMARY rule: keep only what's before the first vertical bar
    # (fullwidth ｜ U+FF5C or ASCII |). Everything after is genre/label noise.
    t = re.sub(r'\s*[｜|].*$', '', t)
    # Strip trailing YouTube-ID brackets, e.g. " [-XxZTgMWKV0]"
    t = re.sub(r'\s*\[[A-Za-z0-9_-]{11}\]\s*$', '', t)
    # Strip "[NCS Release]" / "(NCS10 Release)" suffix variants
    t = re.sub(r'\s*[\[(](?:NCS\d*|No Copyright Sounds)(?:\s+Release)?[\])]\s*$', '', t, flags=re.I)
    return t.strip()

INTERMEDIATE = re.compile(r'\.(?:f\d+|temp|part)$', re.I)
def is_audio_file(name):
    base, ext = os.path.splitext(name)
    if ext.lower() not in exts: return False
    if INTERMEDIATE.search(base): return False
    return True

# Absolute file:// URI — works across roots since the daemon HTML and the audio
# may live on different filesystems. CEF in OBS browser source loads these
# directly thanks to the Flatpak's filesystems=host permission.
def file_uri(path):
    return 'file://' + urllib.parse.quote(os.path.abspath(path), safe='/')

tracks = []
for root in roots:
    if not os.path.isdir(root):
        print(f'  ! skipping missing root: {root}', file=sys.stderr)
        continue
    for dirpath, _, files in os.walk(root):
        for fname in sorted(files):
            if not is_audio_file(fname): continue
            path = os.path.join(dirpath, fname)
            meta = ffprobe_meta(path)
            display = clean_title(os.path.splitext(fname)[0])
            tracks.append({
                'title':       display,
                'file':        file_uri(path),
                'durationSec': int(meta['durationSec']) if meta.get('durationSec') else None,
            })

doc = {
    'syncedAt':   datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ'),
    'sourceDirs': roots,
    'trackCount': len(tracks),
    'tracks':     tracks,
}
with open(out, 'w') as f:
    json.dump(doc, f, indent=2, ensure_ascii=False)
PY

  if ! python3 -m json.tool "$TMP" >/dev/null 2>&1; then
    echo "  ✗ manifest invalid, leaving $TMP" >&2
    exit 1
  fi
  mv -f "$TMP" "$JSON"
  { printf 'window.__PLAYLIST = '; cat "$JSON"; printf ';\n'; } > "$JS.tmp.$$"
  mv -f "$JS.tmp.$$" "$JS"

  local count
  count=$(python3 -c "import json; print(json.load(open('$JSON'))['trackCount'])")
  echo
  echo "  ✓ wrote $JSON ($count tracks)"
  echo "  ✓ wrote $JS"
  echo "  → refresh the Music Daemon browser source in OBS to reload the queue."
}

# ─────────────── control commands ───────────────
# Writes a unique-per-call cmd.js that the daemon polls. The id is
# nanosecond-precise so the daemon's "did this id change?" check always passes
# even on rapid-fire commands.
send_cmd() {
  local type="$1"
  local id ts
  id=$(date +%s%N)
  ts=$(date +%s)
  printf 'window.__CMD = {"id":%s,"type":"%s","ts":%s};\n' "$id" "$type" "$ts" > "$CMD_JS"
  echo "  → ${type}  (id=$id)"
}

cmd_skip()   { send_cmd skip;   }
cmd_prev()   { send_cmd prev;   }
cmd_pause()  { send_cmd pause;  }
cmd_resume() { send_cmd resume; }

cmd_status() {
  if [[ -f "$CMD_JS" ]]; then
    echo "── last queued command ──"
    cat "$CMD_JS"
  else
    echo "  (no command file yet — daemon hasn't been signalled this session)"
  fi
}

# ─────────────── dispatch ───────────────
case "${1:-sync}" in
  sync)        cmd_sync   ;;
  skip|next)   cmd_skip   ;;
  prev|back)   cmd_prev   ;;
  pause)       cmd_pause  ;;
  resume|play) cmd_resume ;;
  status)      cmd_status ;;
  help|-h|--help) usage   ;;
  *)
    echo "  ✗ unknown command: $1" >&2
    usage
    exit 1
    ;;
esac
