import { defineTrait } from '../define.js';

/**
 * Built-in named patterns. User-supplied patterns override these.
 *
 * `#` matches a digit, `A` matches a letter, `*` matches any character.
 * Anything else is a literal mask character that gets injected into the
 * formatted output as the user types past it.
 */
const NAMED_PATTERNS = {
  'phone-us':    '(###) ###-####',
  'phone-intl':  '+## ### ### ####',
  'credit-card': '#### #### #### ####',
  'date-iso':    '####-##-##',
  'time-24h':    '##:##',
  'cvv':         '####',
};

/**
 * Test whether `c` satisfies the placeholder `slot` (`#`, `A`, `*`).
 * Anything else in the pattern is treated as a literal — never a slot.
 */
function matchesSlot(slot, c) {
  if (slot === '#') return /\d/.test(c);
  if (slot === 'A') return /[A-Za-z]/.test(c);
  if (slot === '*') return c.length === 1;
  return false;
}

function isPlaceholder(ch) {
  return ch === '#' || ch === 'A' || ch === '*';
}

/**
 * Resolve the user's `data-mask-pattern` attribute into a literal pattern
 * string. Named shortcuts (`credit-card`, `phone-us`, …) expand to their
 * literal form; everything else is taken verbatim.
 */
function resolvePattern(value) {
  if (!value) return '';
  if (Object.prototype.hasOwnProperty.call(NAMED_PATTERNS, value)) {
    return NAMED_PATTERNS[value];
  }
  return value;
}

/**
 * Walk `raw` (the user-entered chars, mask literals already stripped) and
 * apply `pattern` slot-by-slot. Stops emitting once we run out of raw
 * input — the trailing literals don't get echoed until the next real
 * char arrives, otherwise the cursor would land *after* a separator on
 * every keystroke ("(415"  → "(415) ", caret at end is awkward).
 *
 * Returns `{ formatted, complete, consumed }` where `complete` is true
 * when every placeholder slot was filled and `consumed` is the count of
 * raw chars actually placed (`raw` may contain more chars than the
 * pattern can hold — for cvv:`####`, raw="12345" → consumed=4).
 */
function applyPattern(raw, pattern) {
  let out = '';
  let i = 0;
  let p = 0;
  let placeholderSlots = 0;
  let placeholderFilled = 0;

  for (let k = 0; k < pattern.length; k++) {
    if (isPlaceholder(pattern[k])) placeholderSlots++;
  }

  while (p < pattern.length && i < raw.length) {
    const slot = pattern[p];
    if (isPlaceholder(slot)) {
      const c = raw[i];
      if (matchesSlot(slot, c)) {
        out += c;
        placeholderFilled++;
        i++;
        p++;
      } else {
        // Skip a non-matching raw char (digit-only mask + user typed a
        // letter). Do not advance `p` — keep looking for a valid char.
        i++;
      }
    } else {
      out += slot;
      p++;
    }
  }

  return {
    formatted: out,
    complete: placeholderSlots > 0 && placeholderFilled === placeholderSlots,
    consumed: i,
  };
}

/**
 * Strip every literal mask char from `formatted` to recover the raw
 * sequence the user typed (just placeholder chars). The pattern is the
 * authority on which positions are literals — we walk both strings in
 * lockstep and copy only chars that landed in placeholder slots.
 *
 * Once the pattern is exhausted, any remaining chars in `formatted` are
 * treated as raw input the user typed past the mask boundary — they get
 * captured so a subsequent re-format can decide whether to drop them
 * (over-budget for a fixed-width mask like cvv) or keep them.
 */
function stripPattern(formatted, pattern) {
  let raw = '';
  let p = 0;
  let i = 0;
  for (; i < formatted.length && p < pattern.length; i++) {
    const slot = pattern[p];
    const c = formatted[i];
    if (isPlaceholder(slot)) {
      raw += c;
      p++;
    } else if (slot === c) {
      // mask literal lined up — skip in raw, advance pattern
      p++;
    } else {
      // Diverged from pattern (user typed a literal that wasn't there).
      // Best-effort: keep raw char anyway so subsequent reformatting
      // doesn't drop user input on the floor.
      raw += c;
    }
  }
  // Trailing chars past the pattern's reach — user's raw input that
  // hasn't been mapped onto a slot yet. Keep them; applyPattern will
  // decide whether the mask has room for them.
  if (i < formatted.length) {
    raw += formatted.slice(i);
  }
  return raw;
}

/**
 * Count "real" (user-entered, non-mask) characters before the given
 * cursor position in `formatted`. Used to translate the cursor from
 * the pre-write value into the post-write value, keeping it anchored
 * to the same character the user just touched.
 *
 * Handles three cases at each position:
 * - Pattern slot is a placeholder → count this char as real.
 * - Pattern literal aligns with the char → it's a mask separator, skip.
 * - Pattern literal does NOT align → user is typing the unformatted
 *   raw value (first input), so count it as real.
 */
function realCharsBefore(formatted, pattern, caret) {
  let count = 0;
  let p = 0;
  const limit = Math.min(caret, formatted.length);
  for (let i = 0; i < limit; i++) {
    const c = formatted[i];
    if (p < pattern.length) {
      const slot = pattern[p];
      if (isPlaceholder(slot)) {
        count++;
        p++;
        continue;
      }
      if (slot === c) {
        // Mask literal — skip without counting.
        p++;
        continue;
      }
    }
    // Either pattern is exhausted or the literal doesn't align: this
    // char is raw input the user typed.
    count++;
  }
  return count;
}

/**
 * Find the position in `formatted` after `n` placeholder slots have been
 * filled. The mirror of `realCharsBefore` — used to set the caret on the
 * post-write string.
 */
function positionAfterRealChars(formatted, pattern, n) {
  if (n <= 0) return 0;
  let count = 0;
  let p = 0;
  for (let i = 0; i < formatted.length && p < pattern.length; i++) {
    const slot = pattern[p];
    if (isPlaceholder(slot)) {
      count++;
      p++;
      if (count === n) return i + 1;
    } else {
      p++;
    }
  }
  return formatted.length;
}

export const inputMask = defineTrait({
  name: 'input-mask',
  category: 'forms-data',
  description: 'Locale-aware as-you-type formatter (phone, credit-card, date, currency)',
  attributes: ['data-input-mask-active', 'data-input-mask-complete'],
  events: ['mask-commit'],
  config: ['data-mask-pattern', 'data-mask-strip-on-commit'],
  setup({ host }) {
    function readPattern() {
      return resolvePattern(host.getAttribute('data-mask-pattern') || '');
    }

    function readValue() {
      return host.value ?? host.getAttribute('value') ?? '';
    }

    function writeValue(value) {
      // Assigning `host.value` works for native <input> and any UIElement
      // that reflects a `value` property; fall back to setAttribute
      // otherwise. We don't dispatch a synthetic input event — the
      // trait must not feed itself.
      if ('value' in host) {
        host.value = value;
      } else {
        host.setAttribute('value', value);
      }
    }

    function findInternalInput() {
      // For <input-ui> and similar AdiaUI form primitives, the real
      // contenteditable surface is a nested <input>. Use it for caret
      // selection when present.
      if (host.tagName === 'INPUT' || host.tagName === 'TEXTAREA') return host;
      const inner = host.querySelector?.('input, textarea');
      return inner || null;
    }

    function getCaret() {
      const target = findInternalInput();
      if (!target) return null;
      try {
        if (target.selectionStart != null) return target.selectionStart;
      } catch {
        // Some input types (number, email) throw on selectionStart access.
        return null;
      }
      return null;
    }

    function setCaret(pos) {
      const target = findInternalInput();
      if (!target) return;
      try {
        target.setSelectionRange(pos, pos);
      } catch {
        // Same input-type throw class as getCaret — silently bail.
      }
    }

    function reformat() {
      const pattern = readPattern();
      if (!pattern) {
        host.removeAttribute('data-input-mask-active');
        host.removeAttribute('data-input-mask-complete');
        return;
      }

      const before = readValue();
      const beforeCaret = getCaret();

      // Recover the raw input by stripping the current pattern, then
      // reapply it. This handles mid-string edits: the user inserted a
      // digit at position 3 of "(415) 555-1234" → strip → reapply.
      const raw = stripPattern(before, pattern);
      const realBeforeCaret = beforeCaret == null
        ? raw.length
        : realCharsBefore(before, pattern, beforeCaret);

      const { formatted, complete } = applyPattern(raw, pattern);

      host.setAttribute('data-input-mask-active', '');
      if (complete) host.setAttribute('data-input-mask-complete', '');
      else host.removeAttribute('data-input-mask-complete');

      // Only write back if the formatted value actually differs — this
      // lets the trait coexist with other input listeners without
      // double-firing.
      if (formatted !== before) {
        writeValue(formatted);
        if (beforeCaret != null) {
          const newPos = positionAfterRealChars(formatted, pattern, realBeforeCaret);
          // Defer caret-set: writing to host.value can move the caret to
          // the end on some browsers; we need to clobber that move.
          queueMicrotask(() => setCaret(newPos));
        }
      }
    }

    function commit() {
      const pattern = readPattern();
      if (!pattern) return;
      const formatted = readValue();
      const raw = stripPattern(formatted, pattern);
      const stripOnCommit = host.hasAttribute('data-mask-strip-on-commit');

      if (stripOnCommit && raw !== formatted) {
        writeValue(raw);
      }

      host.dispatchEvent(new CustomEvent('mask-commit', {
        bubbles: true,
        detail: { raw, formatted },
      }));
    }

    function onInput() { reformat(); }
    function onBlur()  { commit(); }

    host.addEventListener('input', onInput);
    host.addEventListener('blur', onBlur);

    // Format any initial value on connect — covers SSR + pre-filled forms.
    reformat();

    return () => {
      host.removeEventListener('input', onInput);
      host.removeEventListener('blur', onBlur);
      host.removeAttribute('data-input-mask-active');
      host.removeAttribute('data-input-mask-complete');
    };
  },
});
