// KueSync — Companion actions
// © 2026 Krometis Analytics. All rights reserved.
//
// Every navigation command fans out to ALL agent machines configured on this connection and uses
// ABSOLUTE slide targets, so the whole fleet stays locked to the same slide and self-corrects.

// 0.12.10: at-zero colour for Hold/Blink — a curated swatch or a custom #rrggbb typed in (allowCustom).
// The agent's sanitizeZeroColor accepts either form and falls back to red on anything else.
// 0.12.11: curated at-zero colours (custom hex removed).
const ZERO_COLOR_CHOICES = [
  { id: 'red', label: 'Red' }, { id: 'amber', label: 'Amber' }, { id: 'white', label: 'White' },
  { id: 'green', label: 'Green' }, { id: 'blue', label: 'Blue' },
]
const zeroColorOption = (id = 'zerocolor') => ({
  type: 'dropdown', id, label: 'At-zero colour (Blink / Hold / Blank)', default: 'red',
  choices: ZERO_COLOR_CHOICES,
  tooltip: 'Colour shown while the clock holds/blinks/blank-holds 0:00.',
})
// 0.12.11: at-zero behaviours — Blank holds 0:00 for 5s then fades to empty; the on-screen message always stays.
const AT_ZERO_CHOICES = [
  { id: 'blink', label: 'Blink 0:00' }, { id: 'zero', label: 'Hold 0:00 (no blink)' },
  { id: 'blank', label: 'Blank (screen empties)' }, { id: 'up', label: 'Count overtime (red)' },
]
const atZeroOption = (id = 'end') => ({ type: 'dropdown', id, label: 'At zero', default: 'blink', choices: AT_ZERO_CHOICES })

export function getActions(self) {
  const modeOption = {
    type: 'dropdown', id: 'mode', label: 'Mode', default: 'relative',
    choices: [
      { id: 'relative', label: 'Builds — plays in-slide builds/animations, synced by timing (default)' },
      { id: 'synced', label: 'Static — absolute slide, self-correcting, SKIPS in-slide builds' },
    ],
  }
  // Target only some machines (advance/jump a subset) or, blank/"all", the whole fleet.
  const machinesOption = {
    type: 'textinput', id: 'machines', useVariables: true, default: '',
    label: 'Machines — blank or "all" = whole fleet. Otherwise comma-separate machine numbers and/or IPs (you can mix), e.g. "1,3" or "192.168.1.51, 192.168.1.52" or "1, 192.168.1.55".',
  }
  return {
    // ---------------- basic navigation ----------------
    next: { name: 'Next', options: [modeOption, machinesOption], callback: async (a) => self.next(a.options.mode || 'relative', await self.parseVariablesInString(a.options.machines || '')) },
    prev: { name: 'Previous', options: [modeOption, machinesOption], callback: async (a) => self.prev(a.options.mode || 'relative', await self.parseVariablesInString(a.options.machines || '')) },

    goto: {
      name: 'Go to slide (number)',
      options: [
        { type: 'textinput', id: 'slide', label: 'Slide number', default: '1', useVariables: true },
        machinesOption,
      ],
      callback: async (a) => {
        const n = Number(await self.parseVariablesInString(a.options.slide))
        const machines = await self.parseVariablesInString(a.options.machines || '')
        if (Number.isFinite(n) && n > 0) self.gotoSlide(n, machines)
      },
    },

    jump: {
      name: 'Jump forward / back by N slides',
      options: [
        { type: 'dropdown', id: 'dir', label: 'Direction', default: 'forward', choices: [
          { id: 'forward', label: 'Forward' }, { id: 'back', label: 'Back' },
        ] },
        { type: 'number', id: 'count', label: 'Slides', default: 5, min: 1, max: 999 },
        machinesOption,
      ],
      callback: async (a) => self.jump(a.options.dir, Number(a.options.count) || 1, await self.parseVariablesInString(a.options.machines || '')),
    },

    // ---------------- launch a numbered show (0.10.0) ----------------
    // Open a numbered deck from KueSync Link on specific machines / groups, full screen at slide 1. The show
    // dropdown auto-populates from the fleet (a deck whose name starts with a number, e.g. "006 Welcome.pptx").
    launch_show: {
      name: 'Launch numbered show (KueSync Link)',
      options: [
        { type: 'dropdown', id: 'show', label: 'Show (numbered deck in KueSync Link)', default: '', choices: self.showChoices(), allowCustom: true,
          tooltip: 'Pick a numbered deck (or type its number). It opens on the chosen machines at slide 1, full screen.' },
        machinesOption,
      ],
      callback: async (a) => self.launchShow(await self.parseVariablesInString(a.options.show || ''), await self.parseVariablesInString(a.options.machines || '')),
    },

    // ---------------- media transport (PowerPoint; Windows) ----------------
    media_playpause: { name: 'Media: play / pause video (PowerPoint)', options: [machinesOption], callback: async (a) => self.media('media_playpause', await self.parseVariablesInString(a.options.machines || '')) },
    media_end:       { name: 'Media: jump to end of video (PowerPoint)', options: [machinesOption], callback: async (a) => self.media('media_end', await self.parseVariablesInString(a.options.machines || '')) },
    media_stop:      { name: 'Media: stop video (PowerPoint)', options: [machinesOption], callback: async (a) => self.media('media_stop', await self.parseVariablesInString(a.options.machines || '')) },
    open_file: {
      name: 'Open / load a presentation file',
      options: [ { type: 'textinput', id: 'path', label: 'File path (.key / .pptx)', default: '', useVariables: true },
        { type: 'textinput', id: 'password', label: 'Deck password (optional — Keynote 15.2+ opens protected decks)', default: '', useVariables: true },
        machinesOption ],
      callback: async (a) => self.openFile(await self.parseVariablesInString(a.options.path || ''), await self.parseVariablesInString(a.options.machines || ''), await self.parseVariablesInString(a.options.password || '')),
    },

    first: { name: 'First slide', options: [], callback: () => self.gotoSlide(1) },
    last: { name: 'Last slide', options: [], callback: () => self.last() },
    start: { name: 'Start slideshow (all machines)', options: [], callback: () => self.sendCommand({ action: 'start' }) },
    stop: { name: 'Stop / end slideshow (all machines)', options: [], callback: () => self.sendCommand({ action: 'stop' }) },

    // ---------------- sync helper ----------------
    sync_now: {
      name: 'Sync Now — match every machine to the Leader',
      options: [
        { type: 'static-text', id: 'info', label: 'What this does',
          value: 'One tap snaps the whole fleet to the Leader’s current slide (from Companion’s view the primary machine IS the Leader), realigning any Follower that drifted, reconnected, or was advanced on its own. It jumps straight to that slide and SKIPS any in-slide builds, so use it between builds — a great "panic re-align" button.' },
      ],
      callback: () => self.resync(0),
    },
    resync: {
      name: 'Resync all machines (snap to a specific slide)',
      options: [
        { type: 'textinput', id: 'slide', label: 'Slide (blank = current primary slide)', default: '', useVariables: true },
      ],
      callback: async (a) => {
        const raw = (await self.parseVariablesInString(a.options.slide || '')).trim()
        const n = raw ? Number(raw) : 0
        self.resync(Number.isFinite(n) ? n : 0)
      },
    },

    // ---------------- follower Active (0.9.6 / picker 0.9.8) ----------------
    // Park / un-park one follower. Pick it from the live dropdown (by DEVICE NAME · IP — matches the leader's
    // labeling) so a button always hits the right physical machine regardless of IP-box order. You can still
    // type a machine number (1–8) or an IP (allowCustom), so older buttons keep working. Pair with the
    // "Follower active" feedback for a green/red button that both controls and shows state.
    follower_activate: {
      name: 'Follower — Activate (rejoin the show)',
      options: [
        { type: 'dropdown', id: 'machine', label: 'Follower (by name · IP)', default: (self.followerChoices()[0] || {}).id || '1', choices: self.followerChoices(), allowCustom: true, tooltip: 'Pick the machine by its name. You can also type a machine number (1–8) or an IP.' },
        { type: 'checkbox', id: 'resync', label: 'Snap it to the fleet’s current slide when it rejoins', default: true },
      ],
      callback: async (a) => {
        const m = (await self.parseVariablesInString(String(a.options.machine || ''))).trim()
        self.setFollowerActive(m, true, { resync: a.options.resync !== false })
      },
    },
    follower_deactivate: {
      name: 'Follower — Deactivate (park / sit out)',
      options: [
        { type: 'dropdown', id: 'machine', label: 'Follower (by name · IP)', default: (self.followerChoices()[0] || {}).id || '1', choices: self.followerChoices(), allowCustom: true, tooltip: 'Pick the machine by its name. You can also type a machine number (1–8) or an IP.' },
      ],
      callback: async (a) => {
        const m = (await self.parseVariablesInString(String(a.options.machine || ''))).trim()
        self.setFollowerActive(m, false)
      },
    },
    follower_toggle_active: {
      name: 'Follower — Toggle Active / Inactive',
      options: [
        { type: 'dropdown', id: 'machine', label: 'Follower (by name · IP)', default: (self.followerChoices()[0] || {}).id || '1', choices: self.followerChoices(), allowCustom: true, tooltip: 'Pick the machine by its name. You can also type a machine number (1–8) or an IP.' },
        { type: 'checkbox', id: 'resync', label: 'When activating, snap it to the fleet’s current slide', default: true },
      ],
      callback: async (a) => {
        const m = (await self.parseVariablesInString(String(a.options.machine || ''))).trim()
        const next = !self.followerActiveOf(m)        // flip from the machine's current state
        self.setFollowerActive(m, next, { resync: a.options.resync !== false })
      },
    },

    // ---------------- Slide driver (0.12.53) ----------------
    // Designate a follower as a SLIDE DRIVER: its own USB advancer / Perfect Cue / keystrokes drive the WHOLE
    // fleet by relaying to the Leader, which fans with its normal gated sync (identical reliability to a clicker
    // on the Leader). Turning it on also keeps that follower's capture always armed. Pick the machine from the
    // live dropdown (by name · IP); pair with the "Follower slide driver" feedback for a lit button that both
    // controls and shows state. A follower can never set this on itself — only the Leader or an authorized surface.
    follower_slidedriver_on: {
      name: 'Follower — Slide driver ON (this follower drives the fleet)',
      options: [
        { type: 'dropdown', id: 'machine', label: 'Follower (by name · IP)', default: (self.followerChoices()[0] || {}).id || '1', choices: self.followerChoices(), allowCustom: true, tooltip: 'Pick the machine by its name. You can also type a machine number (1–8) or an IP.' },
      ],
      callback: async (a) => {
        const m = (await self.parseVariablesInString(String(a.options.machine || ''))).trim()
        self.setFollowerSlideDriver(m, true)
      },
    },
    follower_slidedriver_off: {
      name: 'Follower — Slide driver OFF',
      options: [
        { type: 'dropdown', id: 'machine', label: 'Follower (by name · IP)', default: (self.followerChoices()[0] || {}).id || '1', choices: self.followerChoices(), allowCustom: true, tooltip: 'Pick the machine by its name. You can also type a machine number (1–8) or an IP.' },
      ],
      callback: async (a) => {
        const m = (await self.parseVariablesInString(String(a.options.machine || ''))).trim()
        self.setFollowerSlideDriver(m, false)
      },
    },
    follower_slidedriver_toggle: {
      name: 'Follower — Toggle Slide driver',
      options: [
        { type: 'dropdown', id: 'machine', label: 'Follower (by name · IP)', default: (self.followerChoices()[0] || {}).id || '1', choices: self.followerChoices(), allowCustom: true, tooltip: 'Pick the machine by its name. You can also type a machine number (1–8) or an IP.' },
      ],
      callback: async (a) => {
        const m = (await self.parseVariablesInString(String(a.options.machine || ''))).trim()
        self.setFollowerSlideDriver(m, !self.slideDriverOf(m))
      },
    },

    // ---------------- deck control ----------------
    go_deck: {
      name: 'Go deck (full screen at slide 1, hold — no auto-advance)',
      options: [],
      callback: () => self.goDeck(),
    },
    end_show: {
      name: 'End show (stop the roll/cue AND exit the slideshow on all machines)',
      options: [],
      callback: () => self.endShow(),
    },
    flash_test: {
      name: 'Flash test (every machine\'s /flash page paints white at the same instant)',
      options: [],
      callback: () => self.sendCommand({ action: 'flash' }),
    },

    // ---------------- timed cue / loop ----------------
    run_cue: {
      name: 'Run cue (auto-advance with per-slide timing)',
      options: [
        { type: 'number', id: 'defaultDwell', label: 'Default slide timing (seconds)', default: 10, min: 1, max: 86400 },
        { type: 'textinput', id: 'perSlide', label: 'Override default timing per slide (e.g. 7=120, 12=30) — works with builds too', default: '' },
        { type: 'checkbox', id: 'fromStart', label: 'Start from slide 1', default: false },
        { type: 'checkbox', id: 'loop', label: 'Loop deck (restart at the end)', default: false },
        { type: 'checkbox', id: 'builds', label: 'Play in-slide builds & animations (synced steps; assumes identical decks) — DEFAULT', default: true },
      ],
      callback: (a) =>
        self.runCue({
          defaultDwell: a.options.defaultDwell,
          perSlide: a.options.perSlide,
          fromStart: !!a.options.fromStart,
          loop: !!a.options.loop,
          builds: !!a.options.builds,
        }),
    },
    pause_cue: { name: 'Cue: pause', options: [], callback: () => self.pauseCue() },
    resume_cue: { name: 'Cue: resume', options: [], callback: () => self.resumeCue() },
    cancel_cue: { name: 'Cue: cancel', options: [], callback: () => self.cancelCue() },

    // ---------------- KueSync Timer (show timer) ----------------
    // Set & run the fleet-wide show timer from a stream deck. Commands go to the Leader, which fans the
    // timer to every machine (clickers + every full-screen /clock). Pair with the "KueSync Timer status" feedback
    // and the $(kuetimer:kuetimer_time) variable for a live button.
    kuetimer_start: {
      name: 'KueSync Timer: Set & start (countdown)',
      options: [
        { type: 'number', id: 'minutes', label: 'Limit — minutes', default: 5, min: 0, max: 1440 },
        { type: 'number', id: 'seconds', label: 'Limit — plus seconds', default: 0, min: 0, max: 59 },
        { type: 'number', id: 'warn', label: 'Warn at (seconds remaining; 0 = none)', default: 60, min: 0, max: 86400 },
        atZeroOption(),
        zeroColorOption(),
      ],
      callback: (a) => {
        const dur = ((Number(a.options.minutes) || 0) * 60 + (Number(a.options.seconds) || 0)) * 1000
        self.timerCmd({ action: 'set', mode: 'down', durationMs: dur, warnMs: (Number(a.options.warn) || 0) * 1000, endAction: a.options.end || 'blink', zeroColor: a.options.zerocolor || 'red' })
        self.timerCmd({ action: 'start' })
      },
    },
    // 0.12.10: arm the fleet timer WITHOUT starting it (the "Load" on KueSync Control) — sets limit/warn/at-zero+colour.
    kuetimer_arm: {
      name: 'KueSync Timer: Set / arm (do not start)',
      options: [
        { type: 'number', id: 'minutes', label: 'Limit — minutes', default: 5, min: 0, max: 1440 },
        { type: 'number', id: 'seconds', label: 'Limit — plus seconds', default: 0, min: 0, max: 59 },
        { type: 'number', id: 'warn', label: 'Warn at (seconds remaining; 0 = none)', default: 60, min: 0, max: 86400 },
        atZeroOption(),
        zeroColorOption(),
      ],
      callback: (a) => {
        const dur = ((Number(a.options.minutes) || 0) * 60 + (Number(a.options.seconds) || 0)) * 1000
        self.timerCmd({ action: 'set', mode: 'down', durationMs: dur, warnMs: (Number(a.options.warn) || 0) * 1000, endAction: a.options.end || 'blink', zeroColor: a.options.zerocolor || 'red' })
        self.timerCmd({ action: 'reset' })
      },
    },
    // 0.12.10: live-update the limit on a running/armed countdown without a restart (matches KS Control's Limit field).
    // ── 0.12.15 SHOW SURFACE PARITY ──────────────────────────────────────────────────────────────────────
    // These four existed in KueSync Control but had no Companion action at all, which meant a show caller on
    // a Streamdeck could not reach them. Each drives the SAME server function as the Control button.
    advancer_lock: {
      name: 'Lock advancers — kill ALL clicker / USB input fleet-wide',
      options: [
        { type: 'dropdown', id: 'mode', label: 'Action', default: 'toggle',
          choices: [{ id: 'toggle', label: 'Toggle' }, { id: 'on', label: 'Lock' }, { id: 'off', label: 'Release' }] },
      ],
      callback: (a) => {
        const cur = !!(self.primary() && self.primary().state && self.primary().state.advancerLock)
        const m = a.options.mode
        const on = m === 'on' ? true : m === 'off' ? false : !cur
        // Fans to every follower — the lockout is meaningless if it only covers the leader.
        self.showCmd({ type: 'setLocalFlag', key: 'advancerLock', on })
      },
    },
    notes_scroll: {
      name: 'KueSync Notes — scroll the notes screen',
      options: [
        { type: 'dropdown', id: 'dir', label: 'Direction', default: 'down',
          choices: [{ id: 'down', label: 'Scroll down' }, { id: 'up', label: 'Scroll up' },
                    { id: 'top', label: 'Jump to top' }, { id: 'bottom', label: 'Jump to bottom' }] },
      ],
      callback: (a) => {
        const d = a.options.dir
        if (d === 'top' || d === 'bottom') self.showCmd({ type: 'notesCmd', to: d })
        else self.showCmd({ type: 'notesCmd', dir: d })
      },
    },
    presenter_live: {
      name: 'Presenter clicker — make LIVE (or mute all)',
      options: [
        { type: 'textinput', id: 'linkId', label: 'Clicker link id (blank = mute every presenter)', default: '',
          tooltip: 'The link id from Settings > clicker links. Leave blank to take every presenter to standby.' },
      ],
      callback: (a) => self.showCmd({ type: 'linkCmd', linkId: String(a.options.linkId || '').trim() }),
    },
    rundown_advance: {
      name: 'Showflow rundown — next / previous / go to segment',
      options: [
        { type: 'dropdown', id: 'act', label: 'Action', default: 'next',
          choices: [{ id: 'next', label: 'Next segment' }, { id: 'prev', label: 'Previous segment' }, { id: 'go', label: 'Go to segment #' }] },
        { type: 'number', id: 'index', label: 'Segment number (for "Go to")', default: 1, min: 1, max: 500 },
      ],
      callback: (a) => {
        const act = a.options.act
        if (act === 'go') self.showCmd({ type: 'rundownCmd', action: 'go', index: Number(a.options.index) || 1 })
        else self.showCmd({ type: 'rundownCmd', action: act })
      },
    },

    // ── 0.12.15 TIMER CUES ───────────────────────────────────────────────────────────────────────────────
    // The transport, on a Streamdeck. TAKE with no options behaves exactly like TAKE in KueSync Control:
    // it commits whatever is armed. Giving a cue number makes a button that always fires ONE specific cue,
    // which is how a show caller builds a page of named cue buttons.
    cue_take: {
      name: 'Timer Cues: TAKE (put the armed cue on air)',
      options: [
        { type: 'number', id: 'cue', label: 'Cue number (0 = whatever is armed in preview)', default: 0, min: 0, max: 300 },
      ],
      callback: (a) => {
        const n = Number(a.options.cue) || 0
        self.cueCmd(n >= 1 ? { action: 'take', cue: n } : { action: 'take' })
      },
    },
    cue_preview: {
      name: 'Timer Cues: Arm a cue in preview (does not touch what is running)',
      options: [ { type: 'number', id: 'cue', label: 'Cue number', default: 1, min: 1, max: 300 } ],
      callback: (a) => self.cueCmd({ action: 'preview', cue: Number(a.options.cue) || 1 }),
    },
    cue_preview_next: {
      name: 'Timer Cues: Arm the NEXT cue',
      options: [],
      callback: () => self.cueCmd({ action: 'next' }),
    },
    cue_preview_prev: {
      name: 'Timer Cues: Arm the PREVIOUS cue',
      options: [],
      callback: () => self.cueCmd({ action: 'prev' }),
    },
    cue_stop: {
      name: 'Timer Cues: Stop the program (clears on-air, keeps the list)',
      options: [],
      callback: () => self.cueCmd({ action: 'stop' }),
    },
    kuetimer_setlimit: {
      name: 'KueSync Timer: Set limit (live)',
      options: [
        { type: 'number', id: 'minutes', label: 'Limit — minutes', default: 5, min: 0, max: 1440 },
        { type: 'number', id: 'seconds', label: 'Limit — plus seconds', default: 0, min: 0, max: 59 },
      ],
      callback: (a) => {
        const dur = ((Number(a.options.minutes) || 0) * 60 + (Number(a.options.seconds) || 0)) * 1000
        self.timerCmd({ action: 'set', durationMs: dur })
      },
    },
    // 0.12.10: live-update the warn threshold (matches KS Control's Warn-at field — keeps a running timer counting).
    kuetimer_setwarn: {
      name: 'KueSync Timer: Set warn threshold (live)',
      options: [ { type: 'number', id: 'warn', label: 'Warn at (seconds remaining; 0 = none)', default: 60, min: 0, max: 86400 } ],
      callback: (a) => self.timerCmd({ action: 'set', warnMs: (Number(a.options.warn) || 0) * 1000 }),
    },
    // 0.12.10: live-update the at-zero behaviour + colour (matches KS Control's At-zero + Colour dropdowns).
    kuetimer_setend: {
      name: 'KueSync Timer: Set at-zero mode + colour (live)',
      options: [
        atZeroOption(),
        zeroColorOption(),
      ],
      callback: (a) => self.timerCmd({ action: 'set', endAction: a.options.end || 'blink', zeroColor: a.options.zerocolor || 'red' }),
    },
    // 0.12.11: set ONLY the at-zero colour (leaves the at-zero mode untouched) — for a button-per-colour panel.
    kuetimer_setcolor: {
      name: 'KueSync Timer: Set at-zero colour',
      options: [ zeroColorOption() ],
      callback: (a) => self.timerCmd({ action: 'set', zeroColor: a.options.zerocolor || 'red' }),
    },
    // 0.12.11: set ONLY the at-zero behaviour (leaves the colour untouched) — for a button-per-mode panel.
    kuetimer_setmode: {
      name: 'KueSync Timer: Set at-zero behaviour',
      options: [ atZeroOption() ],
      callback: (a) => self.timerCmd({ action: 'set', endAction: a.options.end || 'blink' }),
    },
    // 0.12.11: warning on/off from a button (off = no yellow warning). Pair with the two presets.
    kuetimer_warn_off: { name: 'KueSync Timer: Warning OFF', options: [], callback: () => self.timerCmd({ action: 'set', warnMs: 0 }) },
    // 0.12.10: push a one-line on-screen message to the clock/clickers (matches KS Control's Message field; auto-clears after 30s).
    kuetimer_message: {
      name: 'KueSync Timer: Send on-screen message',
      options: [ { type: 'textinput', id: 'text', label: 'Message (blank = clear)', default: '', useVariables: true } ],
      callback: async (a) => { const text = await self.parseVariablesInString(String(a.options.text || '')); self.timerCmd({ action: 'message', text }) },
    },
    kuetimer_message_clear: { name: 'KueSync Timer: Clear on-screen message', options: [], callback: () => self.timerCmd({ action: 'message', text: '' }) },
    kuetimer_toggle: { name: 'KueSync Timer: Start / Pause (toggle)', options: [], callback: () => self.timerCmd({ action: 'toggle' }) },
    kuetimer_pause: { name: 'KueSync Timer: Pause', options: [], callback: () => self.timerCmd({ action: 'pause' }) },
    kuetimer_resume: { name: 'KueSync Timer: Resume', options: [], callback: () => self.timerCmd({ action: 'start' }) },
    kuetimer_reset: { name: 'KueSync Timer: Reset (back to the set limit)', options: [], callback: () => self.timerCmd({ action: 'reset' }) },
    kuetimer_zero: { name: 'KueSync Timer: Jump to zero', options: [], callback: () => self.timerCmd({ action: 'zero' }) },
    kuetimer_adjust: {
      name: 'KueSync Timer: Add / subtract time on the fly',
      options: [ { type: 'number', id: 'delta', label: 'Seconds (negative subtracts)', default: 60, min: -86400, max: 86400 } ],
      callback: (a) => self.timerCmd({ action: 'adjust', deltaMs: (Number(a.options.delta) || 0) * 1000 }),
    },
    kuetimer_countup: { name: 'KueSync Timer: Count up (from zero)', options: [], callback: () => { self.timerCmd({ action: 'set', mode: 'up' }); self.timerCmd({ action: 'start' }) } },
    kuetimer_tod: { name: 'KueSync Timer: Time of day', options: [
      // 0.12.17: pick the clock format — 12-hour AM/PM (default) or 24-hour "military".
      { type: 'dropdown', id: 'fmt', label: 'Clock format', default: '12', choices: [ { id: '12', label: '12-hour (AM/PM)' }, { id: '24', label: '24-hour (military)' } ] },
    ], callback: (a) => { self.timerCmd({ action: 'set', mode: 'tod', hour12: ((a.options && a.options.fmt) !== '24') }); self.timerCmd({ action: 'start' }) } },

    // 0.12.49.1: one button that TOGGLES the at-zero colour through the five swatches (no more one-button-per-colour).
    kuetimer_cycle_color: { name: 'KueSync Timer: Cycle at-zero colour (single toggling button)', options: [], callback: () => self.cycleTimerColor() },
    // 0.12.49.1: one button that TOGGLES the at-zero behaviour (Blink → Hold → Blank → Overtime).
    kuetimer_cycle_mode: { name: 'KueSync Timer: Cycle at-zero behaviour (single toggling button)', options: [], callback: () => self.cycleTimerEndMode() },

    // ── 0.12.49.1 KUESYNC NOTES SOURCE ───────────────────────────────────────────────────────────────────────
    // Pin which machine's presenter notes feed the whole fleet, and its backup, straight from a Streamdeck —
    // the same setting as KueSync Control's Notes source / backup. Applied via the leader's config (control-
    // password gated); the leader hot-swaps the source at once. Pair with the "Notes source" feedback + variable.
    notes_set_source: {
      name: 'KueSync Notes: Set notes SOURCE (which machine feeds notes)',
      options: [
        { type: 'dropdown', id: 'source', label: 'Notes source', default: '', choices: self.sourceChoices(), allowCustom: true,
          tooltip: 'AUTO = any machine · Leader = this control target · or pick a machine. You can also type an IP.' },
      ],
      callback: async (a) => self.setNotesSource((await self.parseVariablesInString(String(a.options.source || ''))).trim()),
    },
    notes_set_backup: {
      name: 'KueSync Notes: Set BACKUP notes source (failover)',
      options: [
        { type: 'dropdown', id: 'source', label: 'Backup source (blank = none). Cannot equal the primary — the leader ignores that.', default: '', choices: self.sourceChoices(), allowCustom: true },
      ],
      callback: async (a) => self.setNotesBackup((await self.parseVariablesInString(String(a.options.source || ''))).trim()),
    },

    // ── 0.12.49.1 LOOK: backgrounds, chroma text, Kue-Light pips ─────────────────────────────────────────────
    // Live control of the full-screen surfaces' look, from a Streamdeck. Applied via the leader's config; every
    // open /clock, /notes display auto-refreshes within a few seconds. Pair with the matching feedbacks so a
    // button both sets AND shows the current look.
    clock_bg_set: {
      name: 'Look: Timer/Clock background — set colour',
      options: [ { type: 'dropdown', id: 'bg', label: 'Background', default: 'black', choices: [
        { id: 'black', label: 'Black' }, { id: 'white', label: 'White' }, { id: 'green', label: 'Green (chroma)' }, { id: 'blue', label: 'Blue (chroma)' } ] } ],
      callback: (a) => self.setClockBg(a.options.bg || 'black'),
    },
    clock_bg_cycle: { name: 'Look: Timer/Clock background — cycle (black→white→green→blue)', options: [], callback: () => self.cycleClockBg() },
    notes_bg_set: {
      name: 'Look: Notes background — set colour',
      options: [ { type: 'dropdown', id: 'bg', label: 'Background', default: '', choices: [
        { id: '', label: 'Follow the clock background' }, { id: 'black', label: 'Black' }, { id: 'white', label: 'White' }, { id: 'green', label: 'Green (chroma)' }, { id: 'blue', label: 'Blue (chroma)' } ] } ],
      callback: (a) => self.setNotesBg(a.options.bg || ''),
    },
    notes_bg_cycle: { name: 'Look: Notes background — cycle (follow→black→white→green→blue)', options: [], callback: () => self.cycleNotesBg() },
    clock_chroma_toggle: { name: 'Look: Timer/Clock chroma text — toggle white / black', options: [], callback: () => self.toggleClockChroma() },
    notes_chroma_toggle: { name: 'Look: Notes chroma text — toggle white / black', options: [], callback: () => self.toggleNotesChroma() },
    clock_pip_toggle: {
      name: 'Look: Kue-Light pip on the Timer — on / off / toggle',
      options: [ { type: 'dropdown', id: 'mode', label: 'Action', default: 'toggle', choices: [ { id: 'toggle', label: 'Toggle' }, { id: 'on', label: 'On' }, { id: 'off', label: 'Off' } ] } ],
      callback: (a) => { const m = a.options.mode; self.setClockPip(m === 'on' ? true : m === 'off' ? false : !self.lookClockPip()) },
    },
    notes_pip_toggle: {
      name: 'Look: Kue-Light pips on the Notes screen — on / off / toggle',
      options: [ { type: 'dropdown', id: 'mode', label: 'Action', default: 'toggle', choices: [ { id: 'toggle', label: 'Toggle' }, { id: 'on', label: 'On' }, { id: 'off', label: 'Off' } ] } ],
      callback: (a) => { const m = a.options.mode; self.setNotesPip(m === 'on' ? true : m === 'off' ? false : !self.lookNotesPip()) },
    },
    notes_pip_pos: {
      name: 'Look: Notes timer PIP — position',
      options: [ { type: 'dropdown', id: 'pos', label: 'Corner', default: 'tr', choices: [
        { id: 'tl', label: 'Top left' }, { id: 'tc', label: 'Top centre' }, { id: 'tr', label: 'Top right' },
        { id: 'bl', label: 'Bottom left' }, { id: 'bc', label: 'Bottom centre' }, { id: 'br', label: 'Bottom right' } ] } ],
      callback: (a) => self.setNotesPipPos(a.options.pos || 'tr'),
    },

    // ── 0.12.49.2 TIMER CUES — fire by NAME + full authoring (native KueSync Control parity) ─────────────────
    // Fire a cue by its NAME (resolved to the current position at press time, so it survives a reorder). The
    // dropdown auto-populates from the leader's live cue list; you can also type a name or a 1-based position.
    cue_take_named: {
      name: 'Timer Cues: TAKE a cue by name',
      options: [ { type: 'dropdown', id: 'cue', label: 'Cue', default: (self.cueChoices()[0] || {}).id || '', choices: self.cueChoices(), allowCustom: true,
        tooltip: 'Pick a cue by name. You can also type a cue name or a 1-based position. Blank = whatever is armed in preview.' } ],
      callback: async (a) => self.cueTakeSel((await self.parseVariablesInString(String(a.options.cue || ''))).trim()),
    },
    cue_preview_named: {
      name: 'Timer Cues: Arm a cue in preview by name',
      options: [ { type: 'dropdown', id: 'cue', label: 'Cue', default: (self.cueChoices()[0] || {}).id || '', choices: self.cueChoices(), allowCustom: true } ],
      callback: async (a) => self.cuePreviewSel((await self.parseVariablesInString(String(a.options.cue || ''))).trim()),
    },
    // Build the cue list from a Streamdeck — add / edit / delete / reorder / clear / load-a-sheet.
    cue_add: {
      name: 'Timer Cues: Add a cue (name, length, warn, at-zero, speaker)',
      options: [
        { type: 'textinput', id: 'name', label: 'Cue name', default: '', useVariables: true },
        { type: 'number', id: 'minutes', label: 'Length — minutes', default: 5, min: 0, max: 1440 },
        { type: 'number', id: 'seconds', label: 'Length — plus seconds', default: 0, min: 0, max: 59 },
        { type: 'number', id: 'warn', label: 'Warn at (seconds remaining; 0 = none)', default: 60, min: 0, max: 86400 },
        atZeroOption(),
        { type: 'textinput', id: 'speaker', label: 'Speaker (optional)', default: '', useVariables: true },
        { type: 'textinput', id: 'note', label: 'Note (optional)', default: '', useVariables: true },
      ],
      callback: async (a) => self.cueAdd({
        name: await self.parseVariablesInString(String(a.options.name || '')),
        minutes: a.options.minutes, seconds: a.options.seconds, warnSec: Number(a.options.warn) || 0,
        end: a.options.end || 'blink',
        speaker: await self.parseVariablesInString(String(a.options.speaker || '')),
        note: await self.parseVariablesInString(String(a.options.note || '')),
      }),
    },
    cue_update: {
      name: 'Timer Cues: Edit a cue (change length / warn / name / speaker)',
      options: [
        { type: 'dropdown', id: 'cue', label: 'Cue to edit', default: (self.cueChoices()[0] || {}).id || '', choices: self.cueChoices(), allowCustom: true },
        { type: 'textinput', id: 'name', label: 'New name (blank = leave)', default: '', useVariables: true },
        { type: 'number', id: 'minutes', label: 'New length — minutes (0 = leave length)', default: 0, min: 0, max: 1440 },
        { type: 'number', id: 'seconds', label: 'New length — plus seconds', default: 0, min: 0, max: 59 },
        { type: 'number', id: 'warn', label: 'New warn (seconds; -1 = leave)', default: -1, min: -1, max: 86400 },
        { type: 'textinput', id: 'speaker', label: 'New speaker (blank = leave)', default: '', useVariables: true },
      ],
      callback: async (a) => {
        const fields = {}
        const nm = (await self.parseVariablesInString(String(a.options.name || ''))).trim(); if (nm) fields.name = nm
        const dur = ((Number(a.options.minutes) || 0) * 60 + (Number(a.options.seconds) || 0)) * 1000; if (dur > 0) fields.durationMs = dur
        const w = Number(a.options.warn); if (Number.isFinite(w) && w >= 0) fields.warnMs = w * 1000
        const sp = (await self.parseVariablesInString(String(a.options.speaker || ''))).trim(); if (sp) fields.speaker = sp
        self.cueUpdate((await self.parseVariablesInString(String(a.options.cue || ''))).trim(), fields)
      },
    },
    cue_del: {
      name: 'Timer Cues: Delete a cue',
      options: [ { type: 'dropdown', id: 'cue', label: 'Cue', default: (self.cueChoices()[0] || {}).id || '', choices: self.cueChoices(), allowCustom: true } ],
      callback: async (a) => self.cueDel((await self.parseVariablesInString(String(a.options.cue || ''))).trim()),
    },
    cue_move: {
      name: 'Timer Cues: Move a cue to a position',
      options: [
        { type: 'dropdown', id: 'cue', label: 'Cue', default: (self.cueChoices()[0] || {}).id || '', choices: self.cueChoices(), allowCustom: true },
        { type: 'number', id: 'to', label: 'New position (1-based)', default: 1, min: 1, max: 300 },
      ],
      callback: async (a) => self.cueMove((await self.parseVariablesInString(String(a.options.cue || ''))).trim(), a.options.to),
    },
    cue_clear: { name: 'Timer Cues: Clear the whole cue list', options: [], callback: () => self.cueClear() },
    cue_load: {
      name: 'Timer Cues: Load a cue sheet (paste lines; e.g. "Opening 5:00")',
      options: [
        { type: 'textinput', id: 'text', label: 'Cue sheet — one cue per line (name and time; the leader parses it)', default: '', useVariables: true },
        { type: 'dropdown', id: 'mode', label: 'Mode', default: 'replace', choices: [ { id: 'replace', label: 'Replace the current list' }, { id: 'append', label: 'Append to the current list' } ] },
      ],
      callback: async (a) => self.cueLoad(await self.parseVariablesInString(String(a.options.text || '')), a.options.mode || 'replace'),
    },

    // ── 0.12.49.2 BACK-TIMING — hard out at a time-of-day, and warp/pace-to-out ──────────────────────────────
    // The leader resolves the time-of-day ONCE so every monitor counts to the same instant.
    timer_hardout: {
      name: 'KueSync Timer: Hard out — count down to a wall-clock time',
      options: [
        { type: 'textinput', id: 'out', label: 'Out time (e.g. "2:00 PM", "2pm", "14:00")', default: '2:00 PM', useVariables: true },
        { type: 'number', id: 'warn', label: 'Warn at (seconds remaining; 0 = none)', default: 300, min: 0, max: 86400 },
        { type: 'checkbox', id: 'start', label: 'Start immediately', default: true },
      ],
      callback: async (a) => self.setHardOut(await self.parseVariablesInString(String(a.options.out || '')), { warnSec: Number(a.options.warn) || 0, start: a.options.start !== false }),
    },
    timer_warp: {
      name: 'KueSync Timer: Warp — pace the presenter clock to land on the out',
      options: [
        { type: 'dropdown', id: 'to', label: 'Land on', default: 'tod', choices: [ { id: 'tod', label: 'A wall-clock time' }, { id: 'dur', label: 'A real duration' } ] },
        { type: 'textinput', id: 'out', label: 'Out time (when "wall-clock time" — e.g. "2:00 PM")', default: '2:00 PM', useVariables: true },
        { type: 'number', id: 'realmin', label: 'Real minutes (when "a real duration")', default: 55, min: 0, max: 1440 },
        { type: 'number', id: 'minutes', label: 'Presenter sees — minutes (what the speaker’s clock shows)', default: 60, min: 0, max: 1440 },
        { type: 'number', id: 'seconds', label: 'Presenter sees — plus seconds', default: 0, min: 0, max: 59 },
        { type: 'checkbox', id: 'keep', label: 'Seamless — bend a RUNNING clock without a jump (do not restart)', default: false },
      ],
      callback: async (a) => self.setWarp({
        to: a.options.to || 'tod',
        outStr: await self.parseVariablesInString(String(a.options.out || '')),
        realMin: Number(a.options.realmin) || 0,
        minutes: a.options.minutes, seconds: a.options.seconds,
        keep: !!a.options.keep,
      }),
    },

    // ---------------- licensing ----------------
    revalidate: {
      name: 'Re-validate license',
      options: [],
      callback: () => { self.license.setKey(self.config.licenseKey); self.refreshLicenseUi() },
    },
  }
}
