// Global prompt modal — a single dialog mounted in base.html, opened from
// anywhere via a window CustomEvent:
//
//   $dispatch('bb-prompt-modal', {
//     mode: 'preview' | 'editable',   // default 'preview'
//     edit: true,                      // editable: start in the editor (else preview)
//     title: 'Custom workflow prompt',
//     subtitle: 'Markdown — runs verbatim on every run.',
//     value: '# the markdown',         // initial text (else read from target)
//     target: '#prompt-field' | el,    // editable: where Save writes the value
//     onSaved: (v) => {},              // editable: optional callback after save
//     saveLabel: 'Save prompt',
//   })
//
// Two modes:
//   - preview  : read-only render of `value` + Copy + Close.
//   - editable : Edit/Preview toggle, ⌘/Ctrl+Enter (or Save) writes `value`
//                back into `target` (and/or calls onSaved), then closes.
//
// The preview renders server-side (POST /-/markdown/preview → goldmark +
// bluemonday) so there is no client-side Markdown parser; CSRF is injected by
// the global fetch wrapper in base.html.
document.addEventListener('alpine:init', function () {
  Alpine.data('bbPromptModal', function () {
    return {
      mode: 'preview',
      view: 'preview', // 'edit' | 'preview'
      title: '',
      subtitle: '',
      value: '',
      initialValue: '', // buffer at open; drives the dirty check
      loading: false,
      copied: false,
      expanded: false, // desktop expand toggle (larger modal)
      confirmingDiscard: false, // in-modal "discard unsaved edits?" overlay
      saveLabel: 'Save',
      _targetEl: null,
      _onSaved: null,
      _seq: 0, // guards against out-of-order render responses

      // dirty is true once the buffer diverges from what we opened with.
      // The primary button reads "Save" when dirty, "Done" when not.
      get dirty() {
        return this.value !== this.initialValue;
      },

      open: function (detail) {
        detail = detail || {};
        this.mode = detail.mode === 'editable' ? 'editable' : 'preview';
        this.title = detail.title || 'Prompt';
        this.subtitle = detail.subtitle || '';
        this.saveLabel = detail.saveLabel || 'Save';
        this.copied = false;
        this.expanded = false; // always open at the default size
        this.confirmingDiscard = false;

        // Resolve the target field first so we can fall back to its value.
        this._targetEl = null;
        if (detail.target) {
          this._targetEl =
            typeof detail.target === 'string'
              ? document.querySelector(detail.target)
              : detail.target;
        }
        if (detail.value != null) {
          this.value = String(detail.value);
        } else if (this._targetEl && 'value' in this._targetEl) {
          this.value = this._targetEl.value || '';
        } else {
          this.value = '';
        }
        this.initialValue = this.value;
        this._onSaved = typeof detail.onSaved === 'function' ? detail.onSaved : null;
        // Optional pre-rendered, sanitized HTML for the first preview render.
        // Callers that already have server-rendered markdown (e.g. the
        // workflow prompt endpoint returns prompt_html alongside the raw
        // prompt) pass it here so we skip the /-/markdown/preview round-trip.
        // Cleared after one use; toggling Edit→Preview re-renders from value.
        this._prerenderedHTML = detail.html != null ? String(detail.html) : '';

        // Editable opens in the editor when asked, else in preview; preview-only
        // is always the rendered view.
        this.view = this.mode === 'editable' && detail.edit ? 'edit' : 'preview';

        var self = this;
        this.$nextTick(function () {
          // $root is the component root (the <dialog>); $el would be whatever
          // element invoked the handler.
          if (self.$root && self.$root.showModal && !self.$root.open) self.$root.showModal();
          if (self.view === 'preview') self.renderPreview();
          else self.focusEditor();
        });
      },

      close: function () {
        this.confirmingDiscard = false;
        if (this.$root && this.$root.close && this.$root.open) this.$root.close();
      },

      // Every dismissal path (Esc/cancel event, header ✕, footer Cancel,
      // backdrop click) funnels here so we can gate on focus + unsaved edits.
      requestClose: function () {
        // If the discard confirmation is up, Esc/cancel backs out of it.
        if (this.confirmingDiscard) {
          this.confirmingDiscard = false;
          return;
        }
        // First Esc while the editor is focused just blurs it — a second Esc
        // (editor no longer focused) actually dismisses. Match any focused
        // field inside the dialog rather than strict ref identity, so this is
        // robust to ref-resolution timing across browsers.
        var ae = document.activeElement;
        if (ae && this.$root && this.$root.contains(ae) &&
            (ae.tagName === 'TEXTAREA' || ae.tagName === 'INPUT')) {
          ae.blur();
          return;
        }
        // Unsaved edits → confirm before discarding (in-modal, since this
        // dialog is in the top layer above any z-index overlay).
        if (this.dirty) {
          this.confirmingDiscard = true;
          return;
        }
        this.close();
      },

      // The bottom-right primary action: persist when there are edits, else
      // just dismiss ("Done"). Bound to the button and ⌘/Ctrl+Enter.
      primary: function () {
        if (this.mode !== 'editable') return;
        if (this.dirty) this.save();
        else this.close();
      },

      setView: function (v) {
        if (this.view === v) return;
        this.view = v;
        if (v === 'preview') this.renderPreview();
        else this.focusEditor();
      },

      focusEditor: function () {
        var self = this;
        this.$nextTick(function () {
          var t = self.$refs.editor;
          if (t) t.focus();
        });
      },

      // Render the current buffer to sanitized HTML server-side and inject it.
      renderPreview: function () {
        var self = this;
        // Fast path: a caller handed us already-sanitized HTML for the first
        // render — inject it directly and skip the network round-trip.
        if (self._prerenderedHTML) {
          var html = self._prerenderedHTML;
          self._prerenderedHTML = '';
          self._seq++; // invalidate any render still in flight
          self.loading = false;
          self.$nextTick(function () {
            var el = self.$refs.previewBody;
            if (!el) return;
            el.innerHTML = html;
            if (window.lucide && typeof window.lucide.createIcons === 'function') {
              window.lucide.createIcons();
            }
            el.scrollTop = 0;
          });
          return;
        }
        var seq = ++self._seq;
        self.loading = true;
        fetch('/-/markdown/preview', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ text: self.value || '' }),
        })
          .then(function (r) {
            return r.ok ? r.json() : Promise.reject(r.status);
          })
          .then(function (data) {
            if (seq !== self._seq) return; // a newer render superseded this one
            self.loading = false;
            self.$nextTick(function () {
              var el = self.$refs.previewBody;
              if (!el) return;
              el.innerHTML = (data && data.html) || '';
              // Render <i data-lucide> placeholders the markdown emitted
              // (callout / code-copy icons).
              if (window.lucide && typeof window.lucide.createIcons === 'function') {
                window.lucide.createIcons();
              }
              el.scrollTop = 0;
            });
          })
          .catch(function () {
            if (seq !== self._seq) return;
            self.loading = false;
            self.$nextTick(function () {
              var el = self.$refs.previewBody;
              if (el) el.innerHTML = '<p>Could not render the preview. Please try again.</p>';
            });
          });
      },

      // Copy the raw Markdown source (not the rendered HTML).
      copy: function () {
        var self = this;
        var text = self.value || '';
        if (!text) return;
        var done = function () {
          self.copied = true;
          setTimeout(function () {
            self.copied = false;
          }, 1500);
        };
        if (navigator.clipboard && navigator.clipboard.writeText) {
          navigator.clipboard.writeText(text).then(done).catch(function () {
            self._copyFallback(text, done);
          });
        } else {
          self._copyFallback(text, done);
        }
      },

      _copyFallback: function (text, done) {
        try {
          var ta = document.createElement('textarea');
          ta.value = text;
          ta.style.position = 'fixed';
          ta.style.opacity = '0';
          document.body.appendChild(ta);
          ta.select();
          document.execCommand('copy');
          document.body.removeChild(ta);
          done();
        } catch (e) {
          console.error('prompt-modal copy fallback failed', e);
        }
      },

      // Write the buffer back to the originating field and/or fire the
      // callback, then close. No-op in preview-only mode.
      save: function () {
        if (this.mode !== 'editable') return;
        var v = this.value || '';
        if (this._targetEl && 'value' in this._targetEl) {
          this._targetEl.value = v;
          // Let Alpine x-model / form listeners on the target react.
          this._targetEl.dispatchEvent(new Event('input', { bubbles: true }));
          this._targetEl.dispatchEvent(new Event('change', { bubbles: true }));
        }
        if (this._onSaved) {
          try {
            this._onSaved(v);
          } catch (e) {
            console.error('prompt-modal onSaved failed', e);
          }
        }
        this.close();
      },
    };
  });
});
