#!/bin/bash

# omarchy:summary=Sync the generated Omarchy theme to Hermes as a skin
# omarchy:args=[--activate] [--wait]
# omarchy:hidden=true

# A skin is Hermes' one theme unit for the desktop app, the TUI and the CLI;
# its gateway watches the active skin file and repaints every surface on change.

set -euo pipefail

HERMES_SOURCE_PATH="$HOME/.local/state/omarchy/current/theme/hermes.yaml"
HERMES_THEME_NAME_PATH="$HOME/.local/state/omarchy/current/theme.name"
HERMES_HOME="${HERMES_HOME:-$HOME/.hermes}"
HERMES_CONFIG_PATH="$HERMES_HOME/config.yaml"
HERMES_SKIN_NAME="omarchy"
# The command the readiness probe vets, rather than whichever hermes is on PATH.
HERMES_COMMAND="$HOME/.local/bin/hermes"
# Written by the desktop app once the runtime its first launch provisions is in.
HERMES_BOOTSTRAP_MARKER="$HERMES_HOME/hermes-agent/.hermes-bootstrap-complete"
HERMES_ACTIVATE=0
HERMES_WAIT=0
HERMES_WAIT_LIMIT=$((30 * 60))

usage() {
  echo "Usage: omarchy-theme-set-hermes [--activate] [--wait]"
}

for arg in "$@"; do
  case "$arg" in
    --activate)
      HERMES_ACTIVATE=1
      ;;
    --wait)
      HERMES_ACTIVATE=1
      HERMES_WAIT=1
      ;;
    -h | --help)
      usage
      exit 0
      ;;
    *)
      usage >&2
      exit 1
      ;;
  esac
done

# A theme switch runs this beside a dozen other hooks; only --activate explains.
note() {
  if (( HERMES_ACTIVATE == 1 )); then
    echo "$*" >&2
  fi
}

# A theme applied before the template existed has no skin rendered yet.
if [[ ! -f $HERMES_SOURCE_PATH ]]; then
  (( HERMES_ACTIVATE == 1 )) || exit 0

  if [[ ! -s $HERMES_THEME_NAME_PATH ]]; then
    echo "Hermes skin source missing: $HERMES_SOURCE_PATH" >&2
    echo "Select an Omarchy theme first." >&2
    exit 1
  fi

  omarchy-theme-refresh

  if [[ ! -f $HERMES_SOURCE_PATH ]]; then
    echo "Hermes skin source missing after refreshing the theme: $HERMES_SOURCE_PATH" >&2
    exit 1
  fi
fi

# The first launch takes minutes and may be abandoned; the readiness probe
# would start Hermes to answer, so poll for the marker instead.
if (( HERMES_WAIT == 1 )); then
  waited=0
  until [[ -f $HERMES_BOOTSTRAP_MARKER && -f $HERMES_CONFIG_PATH ]]; do
    if (( waited >= HERMES_WAIT_LIMIT )); then
      echo "Hermes did not finish setting up within $((HERMES_WAIT_LIMIT / 60)) minutes; run omarchy-theme-set-hermes --activate once it has." >&2
      exit 0
    fi

    sleep 10
    waited=$((waited + 10))
  done
fi

# Provisioning creates ~/.hermes on every machine for the Omarchy skill; the
# config is what Hermes writes once it has actually run.
if [[ ! -f $HERMES_CONFIG_PATH ]]; then
  note "Hermes is not set up yet; launch it once, then run omarchy-theme-set-hermes --activate."
  exit 0
fi

# Hermes parses the skin as YAML and hands its strings to every surface, so only
# the name, a plain description and #rrggbb colours may reach it, in the order
# YAML needs them. YAML breaks lines on bytes grep does not, so the bytes are
# counted first, NUL included.
skin_is_well_formed() {
  local skin="$1"

  [[ -f $skin ]] &&
    (( $(LC_ALL=C tr -d ' -~\n' <"$skin" | wc -c) == 0 )) &&
    awk -v name="name: $HERMES_SKIN_NAME" '
      bad { next }
      /^#/ || /^[[:space:]]*$/ { next }
      !seen_name { if ($0 == name) seen_name = 1; else bad = 1; next }
      !seen_colors {
        if ($0 == "colors:") seen_colors = 1
        else if (!seen_description && $0 ~ /^description: [A-Za-z0-9 ,.()-]{0,200}$/) seen_description = 1
        else bad = 1
        next
      }
      /^  [a-z_]{1,64}: "#[0-9a-fA-F]{6}"$/ { colors++; next }
      { bad = 1 }
      END { exit (bad || !seen_colors || colors == 0) }
    ' "$skin"
}

# The check has to cover the bytes that get published, and the theme can change
# underneath between the two, so a private copy is taken and that is checked.
snapshot_dir=$(mktemp -d)
trap 'rm -rf "$snapshot_dir"' EXIT
HERMES_SNAPSHOT="$snapshot_dir/$HERMES_SKIN_NAME.yaml"

take_snapshot() {
  cp "$HERMES_SOURCE_PATH" "$HERMES_SNAPSHOT" 2>/dev/null && skin_is_well_formed "$HERMES_SNAPSHOT"
}

if ! take_snapshot; then
  echo "Skipping Hermes skin: $(basename "$HERMES_SOURCE_PATH") is not a plain color palette." >&2
  exit 0
fi

# The gateway reads the file whole on an mtime change, so the write is atomic;
# -T so a directory at the skin's path is an error rather than a destination.
publish_skin() {
  local skins_dir="$1"
  local tmp

  mkdir -p "$skins_dir" 2>/dev/null || return 1
  tmp=$(mktemp "$skins_dir/$HERMES_SKIN_NAME.yaml.XXXXXX" 2>/dev/null) || return 1
  if ! cp "$HERMES_SNAPSHOT" "$tmp" 2>/dev/null || ! mv -T "$tmp" "$skins_dir/$HERMES_SKIN_NAME.yaml" 2>/dev/null; then
    rm -f "$tmp"
    return 1
  fi
}

# A profile is a Hermes home of its own; existing ones get the skin, none are
# made, and one that cannot take it does not cost the others.
publish_skin_everywhere() {
  local profile

  publish_skin "$HERMES_HOME/skins" || {
    echo "Could not publish the Hermes skin to $HERMES_HOME/skins." >&2
    return 1
  }

  for profile in "$HERMES_HOME"/profiles/*/; do
    [[ -d $profile ]] || continue
    publish_skin "${profile%/}/skins" || note "Could not publish the skin to the Hermes profile $(basename "$profile")."
  done
}

publish_skin_everywhere

# Hermes reads the config of the profile named in active_profile; the profile
# exists once its directory does, with or without a config of its own.
active_config_path() {
  local profile

  profile=$(cat "$HERMES_HOME/active_profile" 2>/dev/null || true)
  profile=${profile,,}

  if [[ -n $profile && $profile != "default" && -d $HERMES_HOME/profiles/$profile ]]; then
    echo "$HERMES_HOME/profiles/$profile/config.yaml"
  else
    echo "$HERMES_CONFIG_PATH"
  fi
}

# A theme switch finishes the hand-over only for the app Omarchy installed, and
# only Hermes' default is ever replaced, so a config plainly naming another skin
# ends it here without starting Hermes. Anything less plain is for Hermes to read.
if (( HERMES_ACTIVATE == 0 )); then
  omarchy-pkg-present hermes-desktop || exit 0

  skin_line=$(grep -m1 -x '  skin: .*' "$(active_config_path)" 2>/dev/null || true)
  case "${skin_line#  skin: }" in
    "" | default | null | true | false | *[!A-Za-z0-9_-]*) ;;
    *) exit 0 ;;
  esac
fi

# The probe vets the command before anything here runs it: a wrapper that is
# not a working Hermes, or the retired mise stub that would build one.
if ! omarchy-install-hermes-cli --check 2>/dev/null; then
  note "Hermes is not ready, so the Omarchy skin is published but not active."
  note "Once Hermes runs, activate it with: hermes config set display.skin $HERMES_SKIN_NAME"
  exit 0
fi

# Only Hermes' own default is replaced, so a skin chosen in Hermes stays; an
# answer that did not come is not a default.
if ! current_skin=$(timeout 15 "$HERMES_COMMAND" config get display.skin 2>/dev/null); then
  note "Hermes did not say which skin it is on, so the Omarchy skin is published but not active."
  exit 0
fi

if [[ -n $current_skin && $current_skin != "default" && $current_skin != "$HERMES_SKIN_NAME" ]]; then
  note "Hermes is set to the '$current_skin' skin; leaving it. Switch with: hermes config set display.skin $HERMES_SKIN_NAME"
  exit 0
fi

# Hermes' own writer: it updates the active profile's config and touches the
# skin file so a running gateway broadcasts the change. A refusal is cosmetic.
if ! timeout 30 "$HERMES_COMMAND" config set display.skin "$HERMES_SKIN_NAME" >/dev/null 2>&1; then
  note "Hermes refused to switch skins, so the Omarchy skin is published but not active."
  exit 0
fi

note "Hermes is on the Omarchy skin."

# The desktop applies a skin only from a broadcast, and a config written before
# the gateway seeded its watcher goes unannounced; a later write is announced.
# The theme may have changed underneath in the meantime, so it is checked again.
if (( HERMES_WAIT == 1 )); then
  sleep 60

  if take_snapshot; then
    publish_skin_everywhere
  fi
fi
