/**
 * `<inline-edit-ui>` — click-to-edit text in place.
 *
 * Static state: textContent reads as plain text with a hover affordance.
 * Editing state: host becomes `contenteditable=plaintext-only`. Enter or
 * blur commits (configurable via `commit`); Escape always cancels.
 *
 * Form-participating via UIFormElement (name/value/required/disabled/
 * readonly inherited). Pair with a `<form>` + `name=` to submit edits
 * as a field.
 *
 * Architecture: no template (stamp-nothing). The element manages its
 * own DOM imperatively — textContent IS the value, contenteditable
 * attribute IS the editing state. This keeps the element trivial and
 * lets the inherited UIFormElement value sync stay authoritative.
 */

import { UIFormElement } from '../../core/form.js';

export class UIInlineEdit extends UIFormElement {
  static properties = {
    ...UIFormElement.properties,
    placeholder: { type: String,  default: 'Click to edit', reflect: true },
    editing:     { type: Boolean, default: false,           reflect: true },
    commit:      { type: String,  default: 'blur',          reflect: true }, // blur | enter | manual
  };

  static template = () => null;

  #originalValue = '';
  #bound = false;
  #suppressBlur = false;

  connected() {
    super.connected();
    if (!this.hasAttribute('role')) this.setAttribute('role', 'textbox');
    if (!this.hasAttribute('tabindex') && !this.disabled) {
      this.setAttribute('tabindex', '0');
    }
    // Reflect the initial value to textContent (the source of truth for
    // display while not editing). Setting via property triggers the
    // reflection but not the textContent sync — do it explicitly.
    if (this.value && this.textContent.trim() === '') {
      this.textContent = this.value;
    } else if (!this.value && this.textContent.trim()) {
      // Author supplied text in the slot — treat it as the initial value.
      this.value = this.textContent.trim();
      this.syncValue();
    }

    if (!this.#bound) {
      this.#bound = true;
      this.addEventListener('click',    this.#onClick);
      this.addEventListener('keydown',  this.#onKeydown);
      this.addEventListener('focus',    this.#onFocus);
      this.addEventListener('blur',     this.#onBlur);
    }
  }

  disconnected() {
    super.disconnected();
    this.removeEventListener('click',    this.#onClick);
    this.removeEventListener('keydown',  this.#onKeydown);
    this.removeEventListener('focus',    this.#onFocus);
    this.removeEventListener('blur',     this.#onBlur);
    this.#bound = false;
  }

  /* ── Public API ──────────────────────────────────────────────────── */

  startEdit() {
    if (this.editing || this.disabled || this.readonly) return;
    this.#originalValue = this.value || '';
    this.editing = true;
    this.setAttribute('contenteditable', 'plaintext-only');
    this.dispatchEvent(new CustomEvent('edit-start', { bubbles: true }));
    // Defer focus/selection — setting contenteditable in the same tick
    // as a click can race with the browser's own caret placement.
    queueMicrotask(() => {
      this.focus();
      this.#selectAll();
    });
  }

  commitEdit() {
    if (!this.editing) return;
    const newValue = (this.textContent || '').trim();
    const oldValue = this.#originalValue;
    this.editing = false;
    this.removeAttribute('contenteditable');
    // Keep the visible text in sync with the canonical value (normalizes
    // trailing whitespace + restores the source-of-truth string).
    if (this.textContent !== newValue) this.textContent = newValue;
    if (newValue !== oldValue) {
      this.value = newValue;
      this.syncValue(newValue);
      this.dispatchEvent(new CustomEvent('change', {
        bubbles: true,
        detail: { value: newValue, oldValue },
      }));
    }
    this.dispatchEvent(new CustomEvent('edit-end', {
      bubbles: true,
      detail: { committed: true },
    }));
  }

  cancelEdit() {
    if (!this.editing) return;
    this.editing = false;
    this.removeAttribute('contenteditable');
    this.textContent = this.#originalValue;
    this.dispatchEvent(new CustomEvent('cancel', { bubbles: true }));
    this.dispatchEvent(new CustomEvent('edit-end', {
      bubbles: true,
      detail: { committed: false },
    }));
  }

  /* ── Event handlers ──────────────────────────────────────────────── */

  #onClick = (e) => {
    if (this.disabled || this.readonly) return;
    if (this.editing) return; // already editing, let caret behavior pass through
    e.preventDefault();
    this.startEdit();
  };

  #onKeydown = (e) => {
    if (this.editing) {
      if (e.key === 'Enter') {
        e.preventDefault();
        this.#suppressBlur = true; // commitEdit blurs us; don't recurse
        this.commitEdit();
        this.blur();
      } else if (e.key === 'Escape') {
        e.preventDefault();
        this.#suppressBlur = true;
        this.cancelEdit();
        this.blur();
      }
    } else {
      // Activate edit from keyboard (Enter or Space)
      if (e.key === 'Enter' || e.key === ' ') {
        if (this.disabled || this.readonly) return;
        e.preventDefault();
        this.startEdit();
      }
    }
  };

  #onFocus = () => {
    // No-op — keyboard activation handled in #onKeydown to distinguish
    // focus-via-Tab (don't edit) from focus-via-click (also don't edit
    // here; the click handler does that). Focus alone never starts an
    // edit — that would surprise tab-traversers.
  };

  #onBlur = () => {
    if (!this.editing) return;
    if (this.#suppressBlur) {
      this.#suppressBlur = false;
      return;
    }
    if (this.commit === 'blur') {
      this.commitEdit();
    } else if (this.commit === 'enter') {
      // blur cancels under `commit=enter` semantics
      this.cancelEdit();
    }
    // commit=manual: do nothing on blur; consumer must call commitEdit()
  };

  /* ── Helpers ─────────────────────────────────────────────────────── */

  #selectAll() {
    const range = document.createRange();
    range.selectNodeContents(this);
    const sel = window.getSelection();
    if (!sel) return;
    sel.removeAllRanges();
    sel.addRange(range);
  }
}
