/**
 * Accessible, dependency-free profile editor mounted into a caller-owned node.
 *
 * It emits JSON profile data for a separate preview renderer. It creates no
 * canvas, Three.js object, webcam, storage, URL, or network request. User text
 * is assigned only through input `.value` and `textContent`; never `innerHTML`.
 */
import {
  ACCENTS,
  BODY_SHAPES,
  HAIR_COLORS,
  OUTFITS,
  SKIN_TONES,
  createDefaultLocalProfile,
  isLocalProfile,
  type HumanoidAppearanceChoices,
  type LocalProfile,
} from "./model.ts";

export interface ProfileEditorOptions {
  container: HTMLElement;
  profile: LocalProfile;
  /** Used only to regenerate deterministic appearance defaults; never rendered or emitted. */
  identityId: string;
  onPreview?: (profile: LocalProfile) => void;
  onSave?: (profile: LocalProfile) => void;
  onCancel?: () => void;
}

export interface ProfileEditorState {
  open: boolean;
  dirty: boolean;
  valid: boolean;
  /** Last saved/caller-supplied profile. */
  profile: LocalProfile;
  /** Current form values. May have an invalid display name while `valid` is false. */
  draft: LocalProfile;
}

export interface ProfileEditor {
  /** Stable dialog node for host layout or integration tests. */
  root: HTMLElement;
  state(): ProfileEditorState;
  /** Replace both committed and draft data through strict profile validation. */
  update(profile: LocalProfile): ProfileEditorState;
  open(): ProfileEditorState;
  /** Hide without committing or discarding the draft. */
  close(): ProfileEditorState;
  /** Include caller-mounted contextual controls in this dialog's keyboard trap. */
  registerFocusables(elements: readonly HTMLElement[]): void;
  dispose(): void;
}

type AppearanceField = keyof HumanoidAppearanceChoices;

const PROFILE_EDITOR_STYLES = `
.tera-profile-editor {
  width: min(420px, calc(100vw - 32px));
  max-height: min(720px, calc(100dvh - 32px));
  overflow: auto;
  color: var(--ink, rgba(255,255,255,.82));
  background: var(--glass-strong, rgba(9,13,18,.9));
  border: 1px solid var(--hairline, rgba(255,255,255,.13));
  border-radius: var(--r, 8px);
  box-shadow: var(--shadow, 0 8px 28px rgba(0,0,0,.45));
  backdrop-filter: var(--blur, blur(14px));
  padding: var(--s5, 24px);
  font: 12px/1.5 ui-monospace, "SF Mono", Menlo, monospace;
}
.tera-profile-editor__title { margin: 0 0 4px; color: white; font: 600 16px/1.3 inherit; }
.tera-profile-editor__intro { margin: 0 0 20px; color: var(--ink-2, rgba(255,255,255,.62)); }
.tera-profile-editor__grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.tera-profile-editor__field { display: grid; gap: 6px; min-width: 0; }
.tera-profile-editor__field--wide { grid-column: 1 / -1; }
.tera-profile-editor__label { color: var(--ink, rgba(255,255,255,.82)); }
.tera-profile-editor__input {
  width: 100%; min-height: 40px; padding: 8px 10px; color: white;
  background: rgba(255,255,255,.07); border: 1px solid var(--hairline, rgba(255,255,255,.15));
  border-radius: var(--r-sm, 5px); font: inherit;
}
.tera-profile-editor__input:hover { border-color: rgba(255,255,255,.3); }
.tera-profile-editor__input:focus-visible, .tera-profile-editor__button:focus-visible {
  outline: 2px solid var(--amber, #f2b134); outline-offset: 2px;
}
.tera-profile-editor__input[aria-invalid="true"] { border-color: #ff8b80; }
.tera-profile-editor__status { min-height: 18px; margin: 12px 0 0; color: #ffb2aa; }
.tera-profile-editor__actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; margin-top: 16px; }
.tera-profile-editor__button {
  min-height: 40px; padding: 8px 14px; color: var(--ink, white); cursor: pointer;
  background: rgba(255,255,255,.07); border: 1px solid var(--hairline, rgba(255,255,255,.15));
  border-radius: var(--r-sm, 5px); font: inherit;
}
.tera-profile-editor__button:hover:not(:disabled) { background: rgba(255,255,255,.12); }
.tera-profile-editor__button--primary { color: #171109; background: var(--amber, #f2b134); border-color: transparent; font-weight: 700; }
.tera-profile-editor__button--primary:hover:not(:disabled) { background: var(--amber-lit, #ffc555); }
.tera-profile-editor__button:disabled { cursor: not-allowed; opacity: .42; }
@media (max-width: 460px) { .tera-profile-editor__grid { grid-template-columns: 1fr; } .tera-profile-editor__field--wide { grid-column: auto; } }
@media (prefers-reduced-motion: reduce) { .tera-profile-editor * { scroll-behavior: auto !important; } }
`;

const FIELD_OPTIONS: Readonly<Record<AppearanceField, readonly string[]>> = {
  skinTone: SKIN_TONES,
  outfit: OUTFITS,
  accent: ACCENTS,
  hair: HAIR_COLORS,
  bodyShape: BODY_SHAPES,
};

const FIELD_LABELS: Readonly<Record<AppearanceField, string>> = {
  skinTone: "Skin tone",
  outfit: "Outfit",
  accent: "Accent",
  hair: "Hair",
  bodyShape: "Body shape",
};

let editorSequence = 0;

function clone(profile: LocalProfile): LocalProfile {
  return { ...profile, appearance: { ...profile.appearance } };
}

function presentChoice(value: string): string {
  return value.replace(/(^|-)([a-z])/g, (_match, separator: string, letter: string) => `${separator}${letter.toUpperCase()}`);
}

function validDisplayName(value: string): boolean {
  return (
    value === value.trim() &&
    value.length >= 1 &&
    value.length <= 64 &&
    !/[\u0000-\u001f\u007f]/u.test(value)
  );
}

function sameProfile(a: LocalProfile, b: LocalProfile): boolean {
  return (
    a.version === b.version &&
    a.displayName === b.displayName &&
    a.appearance.skinTone === b.appearance.skinTone &&
    a.appearance.outfit === b.appearance.outfit &&
    a.appearance.accent === b.appearance.accent &&
    a.appearance.hair === b.appearance.hair &&
    a.appearance.bodyShape === b.appearance.bodyShape
  );
}

function focusable(value: Element | null): value is HTMLElement {
  return value !== null && typeof (value as HTMLElement).focus === "function";
}

export function createProfileEditor(options: ProfileEditorOptions): ProfileEditor {
  if (!options.container || typeof options.container.append !== "function") {
    throw new RangeError("profile editor: container must be an HTMLElement");
  }
  if (!isLocalProfile(options.profile)) throw new RangeError("profile editor: invalid profile");
  if (typeof options.identityId !== "string" || options.identityId.length === 0) {
    throw new RangeError("profile editor: identity id is required");
  }

  const doc = options.container.ownerDocument;
  const number = ++editorSequence;
  const titleId = `tera-profile-title-${number}`;
  const descriptionId = `tera-profile-description-${number}`;
  const statusId = `tera-profile-status-${number}`;
  const root = doc.createElement("section");
  root.className = "tera-profile-editor";
  root.setAttribute("role", "dialog");
  root.setAttribute("aria-modal", "true");
  root.setAttribute("aria-labelledby", titleId);
  root.setAttribute("aria-describedby", `${descriptionId} ${statusId}`);
  root.setAttribute("aria-hidden", "true");
  root.hidden = true;

  const style = doc.createElement("style");
  style.setAttribute("data-tera-profile-editor-style", "");
  style.textContent = PROFILE_EDITOR_STYLES;
  root.append(style);

  const title = doc.createElement("h2");
  title.id = titleId;
  title.className = "tera-profile-editor__title";
  title.textContent = "Your Tera character";
  root.append(title);

  const intro = doc.createElement("p");
  intro.id = descriptionId;
  intro.className = "tera-profile-editor__intro";
  intro.textContent = "Choose how your procedural humanoid appears. Preview stays local to this page.";
  root.append(intro);

  const form = doc.createElement("form");
  form.setAttribute("novalidate", "");
  const grid = doc.createElement("div");
  grid.className = "tera-profile-editor__grid";
  form.append(grid);

  const nameField = doc.createElement("div");
  nameField.className = "tera-profile-editor__field tera-profile-editor__field--wide";
  const nameLabel = doc.createElement("label");
  const nameId = `tera-profile-name-${number}`;
  nameLabel.className = "tera-profile-editor__label";
  nameLabel.setAttribute("for", nameId);
  nameLabel.textContent = "Display name";
  const nameInput = doc.createElement("input");
  nameInput.id = nameId;
  nameInput.className = "tera-profile-editor__input";
  nameInput.setAttribute("data-field", "displayName");
  nameInput.setAttribute("aria-describedby", statusId);
  nameInput.type = "text";
  nameInput.maxLength = 64;
  nameInput.autocomplete = "name";
  nameField.append(nameLabel, nameInput);
  grid.append(nameField);

  const selects = {} as Record<AppearanceField, HTMLSelectElement>;
  const appearanceFields = Object.keys(FIELD_OPTIONS) as AppearanceField[];
  for (const field of appearanceFields) {
    const wrapper = doc.createElement("div");
    wrapper.className = "tera-profile-editor__field";
    const label = doc.createElement("label");
    const id = `tera-profile-${field}-${number}`;
    label.className = "tera-profile-editor__label";
    label.setAttribute("for", id);
    label.textContent = FIELD_LABELS[field];
    const select = doc.createElement("select");
    select.id = id;
    select.className = "tera-profile-editor__input";
    select.setAttribute("data-field", field);
    for (const choice of FIELD_OPTIONS[field]) {
      const option = doc.createElement("option");
      option.value = choice;
      option.textContent = presentChoice(choice);
      select.append(option);
    }
    selects[field] = select;
    wrapper.append(label, select);
    grid.append(wrapper);
  }

  const status = doc.createElement("p");
  status.id = statusId;
  status.className = "tera-profile-editor__status";
  status.setAttribute("role", "status");
  status.setAttribute("aria-live", "polite");
  form.append(status);

  const actions = doc.createElement("div");
  actions.className = "tera-profile-editor__actions";
  const resetButton = button(doc, "Reset appearance", "reset");
  const cancelButton = button(doc, "Cancel", "cancel");
  const saveButton = button(doc, "Save profile", "save", true);
  actions.append(resetButton, cancelButton, saveButton);
  form.append(actions);
  root.append(form);
  options.container.append(root);

  const focusOrder: HTMLElement[] = [nameInput, ...appearanceFields.map((field) => selects[field]), resetButton, cancelButton, saveButton];
  let committed = clone(options.profile);
  let draft = clone(options.profile);
  let isOpen = false;
  let disposed = false;
  let returnFocus: HTMLElement | null = null;

  function valid(): boolean {
    return validDisplayName(draft.displayName) && isLocalProfile(draft);
  }

  function snapshot(): ProfileEditorState {
    return {
      open: isOpen,
      dirty: !sameProfile(committed, draft),
      valid: valid(),
      profile: clone(committed),
      draft: clone(draft),
    };
  }

  function render(): void {
    nameInput.value = draft.displayName;
    for (const field of appearanceFields) selects[field].value = draft.appearance[field];
    const isValid = valid();
    nameInput.setAttribute("aria-invalid", isValid ? "false" : "true");
    saveButton.disabled = !isValid;
    status.textContent = isValid ? "" : "Enter a name from 1 to 64 characters without leading or trailing spaces.";
  }

  function emitPreview(): void {
    if (valid()) options.onPreview?.(clone(draft));
  }

  function assignField(field: AppearanceField, value: string): void {
    if (!(FIELD_OPTIONS[field] as readonly string[]).includes(value)) return;
    draft = { ...draft, appearance: { ...draft.appearance, [field]: value } } as LocalProfile;
    render();
    emitPreview();
  }

  function save(): void {
    if (!valid()) {
      render();
      nameInput.focus();
      return;
    }
    committed = clone(draft);
    options.onSave?.(clone(committed));
    close();
  }

  function cancel(): void {
    draft = clone(committed);
    render();
    emitPreview();
    options.onCancel?.();
    close();
  }

  function close(): ProfileEditorState {
    if (!isOpen || disposed) return snapshot();
    isOpen = false;
    root.hidden = true;
    root.setAttribute("aria-hidden", "true");
    const target = returnFocus;
    returnFocus = null;
    target?.focus();
    return snapshot();
  }

  nameInput.addEventListener("input", () => {
    if (disposed) return;
    draft = { ...draft, displayName: nameInput.value };
    render();
    emitPreview();
  });
  for (const field of appearanceFields) {
    selects[field].addEventListener("change", () => {
      if (!disposed) assignField(field, selects[field].value);
    });
  }
  resetButton.addEventListener("click", () => {
    if (disposed) return;
    const defaults = createDefaultLocalProfile(options.identityId, committed.displayName);
    draft = { ...defaults, displayName: draft.displayName };
    render();
    emitPreview();
  });
  cancelButton.addEventListener("click", () => {
    if (!disposed) cancel();
  });
  saveButton.addEventListener("click", () => {
    if (!disposed) save();
  });
  form.addEventListener("submit", (event) => {
    event.preventDefault();
    if (!disposed) save();
  });
  root.addEventListener("keydown", (event) => {
    if (disposed || !isOpen) return;
    if (event.key === "Escape") {
      event.preventDefault();
      cancel();
      return;
    }
    if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
      event.preventDefault();
      save();
      return;
    }
    if (event.key !== "Tab") return;
    const enabled = focusOrder.filter(
      (element) => !element.hidden && !((element as HTMLButtonElement).disabled),
    );
    const first = enabled[0];
    const last = enabled[enabled.length - 1];
    if (!first || !last) return;
    if (event.shiftKey && doc.activeElement === first) {
      event.preventDefault();
      last.focus();
    } else if (!event.shiftKey && doc.activeElement === last) {
      event.preventDefault();
      first.focus();
    }
  });

  render();

  return {
    root,
    state: snapshot,
    update(profile) {
      if (disposed) return snapshot();
      if (!isLocalProfile(profile)) throw new RangeError("profile editor: invalid profile");
      committed = clone(profile);
      draft = clone(profile);
      render();
      emitPreview();
      return snapshot();
    },
    open() {
      if (disposed || isOpen) return snapshot();
      isOpen = true;
      root.hidden = false;
      root.setAttribute("aria-hidden", "false");
      returnFocus = focusable(doc.activeElement) ? doc.activeElement : null;
      nameInput.focus();
      emitPreview();
      return snapshot();
    },
    close,
    registerFocusables(elements) {
      if (disposed) return;
      for (const element of elements) {
        if (focusable(element) && !focusOrder.includes(element)) focusOrder.push(element);
      }
    },
    dispose() {
      if (disposed) return;
      if (isOpen) close();
      disposed = true;
      root.remove();
    },
  };
}

function button(
  doc: Document,
  label: string,
  action: "reset" | "cancel" | "save",
  primary = false,
): HTMLButtonElement {
  const element = doc.createElement("button");
  element.type = "button";
  element.className = `tera-profile-editor__button${primary ? " tera-profile-editor__button--primary" : ""}`;
  element.setAttribute("data-action", action);
  element.textContent = label;
  return element;
}
