# Waybar — two-tier theming:
#
#  1. Default: structure, fonts, geometry AND palette baked from
#     theme-state.json (colors as GTK named colors, @define-color).
#
#  2. Whole-swap: themes with their own visual identity ship
#     <themesDir>/<slug>/waybar.css (and optionally waybar.jsonc) which
#     replace the generated style/layout entirely. Probed at eval time —
#     pure, it's flake source.
{ config, lib, pkgs, ... }:

let
  t = config.nomarchy.theme;

  # Show the active-keyboard-layout indicator only when more than one layout
  # is in play — multiple session layouts (comma-separated) or per-device
  # overrides (nomarchy.keyboard.devices) — so single-layout bars stay clean.
  showLanguage = lib.hasInfix "," config.nomarchy.keyboard.layout
    || config.nomarchy.keyboard.layouts != [ ]
    || config.nomarchy.keyboard.devices != { };

  # Power-profile indicator (power-profiles-daemon). Both scripts self-
  # gate: they exit silently unless this is a laptop (a battery is
  # present) running PPD, so the module hides itself on desktops and
  # under the TLP backend — no system→home coupling. Speedometer glyphs
  # (slow/medium/fast) map to power-saver/balanced/performance.
  #
  # Named writeShellScriptBins (put on PATH via home.packages below) rather
  # than bare writeShellScript store paths, so the whole-swap themes' static
  # waybar.jsonc can exec them by name too — same as the swaync bell.
  # Waybar supervisor — exec-once has no restart, so a crashed bar used to
  # leave the session bar-less until relogin (seen on hardware: a theme
  # switch crashed waybar mid-reload). Respawns on ANY exit — a plain
  # `pkill -x waybar` is now a clean restart, which nomarchy-theme-sync
  # uses instead of the crash-prone in-place SIGUSR2 reload when it sees
  # this supervisor running. Crash-loop guard: 5 exits within 10s of
  # their start → give up with a critical notification instead of
  # spinning. Stop the bar for real: pkill -f nomarchy-waybar (TERM is
  # trapped to take the child down too).
  waybarSupervisor = pkgs.writeShellScriptBin "nomarchy-waybar" ''
    child=
    trap '[ -n "$child" ] && kill "$child" 2>/dev/null; exit 0' TERM INT
    fails=0
    while :; do
      start=$(date +%s)
      waybar & child=$!
      wait "$child"; code=$?
      if [ $(( $(date +%s) - start )) -lt 10 ]; then
        fails=$((fails + 1))
        if [ "$fails" -ge 5 ]; then
          notify-send -u critical "Waybar" \
            "Crashing on start (exit $code) — check ~/.config/waybar. Giving up." 2>/dev/null
          exit 1
        fi
      else
        fails=0
      fi
      sleep 1
    done
  '';

  powerProfileStatus = pkgs.writeShellScriptBin "nomarchy-powerprofile-status" ''
    # Name-agnostic system battery (BAT0, CMB0, …) — BACKLOG #60.
    has_bat=
    for d in /sys/class/power_supply/*/; do
      [ "$(cat "$d/type" 2>/dev/null)" = Battery ] || continue
      [ "$(cat "$d/scope" 2>/dev/null || echo System)" = Device ] && continue
      has_bat=1; break
    done
    [ -n "$has_bat" ] || exit 0
    command -v powerprofilesctl >/dev/null 2>&1 || exit 0
    prof=$(powerprofilesctl get 2>/dev/null) || exit 0
    case "$prof" in
      power-saver) icon="󰾆" ;;
      performance) icon="󰓅" ;;
      *)           icon="󰾅" ;;
    esac
    printf '{"text":"%s","tooltip":"Power profile: %s","class":"%s"}\n' "$icon" "$prof" "$prof"
  '';
  # Cycle to the next profile this machine actually offers (wraps around).
  powerProfileCycle = pkgs.writeShellScriptBin "nomarchy-powerprofile-cycle" ''
    command -v powerprofilesctl >/dev/null 2>&1 || exit 0
    cur=$(powerprofilesctl get 2>/dev/null) || exit 0
    avail=$(powerprofilesctl list 2>/dev/null | sed -nE 's/^[* ] ([a-z-]+):$/\1/p')
    next=$(printf '%s\n' "$avail" | awk -v cur="$cur" '
      { a[NR] = $0 }
      END { for (i = 1; i <= NR; i++) if (a[i] == cur) { print a[i % NR + 1]; f = 1 }
            if (!f && NR) print a[1] }')
    [ -n "$next" ] && powerprofilesctl set "$next"
  '';

  # Opens calcurse (a lightweight TUI calendar, month view by default) in a
  # floating, centered ghostty window — bound to the Waybar clock's on-click.
  # A distinct --class gives the window a matchable app-id for the
  # float+center+size windowrule (hyprland.nix); --gtk-single-instance=false
  # forces a fresh standalone window (ghostty defaults single-instance on).
  # calcurse ships uncommented in the downstream template (opt-out), so
  # self-gate with a helpful notify if it's been removed. ghostty is always
  # installed (ghostty.nix), so it can be relied on for the --class window.
  calendarLauncher = pkgs.writeShellScriptBin "nomarchy-calendar" ''
    if ! command -v calcurse >/dev/null 2>&1; then
      notify-send "Calendar" "calcurse isn't installed (removed from home.packages?)." 2>/dev/null
      exit 0
    fi
    exec ghostty --class=com.nomarchy.calendar --gtk-single-instance=false -e calcurse
  '';

  # VPN indicator — shows a shield when a NetworkManager VPN/WireGuard
  # connection is active OR Tailscale is up; prints nothing otherwise so the
  # module self-hides (like nightlight/updates). Click opens the VPN submenu.
  vpnStatus = pkgs.writeShellScriptBin "nomarchy-vpn-status" ''
    active=$(nmcli -t -f TYPE connection show --active 2>/dev/null | grep -Exm1 'vpn|wireguard')
    ts=""
    if command -v tailscale >/dev/null 2>&1; then
      [ "$(tailscale status --json 2>/dev/null | jq -r '.BackendState // empty')" = Running ] && ts=1
    fi
    [ -n "$active" ] || [ -n "$ts" ] || exit 0
    printf '{"text":"󰦝","tooltip":"VPN active (click to manage)","class":"on"}\n'
  '';

  # Health warning — self-gates: prints nothing while nomarchy-doctor
  # exits 0, so the module only appears when the sheet has a ✖ (the
  # same self-hide discipline as vpn/nightlight/updates). The tooltip
  # carries the first failing lines; click opens the full sheet.
  # Absolute path to the doctor binary: waybar's custom-module env can
  # miss system PATH, and `command -v … || exit 0` then self-hides forever.
  doctorStatus = pkgs.writeShellScriptBin "nomarchy-doctor-status" ''
    out=$(${pkgs.nomarchy-doctor}/bin/nomarchy-doctor 2>/dev/null) && exit 0
    # Strip ANSI so the tooltip is plain text (and waybar never chokes on
    # control chars); keep only the ✖ lines.
    tip=$(printf '%s\n' "$out" \
      | ${pkgs.gnused}/bin/sed 's/\x1b\[[0-9;]*m//g' \
      | grep '✖' | head -5 \
      | ${pkgs.jq}/bin/jq -Rs 'rtrimstr("\n") + "\n(click for the full sheet)"')
    # 󰀨 (md-alert-circle): alert shape that survives incomplete Nerd Font
    # cuts better than 󰒡; class:bad still paints it @bad.
    printf '{"text":"󰀨","tooltip":%s,"class":"bad"}\n' "$tip"
  '';

  # Per-theme override probe.
  assetDir = config.nomarchy.themesDir + "/${t.slug}";
  styleOverride = assetDir + "/waybar.css";
  configOverride = assetDir + "/waybar.jsonc";
  hasStyleOverride = builtins.pathExists styleOverride;
  hasConfigOverride = builtins.pathExists configOverride;

  # The palette as GTK named colors, straight from the JSON.
  colorDefs = lib.concatStringsSep "\n"
    (lib.mapAttrsToList (name: value: "@define-color ${name} ${value};") t.colors);

  generatedSettings = {
    # `bottom`, not `top`: on Hyprland the `top` layer renders above every
    # window — so a real fullscreen surface (a browser video gone
    # fullscreen) sits *under* the bar. On `bottom` the fullscreen window
    # covers the bar, while the exclusive zone still reserves its space in
    # normal tiling. Keep in sync with the whole-swap jsoncs (parity rule).
    layer = "bottom";
    position = "top";
    height = 34;
    margin-top = t.ui.gapsOut;
    margin-left = t.ui.gapsOut;
    margin-right = t.ui.gapsOut;
    spacing = 8;

    # waybar re-reads style.css when it changes on disk, so a
    # home-manager switch restyles the running bar without a restart.
    reload_style_on_change = true;

    # Logo + powermenu: whole-swaps already ship these (parity was reverse
    # — BACKLOG #63). Click targets are existing nomarchy-menu entry points.
    modules-left = [ "custom/nomarchy" "hyprland/workspaces" "hyprland/window" ];
    modules-center = [ "clock" ];
    modules-right = [ "custom/recording" "idle_inhibitor" "tray" "custom/vpn" "pulseaudio" "custom/powerprofile" "custom/nightlight" ]
      ++ lib.optional showLanguage "hyprland/language"
      ++ [ "battery" "custom/doctor" "custom/updates" "custom/notification" "custom/powermenu" ];

    "custom/nomarchy" = {
      interval = "once";
      # U+F000 — Nomarchy monogram (literal UTF-8; Nix has no \u escapes).
      # CSS pins font-family: Nomarchy so Nerd Fonts' glass glyph does not win.
      format = "";
      on-click = "nomarchy-menu";
      tooltip-format = "Nomarchy menu";
    };

    "custom/powermenu" = {
      format = ""; # U+F011 power symbol
      on-click = "nomarchy-menu power";
      tooltip = false;
    };

    "hyprland/workspaces" = {
      format = "{icon}";
      on-click = "activate";
    };

    "hyprland/window" = {
      max-length = 48;
      separate-outputs = true;
    };

    clock = {
      format = "{:%H:%M}";
      # Left-click → the calendar (nomarchy-calendar → calcurse in a floating
      # ghostty). Replaces the old format-alt date toggle; the long date now
      # rides in the tooltip's first line, so nothing is lost on hover.
      on-click = "nomarchy-calendar";
      # The zone line stays live under auto-timezone (the tz-watch SIGUSR2
      # reload keeps it showing the currently *detected* zone).
      tooltip-format = "{:%A %d %B %Y}\n{:%Z (UTC%z)}\n<tt><small>{calendar}</small></tt>";
    };

    # Active keyboard layout (per focused device) — only placed in
    # modules-right when showLanguage (see above).
    "hyprland/language" = {
      format = "<span size='${toString (t.fonts.size + 2)}pt'>󰌌</span> {short}";
      tooltip = false;
    };

    pulseaudio = {
      format = "<span size='${toString (t.fonts.size + 2)}pt'>{icon}</span>  {volume}%";
      format-muted = "󰝟";
      format-icons.default = [ "󰕿" "󰖀" "󰕾" ];
      on-click = "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle";
      # Right-click → the full mixer (per-app volumes) in a floating window
      # (item 35). pwvucontrol ships in the template suite; the Hyprland
      # windowrule floats it. Keep in sync with the whole-swap jsoncs.
      on-click-right = "pwvucontrol";
    };

    # Idle inhibitor (caffeine): click 󰾪 → 󰅶 to hold the screen awake —
    # blocks hypridle's lock / display-off / suspend during video or a
    # presentation. Waybar holds a Wayland idle-inhibit while activated;
    # the state resets with the bar (deliberate — caffeine shouldn't
    # survive a relogin). Summer-night's whole-swap bar had this first
    # (reverse parity gap, 2026-07-04).
    idle_inhibitor = {
      format = "{icon}";
      format-icons = { activated = "󰅶"; deactivated = "󰾪"; };
      tooltip-format-activated = "Screen held awake — click to release";
      tooltip-format-deactivated = "Keep the screen awake (caffeine)";
    };

    # No network module: nm-applet lives in the tray (the GUI path), so the
    # bar's wifi/ethernet indicator would just duplicate it.

    battery = {
      states = { warning = 25; critical = 10; };
      format = "<span size='${toString (t.fonts.size + 2)}pt'>{icon}</span> {capacity}%";
      format-charging = "<span size='${toString (t.fonts.size + 2)}pt'>󰂄</span> {capacity}%";
      format-icons = [ "󰁺" "󰁼" "󰁾" "󰂀" "󰁹" ];
      # Click either the battery or the power-profile icon → the combined
      # power menu (profile + charge cap). The granular System rows stay.
      on-click = "nomarchy-menu powermgmt";
    };

    "custom/powerprofile" = {
      exec = "nomarchy-powerprofile-status";
      return-type = "json";
      interval = 5;
      # Opens the same combined power menu as the battery icon (item 36b).
      # nomarchy-powerprofile-cycle stays a standalone bin for downstream
      # rebinding — it's just no longer the default click.
      on-click = "nomarchy-menu powermgmt";
    };

    # VPN shield — self-hides unless a NM VPN/WireGuard tunnel or Tailscale is
    # up. Click opens the VPN submenu (nomarchy-vpn). 5s poll like powerprofile.
    "custom/vpn" = {
      exec = "nomarchy-vpn-status";
      return-type = "json";
      interval = 5;
      on-click = "nomarchy-vpn";
    };

    # Health check is real work (systemctl/disk/git sweeps), so a long
    # interval — this is a tripwire, not a monitor.
    "custom/doctor" = {
      exec = "nomarchy-doctor-status";
      return-type = "json";
      format = "{}";
      interval = 300;
      # signal 10: poke after a fix (or from theme-shot) so the tripwire
      # doesn't wait a full interval after a first-poll miss.
      signal = 10;
      on-click = "nomarchy-menu doctor";
    };

    # Screen recording indicator. Self-gates: visible only while
    # nomarchy-record runs (status prints nothing otherwise), and the
    # click IS the stop surface — the menu only starts recordings.
    # signal 8: the recorder pokes the bar on start/stop so the ⏺
    # appears/vanishes instantly (the interval is just a safety net).
    "custom/recording" = {
      exec = "nomarchy-record status";
      return-type = "json";
      interval = 10;
      signal = 8;
      on-click = "nomarchy-record stop";
    };

    # Night-light (hyprsunset) indicator. Self-gates: the moon shows only while
    # the schedule runs; otherwise the status helper prints nothing => hidden
    # (enable / re-enable from the System menu). Click toggles instantly — writes
    # the in-flake on/off (settings.nightlight.on, no rebuild) and flips the unit
    # with systemctl, so the choice lands in the flake and survives reboot.
    "custom/nightlight" = {
      exec = "nomarchy-nightlight status";
      return-type = "json";
      interval = 3;
      on-click = "nomarchy-nightlight toggle";
    };

    # Update awareness. Self-gates: hidden unless nomarchy.updates is enabled
    # AND the periodic check found something (the helper prints nothing then).
    # signal 9 lets the checker refresh it instantly; click opens the upgrade
    # flow in a terminal.
    "custom/updates" = {
      exec = "nomarchy-updates status";
      return-type = "json";
      interval = 1800;
      signal = 9;
      on-click = "${config.nomarchy.terminal} -e nomarchy-updates upgrade";
    };

    # swaync notification bell + Do-Not-Disturb state. `-swb` streams JSON
    # (text/tooltip/class) on every change, so it tracks count and DND with
    # no polling. Left-click toggles the panel; right-click toggles DND.
    # Every *suppressed* state — DND or app-inhibited — maps to the bell-off
    # glyph 󰂛 and the muted color below, so "notifications are off right now"
    # reads by SHAPE, not color alone (color-blind sweep, item 28): inhibited
    # used to reuse the normal 󰂜/󰂚 bells, distinguishable only by color.
    "custom/notification" = {
      exec = "swaync-client -swb";
      return-type = "json";
      exec-if = "which swaync-client";
      format = "{icon}";
      format-icons = {
        none = "󰂜";
        notification = "󰂚";
        dnd-none = "󰂛";
        dnd-notification = "󰂛";
        inhibited-none = "󰂛";
        inhibited-notification = "󰂛";
        dnd-inhibited-none = "󰂛";
        dnd-inhibited-notification = "󰂛";
      };
      on-click = "swaync-client -t -sw";
      on-click-right = "swaync-client -d -sw";
      tooltip = true;
      escape = true;
    };

    tray.spacing = 8;
  };

  generatedStyle = ''
    /* Palette baked from theme-state.json */
    ${colorDefs}

    * {
      font-family: "${t.fonts.ui}", "${t.fonts.mono}";
      font-size: ${toString t.fonts.size}pt;
      min-height: 0;
    }

    window#waybar {
      background: alpha(@base, 0.85);
      color: @text;
      border: ${toString t.ui.borderSize}px solid alpha(@accent, 0.4);
      border-radius: ${toString t.ui.rounding}px;
    }

    /* Dim states use the palette's @muted role: since item 28b it is
       floor-guaranteed legible on @base in every theme (muted/base >=
       2.0, gated by tools/check-theme-contrast.py) — the palettes that
       once made it vanish (gruvbox muted≈base, item 27) were retuned.
       Secondary-but-not-dim stays alpha(@text, 0.85). */
    #workspaces button {
      padding: 0 8px;
      color: @muted;
      border-radius: ${toString t.ui.rounding}px;
    }

    #workspaces button.active {
      color: @base;
      background: @accent;
    }

    #workspaces button.urgent {
      color: @base;
      background: @bad;
    }

    #window {
      color: alpha(@text, 0.85);
      padding: 0 12px;
    }

    #clock {
      color: @text;
      font-weight: bold;
    }

    #custom-nomarchy {
      color: @accent;
      font-family: Nomarchy;
      font-size: ${toString (t.fonts.size + 4)}pt;
      padding: 0 10px;
    }
    #custom-nomarchy:hover { color: @accentAlt; }

    #tray, #pulseaudio, #custom-powerprofile, #custom-nightlight, #custom-updates, #custom-vpn, #custom-recording, #idle_inhibitor, #language, #battery, #custom-doctor, #custom-notification, #custom-powermenu {
      color: alpha(@text, 0.85);
      padding: 0 10px;
    }
    #custom-powermenu:hover { color: @bad; }

    /* Group rhythm (item 28c): a wider breath before each functional
       group of the right cluster — media/stats · toggles · status —
       on top of the uniform module spacing. A group head can self-hide
       (e.g. no battery on desktops); grouping then degrades to the
       uniform spacing, never breaks. */
    #pulseaudio, #custom-powerprofile, #battery { margin-left: 14px; }

    /* Caffeine engaged → warm tone (the screen is being held awake). */
    #idle_inhibitor.activated { color: @warn; }

    /* Recording in progress → the alert red; the ⏺ is also the stop button. */
    #custom-recording.recording { color: @bad; }

    /* VPN active → accent green, reading as "connected / protected". */
    #custom-vpn.on { color: @good; }

    /* Night-light active → warm tone, matching the filter it represents. */
    #custom-nightlight.on { color: @warn; }

    /* Updates pending → accent, to draw the eye. */
    #custom-updates.available { color: @accent; }

    /* Doctor tripwire — only rendered when something is ✖, so it is
       always the alert color. */
    #custom-doctor { color: @bad; }

    /* Icon-only status modules carry no text, so they don't get the
       (size+2)pt Pango icon span the icon+text modules use — bump their
       font-size to match, or these glyphs read smaller than the volume /
       battery / language icons beside them. */
    #custom-recording, #custom-updates, #custom-vpn, #custom-nightlight, #custom-doctor, #custom-notification, #custom-powermenu { font-size: ${toString (t.fonts.size + 2)}pt; }

    /* The speedometer + caffeine glyphs render small in their em box —
       size them up a touch more so they read at a glance. */
    #custom-powerprofile, #idle_inhibitor { font-size: ${toString (t.fonts.size + 3)}pt; }

    /* notifications waiting → accent; suppressed (DND or app-inhibited) →
       muted bell-off, so it reads by shape+color, never color alone. */
    #custom-notification.notification { color: @accent; }
    #custom-notification.dnd-none,
    #custom-notification.dnd-notification,
    #custom-notification.inhibited-none,
    #custom-notification.inhibited-notification { color: @muted; }

    #pulseaudio.muted { color: @muted; }
    #battery.warning { color: @warn; }
    #battery.critical { color: @bad; }
    #battery.charging { color: @good; }
  '';
in
{
  programs.waybar = lib.mkIf config.nomarchy.waybar.enable {
    enable = true;
    # Launched from Hyprland's exec-once (hyprland.nix), NOT a systemd user
    # service. Bound to graphical-session.target the unit raced Hyprland's IPC
    # on a warm relogin — it started before the socket was up, exited, landed
    # in `failed`, and was never retried, so the bar vanished. exec-once only
    # fires once Hyprland is up, dodging the race; theme switches reload the
    # running bar via SIGUSR2 (nomarchy-theme-sync). No uwsm here to manage the
    # session target, so we don't depend on its lifecycle.
    systemd.enable = false;

    # mkDefault so downstream can replace the whole bar config/style with
    # a plain home.nix assignment. For per-theme identity, prefer the
    # whole-swap assets (themes/<slug>/waybar.{jsonc,css}); for one-off
    # tweaks of the generated bar, lib.mkForce a sub-key. See
    # docs/OVERRIDES.md.
    settings.mainBar = lib.mkDefault (
      if hasConfigOverride
      then builtins.fromJSON (builtins.readFile configOverride)
      else generatedSettings
    );

    style = lib.mkDefault (
      if hasStyleOverride
      then builtins.readFile styleOverride
      else generatedStyle
    );
  };

  # The power-profile helpers on PATH, so both the generated bar and the
  # whole-swap themes' static waybar.jsonc can exec them by name — plus
  # the supervisor hyprland.nix exec-onces.
  home.packages = lib.optionals config.nomarchy.waybar.enable
    [ waybarSupervisor powerProfileStatus powerProfileCycle vpnStatus doctorStatus calendarLauncher ];
}
