#!/usr/bin/env bash
# ==============================================================================
# Motion Wallpaper — gum-powered TUI for mpvpaper on Omarchy / Hyprland.
#
# Actions:
#   toggle   (default)  Interactive TUI. If running, offers Stop / Change.
#   start               Non-interactive start from saved state (for systemd).
#   stop                Stop mpvpaper and restore the normal wallpaper.
#   change              Pick a new video while already running.
#   status              Print current state (TUI header via gum).
#
# Files:
#   ~/.config/motion-wallpaper/state        last video + target monitor
#   ~/Videos/Wallpapers/                    optional quick-pick library
#   ~/.cache/motion-wallpaper.log           runtime log
# ==============================================================================

set -euo pipefail

APP_NAME="Motion Wallpaper"
STATE_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/motion-wallpaper"
STATE_FILE="$STATE_DIR/state"
LOG_DIR="${XDG_CACHE_HOME:-$HOME/.cache}"
LOG_FILE="$LOG_DIR/motion-wallpaper.log"
LIBRARY_DIR="$HOME/Videos/Wallpapers"

# mpv options forwarded via mpvpaper -o. An input-ipc-server socket is opened
# so our companion watcher can send pause/resume commands when Hyprland
# reports a fullscreen window (mpvpaper's own -p is unreliable on 0.54.x).
RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
MPV_IPC_SOCK="$RUNTIME_DIR/motion-wallpaper-mpv.sock"
MPV_OPTS="--loop --no-audio --mute=yes --vo=gpu --profile=high-quality --input-ipc-server=$MPV_IPC_SOCK"

# Catppuccin Mocha-ish accents
COLOR_ACCENT="#cba6f7"
COLOR_ERROR="#f38ba8"
COLOR_OK="#a6e3a1"
COLOR_MUTED="#6c7086"

BROWSE_SENTINEL="── Browse filesystem… ──"

mkdir -p "$STATE_DIR" "$LOG_DIR"

# ===== helpers ================================================================

log() {
  printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >> "$LOG_FILE"
}

require_gum() {
  if ! command -v gum >/dev/null 2>&1; then
    echo "ERROR: gum is not installed. Run: sudo pacman -S gum" >&2
    exit 1
  fi
}

require_tty() {
  if [ ! -t 0 ] || [ ! -t 1 ]; then
    cat >&2 <<MSG
$APP_NAME: interactive mode requires a terminal.
Run from a terminal, or use a non-interactive action:
  $0 start | stop | status
MSG
    exit 1
  fi
}

tui_err() {
  gum style --foreground="$COLOR_ERROR" --bold "ERROR: $1"
  log "ERROR: $1"
  # Hold the terminal open so launcher-spawned windows don't flash away.
  if [ -t 0 ]; then
    gum input --placeholder="Press enter to close…" >/dev/null 2>&1 || true
  fi
}

tui_ok() {
  gum style --foreground="$COLOR_OK" "✓ $1"
  log "$1"
}

notify() {
  # Fire-and-forget system notification so the user sees results after the
  # TUI terminal closes.
  if command -v notify-send >/dev/null 2>&1; then
    notify-send "$APP_NAME" "$1" || true
  fi
  log "$1"
}

load_state() {
  LAST_VIDEO=""
  LAST_TARGET=""
  LAST_DIR=""
  [ -f "$STATE_FILE" ] || return 0
  # Parse KEY="VALUE" lines directly instead of `source`, so a maliciously
  # crafted video path (e.g. containing `";rm -rf …`) can't execute.
  local key val
  while IFS='=' read -r key val; do
    val="${val#\"}"
    val="${val%\"}"
    case "$key" in
      LAST_VIDEO)  LAST_VIDEO="$val"  ;;
      LAST_TARGET) LAST_TARGET="$val" ;;
      LAST_DIR)    LAST_DIR="$val"    ;;
    esac
  done < "$STATE_FILE"
}

save_state() {
  # $1 video, $2 target, $3 (optional) last dir — defaults to dirname of $1
  # so the filesystem browser re-opens where the user last landed.
  local dir="${3:-$(dirname "$1")}"
  umask 077
  # Atomic write: avoids leaving a truncated file if the process dies mid-cat.
  local tmp="$STATE_FILE.tmp"
  cat > "$tmp" <<STATE
LAST_VIDEO="$1"
LAST_TARGET="$2"
LAST_DIR="$dir"
STATE
  mv -f "$tmp" "$STATE_FILE"
}

is_running() {
  pgrep -x mpvpaper >/dev/null 2>&1
}

# ===== autostart (systemd user unit) ==========================================

autostart_installed() {
  systemctl --user list-unit-files motion-wallpaper.service >/dev/null 2>&1
}

autostart_enabled() {
  systemctl --user is-enabled motion-wallpaper.service >/dev/null 2>&1
}

autostart_enable() {
  if ! autostart_installed; then
    tui_err "motion-wallpaper.service is not installed. Re-run wallpaper.sh."
    return 1
  fi
  if ! systemctl --user enable motion-wallpaper.service >/dev/null 2>&1; then
    tui_err "Failed to enable autostart (systemctl error)."
    return 1
  fi
  log "autostart enabled"
  return 0
}

autostart_disable() {
  systemctl --user disable motion-wallpaper.service >/dev/null 2>&1 || true
  log "autostart disabled"
}

show_header() {
  load_state
  local status_line target_line video_line autostart_line
  if is_running; then
    status_line="status:    $(gum style --foreground="$COLOR_OK" running)"
  else
    status_line="status:    $(gum style --foreground="$COLOR_MUTED" stopped)"
  fi
  target_line="target:    ${LAST_TARGET:-$(gum style --foreground="$COLOR_MUTED" --italic '(none)')}"
  video_line="video:     ${LAST_VIDEO:-$(gum style --foreground="$COLOR_MUTED" --italic '(none)')}"
  if autostart_enabled; then
    autostart_line="autostart: $(gum style --foreground="$COLOR_OK" enabled)"
  else
    autostart_line="autostart: $(gum style --foreground="$COLOR_MUTED" disabled)"
  fi
  gum style --border=rounded --border-foreground="$COLOR_ACCENT" \
            --padding="1 2" --margin="1 0" \
            "$(gum style --bold --foreground="$COLOR_ACCENT" "◐ $APP_NAME")" \
            "" \
            "$status_line" \
            "$target_line" \
            "$video_line" \
            "$autostart_line"
}

# ===== selection ==============================================================

ensure_hyprland_env() {
  # The TUI can be launched from contexts whose HYPRLAND_INSTANCE_SIGNATURE
  # isn't usable. Three failure modes, all silent:
  #   1) HIS unset (fresh login shell, cron, ssh).
  #   2) HIS set but points to a dead session — the `hypr/<sig>/` directory
  #      and its socket files can linger on disk after the Hyprland process
  #      exits, so a dir-exists check is not enough.
  #   3) Walker's env retained an old HIS across a Hyprland restart.
  # `hyprctl instances` is the source of truth: it only reports live
  # instances. Always cross-check against it and rewrite HIS if ours isn't
  # in the list.
  local his="${HYPRLAND_INSTANCE_SIGNATURE:-}"
  local live
  live="$(hyprctl instances 2>/dev/null | awk '/^instance /{sub(/:$/,"",$2); print $2}')"
  if [ -z "$live" ]; then
    # No live Hyprland — nothing we can do, let the caller surface an error.
    return 0
  fi
  if [ -n "$his" ] && printf '%s\n' "$live" | grep -qxF "$his"; then
    return 0
  fi
  local sig
  sig="$(printf '%s\n' "$live" | head -n 1)"
  export HYPRLAND_INSTANCE_SIGNATURE="$sig"
  if [ -n "$his" ]; then
    log "replaced stale HYPRLAND_INSTANCE_SIGNATURE ($his → $sig)"
  else
    log "recovered HYPRLAND_INSTANCE_SIGNATURE=$sig from hyprctl instances"
  fi
}

get_monitors() {
  # hyprctl's failure modes are noisy: when HYPRLAND_INSTANCE_SIGNATURE isn't
  # set (can happen when the TUI is spawned from a launcher whose env doesn't
  # inherit the Hyprland session), hyprctl prints a plain-text error to stdout
  # and exits 0 — which then makes jq complain loudly. Validate the JSON and
  # fall back to parsing the human-readable form.
  ensure_hyprland_env
  local mon_json mon_text
  mon_json="$(hyprctl monitors -j 2>/dev/null || true)"
  if [ -n "$mon_json" ] && printf '%s' "$mon_json" | jq -e . >/dev/null 2>&1; then
    printf '%s' "$mon_json" | jq -r '.[].name'
    return 0
  fi
  mon_text="$(hyprctl monitors 2>/dev/null || true)"
  if [ -n "$mon_text" ]; then
    printf '%s\n' "$mon_text" \
      | awk '/^Monitor /{print $2; ok=1} END{exit !ok}' \
      && return 0
  fi
  log "get_monitors: hyprctl produced no usable output (HIS=${HYPRLAND_INSTANCE_SIGNATURE:-unset})"
  return 1
}

pick_target() {
  command -v hyprctl >/dev/null || { tui_err "hyprctl not found. Are you in Hyprland?"; return 1; }
  command -v jq      >/dev/null || { tui_err "jq is not installed.";                     return 1; }

  local monitors
  if ! monitors="$(get_monitors)" || [ -z "$monitors" ]; then
    if [ -z "${HYPRLAND_INSTANCE_SIGNATURE:-}" ]; then
      tui_err "HYPRLAND_INSTANCE_SIGNATURE is not set — the launcher didn't inherit the Hyprland session. Try running motion-wallpaper-toggle from a regular terminal."
    else
      tui_err "Could not read monitors from hyprctl. See $LOG_FILE."
    fi
    return 1
  fi

  local count
  count="$(printf '%s\n' "$monitors" | wc -l)"
  if [ "$count" -eq 1 ]; then
    printf '%s' "$monitors"
    return 0
  fi

  local selected
  selected=$( { echo "All monitors"; printf '%s\n' "$monitors"; } \
    | gum choose --header="Select a monitor") || return 1

  if [ "$selected" = "All monitors" ]; then
    printf '%s' '*'
  else
    printf '%s' "$selected"
  fi
}

browse_filesystem() {
  # Start gum file at the last directory we successfully picked from, so
  # changing videos returns the user to where they were. Falls back to the
  # last video's dirname (for state files pre-dating LAST_DIR), then $HOME.
  load_state
  local start_dir="${LAST_DIR:-}"
  if [ -z "$start_dir" ] && [ -n "${LAST_VIDEO:-}" ]; then
    start_dir="$(dirname "$LAST_VIDEO")"
  fi
  if [ -z "$start_dir" ] || [ ! -d "$start_dir" ]; then
    start_dir="$HOME"
  fi
  gum file --height=20 "$start_dir"
}

pick_video() {
  local library=() basenames=() v
  if [ -d "$LIBRARY_DIR" ]; then
    while IFS= read -r -d '' f; do
      library+=("$f")
    done < <(
      find "$LIBRARY_DIR" -maxdepth 1 -type f \
        \( -iname '*.mp4' -o -iname '*.mkv' -o -iname '*.webm' -o -iname '*.mov' -o -iname '*.avi' \) \
        -print0 2>/dev/null | sort -z
    )
  fi

  if [ ${#library[@]} -eq 0 ]; then
    browse_filesystem || return 1
    return 0
  fi

  for v in "${library[@]}"; do
    basenames+=("$(basename "$v")")
  done

  local choice
  choice=$( { printf '%s\n' "${basenames[@]}"; echo "$BROWSE_SENTINEL"; } \
    | gum choose --header="Choose a video from $LIBRARY_DIR") || return 1

  if [ "$choice" = "$BROWSE_SENTINEL" ]; then
    browse_filesystem
  else
    printf '%s' "$LIBRARY_DIR/$choice"
  fi
}

# ===== mpvpaper control =======================================================

kill_static_wallpapers() {
  pkill -x hyprpaper 2>/dev/null || true
  pkill -x swaybg    2>/dev/null || true
}

kill_watcher() {
  pkill -f motion-wallpaper-watcher       2>/dev/null || true
  pkill -f motion-wallpaper-theme-watcher 2>/dev/null || true
}

# Locate a sibling script — first in PATH, then in this script's dir. Used
# for both watchers because launcher-spawned PATH is minimal.
locate_sibling() {
  local name="$1" path
  path="$(command -v "$name" 2>/dev/null || true)"
  if [ -z "$path" ]; then
    local self_dir
    self_dir="$(dirname "$(readlink -f "$0")")"
    if [ -x "$self_dir/$name" ]; then
      path="$self_dir/$name"
    fi
  fi
  printf '%s' "$path"
}

start_watcher() {
  local watcher theme_watcher
  watcher="$(locate_sibling motion-wallpaper-watcher)"
  theme_watcher="$(locate_sibling motion-wallpaper-theme-watcher)"

  kill_watcher

  if [ -n "$watcher" ]; then
    setsid "$watcher" < /dev/null >> "$LOG_FILE" 2>&1 &
    disown 2>/dev/null || true
    log "auto-pause watcher spawned via $watcher"
  else
    log "auto-pause watcher binary not found — auto-pause disabled"
  fi

  if [ -n "$theme_watcher" ]; then
    setsid "$theme_watcher" < /dev/null >> "$LOG_FILE" 2>&1 &
    disown 2>/dev/null || true
    log "theme watcher spawned via $theme_watcher"
  else
    log "theme watcher binary not found — theme-change auto-stop disabled"
  fi
}

restore_static_wallpaper() {
  local omarchy_bg="$HOME/.config/omarchy/current/background"
  if [ -e "$omarchy_bg" ]; then
    # Stop may be called twice in quick succession when the TUI stops the
    # wallpaper and systemd's ExecStop fires right after. Skip if someone
    # else already put swaybg back so we don't kill-and-respawn (causes a
    # visible flicker).
    if pgrep -x swaybg >/dev/null 2>&1 && ! pgrep -x mpvpaper >/dev/null 2>&1; then
      log "swaybg already running, skipping restore"
      return 0
    fi
    pkill -x hyprpaper 2>/dev/null || true
    pkill -x swaybg    2>/dev/null || true
    if command -v uwsm-app >/dev/null 2>&1; then
      setsid uwsm-app -- swaybg -i "$omarchy_bg" -m fill >/dev/null 2>&1 &
    else
      setsid swaybg -i "$omarchy_bg" -m fill >/dev/null 2>&1 &
    fi
    disown 2>/dev/null || true
    log "restored swaybg -> $omarchy_bg"
  elif command -v hyprpaper >/dev/null 2>&1; then
    hyprctl dispatch exec hyprpaper >/dev/null 2>&1 || true
    log "restored hyprpaper"
  else
    log "no known static wallpaper daemon to restore"
  fi
}

start_mpvpaper_fg() {
  local target="$1" video="$2"
  kill_static_wallpapers
  pkill -x mpvpaper 2>/dev/null || true
  sleep 0.3
  log "systemd start target=$target video=$video"
  start_watcher
  # shellcheck disable=SC2086  # intentional word-splitting on MPV_OPTS
  exec mpvpaper -o "$MPV_OPTS" "$target" "$video"
}

start_mpvpaper_bg() {
  local target="$1" video="$2"
  kill_static_wallpapers
  pkill -x mpvpaper 2>/dev/null || true
  sleep 0.3
  log "start target=$target video=$video"
  # setsid detaches from the controlling terminal so mpvpaper survives the
  # TUI terminal closing; uwsm-app parents it to the user systemd scope
  # (matches how Omarchy autostarts swaybg). Without both, mpvpaper was
  # getting SIGHUP'd when the Walker-spawned terminal exited.
  # shellcheck disable=SC2086  # intentional word-splitting on MPV_OPTS
  if command -v uwsm-app >/dev/null 2>&1; then
    setsid uwsm-app -- mpvpaper -o "$MPV_OPTS" "$target" "$video" \
      < /dev/null >> "$LOG_FILE" 2>&1 &
  else
    # shellcheck disable=SC2086
    setsid mpvpaper -o "$MPV_OPTS" "$target" "$video" \
      < /dev/null >> "$LOG_FILE" 2>&1 &
  fi
  disown 2>/dev/null || true
  start_watcher
  sleep 0.8
  if ! is_running; then
    tui_err "mpvpaper failed to start. See $LOG_FILE for details."
    return 1
  fi
}

stop_mpvpaper() {
  # Serialize stops so the TUI-initiated path and systemd's ExecStop (which
  # fires when the TUI kills mpvpaper) can't both be mid-restore at once.
  # `flock -n` → second caller skips cleanly if the first still holds it.
  (
    flock -n 9 || { log "stop already in progress, skipping"; exit 0; }
    kill_watcher
    pkill -x mpvpaper 2>/dev/null || true
    # mpv cleans up its IPC socket on exit; a hard kill can leave it dangling.
    rm -f "$MPV_IPC_SOCK" 2>/dev/null || true
    restore_static_wallpaper
    log "stopped"
  ) 9>"$STATE_DIR/.stop.lock"
}

# ===== actions ================================================================

action_toggle() {
  require_gum
  require_tty

  if is_running; then
    show_header
    local autostart_label
    if autostart_enabled; then
      autostart_label="Turn autostart OFF"
    else
      autostart_label="Turn autostart ON"
    fi
    local choice
    choice=$(gum choose --header="What would you like to do?" \
      "Stop motion wallpaper" "Change video" "$autostart_label" "Cancel") || exit 0

    case "$choice" in
      "Stop motion wallpaper")
        stop_mpvpaper
        tui_ok "Stopped. Normal wallpaper restored."
        notify "Motion wallpaper stopped."
        # If autostart is on, a plain stop will let the wallpaper return on
        # next reboot. Offer to turn autostart off so "stop" means "stop".
        if autostart_enabled; then
          if gum confirm "Autostart is still enabled — also disable it so the wallpaper doesn't resume after reboot?"; then
            autostart_disable
            tui_ok "Autostart disabled."
          fi
        fi
        ;;
      "Change video")
        load_state
        local video
        video=$(pick_video) || exit 0
        [ -z "$video" ]  && exit 0
        [ -f "$video" ]  || { tui_err "File not found: $video"; exit 1; }
        save_state "$video" "$LAST_TARGET"
        start_mpvpaper_bg "$LAST_TARGET" "$video" || exit 1
        tui_ok "Swapped to $(basename "$video")."
        notify "Motion wallpaper updated."
        ;;
      "Turn autostart ON")
        autostart_enable && tui_ok "Autostart enabled — wallpaper will resume after reboot."
        ;;
      "Turn autostart OFF")
        autostart_disable
        tui_ok "Autostart disabled."
        # Motion wallpaper is still running at this point. Offer to also stop
        # it now so the label "turn off" matches visible behaviour.
        if is_running; then
          if gum confirm "Motion wallpaper is still running — stop it now too?"; then
            stop_mpvpaper
            tui_ok "Stopped. Normal wallpaper restored."
            notify "Motion wallpaper stopped."
          fi
        fi
        ;;
      Cancel) exit 0 ;;
    esac
    return 0
  fi

  show_header
  local target video
  target=$(pick_target) || exit 0
  [ -z "$target" ] && exit 0
  video=$(pick_video) || exit 0
  [ -z "$video" ]  && exit 0
  [ -f "$video" ]  || { tui_err "File not found: $video"; exit 1; }

  save_state "$video" "$target"
  start_mpvpaper_bg "$target" "$video" || exit 1
  tui_ok "Started $(basename "$video") on $target."
  notify "Motion wallpaper started on $target."

  # Offer autostart on first fresh start (skipped silently if already on or
  # if the systemd unit isn't installed).
  if autostart_installed && ! autostart_enabled; then
    if gum confirm "Start motion wallpaper automatically after login / reboot?"; then
      autostart_enable && tui_ok "Autostart enabled."
    fi
  fi
}

action_change() {
  require_gum
  require_tty
  if ! is_running; then
    tui_err "Motion wallpaper is not running."
    exit 1
  fi
  load_state
  local video
  video=$(pick_video) || exit 0
  [ -z "$video" ]  && exit 0
  [ -f "$video" ]  || { tui_err "File not found: $video"; exit 1; }
  save_state "$video" "$LAST_TARGET"
  start_mpvpaper_bg "$LAST_TARGET" "$video" || exit 1
  tui_ok "Swapped to $(basename "$video")."
  notify "Motion wallpaper updated."
}

action_start() {
  load_state
  if [ -z "$LAST_VIDEO" ] || [ -z "$LAST_TARGET" ]; then
    log "autostart: no saved state, exiting cleanly"
    exit 0
  fi
  if [ ! -f "$LAST_VIDEO" ]; then
    log "autostart: saved video missing ($LAST_VIDEO)"
    exit 1
  fi
  start_mpvpaper_fg "$LAST_TARGET" "$LAST_VIDEO"
}

action_stop() {
  stop_mpvpaper
}

action_status() {
  if command -v gum >/dev/null 2>&1 && [ -t 1 ]; then
    show_header
  else
    load_state
    if is_running; then echo "status: running"; else echo "status: stopped"; fi
    if [ -n "${LAST_TARGET:-}" ]; then echo "target: $LAST_TARGET"; fi
    if [ -n "${LAST_VIDEO:-}"  ]; then echo "video:  $LAST_VIDEO";  fi
  fi
}

# ===== main ===================================================================

case "${1:-toggle}" in
  toggle) action_toggle ;;
  start)  action_start  ;;
  stop)   action_stop   ;;
  change) action_change ;;
  status) action_status ;;
  -h|--help)
    cat <<USAGE
$APP_NAME
Usage: ${0##*/} [toggle|start|stop|change|status]

  toggle   Interactive gum TUI (default). If running, offers Stop / Change.
  start    Start from saved state without prompting (for systemd).
  stop     Stop mpvpaper and restore the normal wallpaper.
  change   Pick a new video while already running.
  status   Print current state.
USAGE
    ;;
  *)
    echo "Unknown action: $1" >&2
    echo "Try: ${0##*/} --help" >&2
    exit 1
    ;;
esac
