/**
 * UIFormElement — Form-participating base class.
 *
 * Extends UIElement with ElementInternals form participation,
 * constraint validation (required, pattern, minlength, maxlength),
 * and auto-validation UX (submit, blur, input re-validation).
 *
 * Usage:
 *   import { UIFormElement } from './form.js';
 *
 *   class MyField extends UIFormElement {
 *     static properties = {
 *       ...UIFormElement.properties,
 *       placeholder: { type: String, default: '', reflect: true },
 *     };
 *   }
 *
 * Auto-validation lifecycle:
 *   - On form submit: browser fires `invalid` → auto-sets `error` attr
 *   - On input (if currently invalid): re-validates to clear as soon as valid
 *   - On blur (if dirty): validates
 *   - On form reset: clears all validation state
 *
 * Custom error messages via data attributes:
 *   data-msg-required="Please enter your name"
 *   data-msg-pattern="Invalid format"
 *   data-msg-minlength="Too short"
 *   data-msg-maxlength="Too long"
 *
 * Manual control:
 *   el.validate()          → returns boolean, surfaces error
 *   el.setInvalid('msg')   → custom error
 *   el.setValid()          → clear error
 *   el.syncValue(val)      → update form value + run constraints
 */

import { UIElement } from './element.js';

export class UIFormElement extends UIElement {
  static formAssociated = true;

  static get properties() {
    return {
      name:      { type: String,  default: '', reflect: true },
      value:     { type: String,  default: '', reflect: true },
      disabled:  { type: Boolean, default: false, reflect: true },
      required:  { type: Boolean, default: false, reflect: true },
      readonly:  { type: Boolean, default: false, reflect: true },
      error:     { type: String,  default: '', reflect: true },
      hint:      { type: String,  default: '', reflect: true },
      pattern:   { type: String,  default: '', reflect: true },
      minlength: { type: Number,  default: null, reflect: true },
      maxlength: { type: Number,  default: null, reflect: true },
      // §220 (v0.5.9, FEEDBACK-14 §3): declarative trailing-debounce on the
      // `input` event. When `throttle > 0`, `scheduleThrottledInput()`
      // collapses bursts of value changes to one trailing emission at
      // `quiet+throttle` ms. `change` continues to fire unthrottled. Use
      // `flushPendingInput()` from `change` / blur paths to guarantee the
      // trailing `input` fires before `change`. Default `0` keeps the
      // synchronous behavior for non-opted-in consumers. Pattern landed
      // for `<slider-ui>` in v0.5.5 §184; this graduates it to the shared
      // base so `<input-ui>`, `<textarea-ui>`, `<slider-ui>` all share
      // the same semantics + parity for consumer code.
      throttle:  { type: Number,  default: 0, reflect: true },
    };
  }

  #dirty = false;
  // §220 (v0.5.9): shared trailing-debounce timer + pending value for the
  // `throttle` property. See `scheduleThrottledInput()` + `flushPendingInput()`.
  #inputTimer = null;
  #pendingValue = undefined;

  // ── ElementInternals accessors ──

  get form() { return this.internals.form; }
  get labels() { return this.internals.labels; }
  get validity() { return this.internals.validity; }
  get validationMessage() { return this.internals.validationMessage; }
  get willValidate() { return this.internals.willValidate; }

  checkValidity() { return this.internals.checkValidity(); }
  reportValidity() { return this.internals.reportValidity(); }

  // ── Value sync ──

  /** Update the form value and run constraint validation. */
  syncValue(val) {
    val = val ?? this.value ?? '';
    this.internals.setFormValue(val);
    this.#runConstraints(val);
  }

  // ── Manual validation control ──

  /** Clear all validation errors. */
  setValid() {
    this.internals.setValidity({});
    this.removeAttribute('aria-invalid');
    if (this.error) this.error = '';
  }

  /** Set a custom validation error. */
  setInvalid(message, anchor) {
    this.internals.setValidity({ customError: true }, message || 'Invalid', anchor || this);
    this.setAttribute('aria-invalid', 'true');
    this.error = message || 'Invalid';
  }

  /** Run validation and surface errors. Returns boolean. */
  validate() {
    const valid = this.#runConstraints(this.value ?? '');
    if (!valid) {
      this.setAttribute('aria-invalid', 'true');
      this.error = this.validationMessage;
    } else {
      this.removeAttribute('aria-invalid');
      if (this.error) this.error = '';
    }
    return valid;
  }

  // ── §220 (v0.5.9): shared throttle helpers ──
  //
  // Subclasses with `throttle > 0` get trailing-debounced `input`
  // dispatch + a synchronous flush API. Pattern source: v0.5.5 §184
  // slider; graduated to the base in v0.5.9 §220 for parity across
  // `<input-ui>`, `<textarea-ui>`, `<slider-ui>`.
  //
  // Usage in a subclass:
  //   #onInput = () => {
  //     this.value = this.#textEl.textContent;
  //     this.scheduleThrottledInput();
  //   };
  //   #onCommit = () => {
  //     this.flushPendingInput();
  //     this.dispatchEvent(new CustomEvent('change', {...}));
  //   };

  /**
   * Schedule (or immediately dispatch) a trailing-debounced `input`
   * CustomEvent. When `this.throttle > 0`, the dispatch is deferred and
   * collapsed across calls within a `throttle`-ms window. When 0, fires
   * synchronously (legacy behavior).
   *
   * @param {Object} [detail] — optional detail override. Defaults to
   *   `{ value: this.value }`.
   */
  scheduleThrottledInput(detail = null) {
    const t = Number(this.throttle) || 0;
    const event = () => new CustomEvent('input', {
      bubbles: true,
      detail: detail || { value: this.value },
    });
    if (t > 0) {
      if (this.#inputTimer != null) clearTimeout(this.#inputTimer);
      this.#pendingValue = this.value;
      this.#inputTimer = setTimeout(() => {
        this.#inputTimer = null;
        this.#pendingValue = undefined;
        this.dispatchEvent(event());
      }, t);
      return;
    }
    this.dispatchEvent(event());
  }

  /**
   * Synchronously flush a pending throttled `input` dispatch. No-op when
   * no timer is pending. Call this from `change` / blur / explicit-commit
   * paths so consumers see the trailing `input` BEFORE the commit.
   *
   * @param {Object} [detail] — optional detail override.
   */
  flushPendingInput(detail = null) {
    if (this.#inputTimer == null) return;
    clearTimeout(this.#inputTimer);
    this.#inputTimer = null;
    this.#pendingValue = undefined;
    this.dispatchEvent(new CustomEvent('input', {
      bubbles: true,
      detail: detail || { value: this.value },
    }));
  }

  /**
   * Drop any pending throttled `input` dispatch without firing. Called
   * automatically on `disconnected()` to avoid late dispatches after
   * teardown. Subclasses overriding `disconnected()` should `super.disconnected()`.
   */
  dropPendingInput() {
    if (this.#inputTimer == null) return;
    clearTimeout(this.#inputTimer);
    this.#inputTimer = null;
    this.#pendingValue = undefined;
  }

  // ── Constraint engine ──

  #runConstraints(val) {
    val = val ?? this.value ?? '';
    const str = String(val);

    if (this.required && !str.trim()) {
      this.internals.setValidity(
        { valueMissing: true },
        this.getAttribute('data-msg-required') || 'This field is required.',
        this,
      );
      return false;
    }

    if (this.pattern && str) {
      try {
        if (!new RegExp(`^(?:${this.pattern})$`).test(str)) {
          this.internals.setValidity(
            { patternMismatch: true },
            this.getAttribute('data-msg-pattern') || 'Please match the requested format.',
            this,
          );
          return false;
        }
      } catch {}
    }

    if (this.minlength != null && this.minlength > 0 && str.length > 0 && str.length < this.minlength) {
      this.internals.setValidity(
        { tooShort: true },
        this.getAttribute('data-msg-minlength') || `Please use at least ${this.minlength} characters.`,
        this,
      );
      return false;
    }

    if (this.maxlength != null && this.maxlength > 0 && str.length > this.maxlength) {
      this.internals.setValidity(
        { tooLong: true },
        this.getAttribute('data-msg-maxlength') || `Please use no more than ${this.maxlength} characters.`,
        this,
      );
      return false;
    }

    this.internals.setValidity({});
    return true;
  }

  // ── Auto-validation listeners ──

  #onInvalid = (e) => {
    e.preventDefault();
    this.setAttribute('aria-invalid', 'true');
    this.error = this.validationMessage;
  };

  #onInput = () => {
    this.#dirty = true;
    if (this.error) this.validate();
  };

  #onBlur = () => {
    if (this.#dirty) this.validate();
  };

  connected() {
    super.connected();
    this.syncValue();
    this.addEventListener('invalid', this.#onInvalid);
    this.addEventListener('input', this.#onInput);
    this.addEventListener('blur', this.#onBlur, true);
    this.#warnDeprecatedLabel();
  }

  /**
   * Soft-deprecation warning for the per-control `label` attribute.
   * Wrapping the control in `<field-ui label="…">` (Phase 1 shipped
   * 2026-04-23) gives proper `<label for>` association and composition
   * slots; the per-control `label` attr renders via an inert shadow
   * slot without `[for]`, so click-to-focus doesn't work.
   *
   * Subclasses that have promoted `label` to a first-class API with
   * proper a11y wiring (e.g. input-ui's inline-leading caption with
   * aria-labelledby) opt out by setting `static labelDeprecated = false`.
   *
   * One-shot per class so the console isn't spammed on docs pages.
   */
  #warnDeprecatedLabel() {
    const ctor = this.constructor;
    if (ctor.labelDeprecated === false) return;
    if (!ctor.properties?.label) return;
    if (!this.hasAttribute('label')) return;
    if (UIFormElement.#labelWarned.has(ctor)) return;
    UIFormElement.#labelWarned.add(ctor);
    // eslint-disable-next-line no-console
    console.warn(
      `[AdiaUI] <${this.localName} label="…"> is deprecated — wrap in ` +
      `<field-ui label="…"> for proper label association. ` +
      `Docs: https://ui-kit.exe.xyz/site/components/field`
    );
  }

  static #labelWarned = new Set();

  disconnected() {
    super.disconnected();
    this.removeEventListener('invalid', this.#onInvalid);
    this.removeEventListener('input', this.#onInput);
    this.removeEventListener('blur', this.#onBlur, true);
    // §220 (v0.5.9): drop any pending throttled `input` dispatch — the
    // element is gone; no late emissions.
    this.dropPendingInput();
  }

  // ── Form lifecycle callbacks ──

  formResetCallback() {
    this.#dirty = false;
    this.setValid();
    this.onFormReset();
  }

  formDisabledCallback(disabled) {
    this.disabled = disabled;
    this.onFormDisabled(disabled);
  }

  formAssociatedCallback(form) {
    this.onFormAssociated(form);
  }

  formStateRestoreCallback(state, reason) {
    this.onFormStateRestore(state, reason);
  }

  // ── Override points ──

  onFormReset() {}
  onFormDisabled(_disabled) {}
  onFormAssociated(_form) {}
  onFormStateRestore(_state, _reason) {}
}
