import { Component, ElementRef, computed, effect, input, output, viewChild } from '@angular/core';
import { RichTextBlock, emptyBlock } from '@shared/kernel/rich-text';
import { adjacentChip, createChip, readBlock, renderInto } from './rich-text-dom';

/** A menu entry for the insert-placeholder control — a plain {key,label}, so the
    editor stays domain-free (it never sees the brief's PlaceholderDef). */
export interface PlaceholderOption {
  readonly key: string;
  readonly label: string;
  // Auto-resolvable fields are filled server-side at send; manual fields need a value.
  // Drives the chip's styling so the two read apart at a glance.
  readonly autoResolvable?: boolean;
}

/**
 * Molecule: a minimal no-dependency WYSIWYG editor over a `RichTextBlock`.
 *
 * It is the single quarantined boundary to the imperative `contenteditable` DOM:
 * `content` in, `contentChanged` (a `RichTextBlock`) out, holding NO letter state.
 * Placeholders render as non-editable chips and can only be inserted from the menu
 * (valid keys only) — never typed as raw braces. Swapping in a real editor library
 * later (TipTap) means replacing only this component; nothing else sees the DOM.
 */
@Component({
  selector: 'app-rich-text-editor',
  styles: [`
    :host{display:block}
    .rte-toolbar{display:flex;flex-wrap:wrap;gap:var(--rhc-space-max-sm);align-items:center;margin-block-end:var(--rhc-space-max-sm)}
    .rte-toolbar button{min-inline-size:2.2rem}
    .rte-sep{inline-size:1px;align-self:stretch;background:var(--rhc-color-border-default)}
    .rte-editable{
      border:1px solid var(--rhc-color-border-default);border-radius:var(--rhc-border-radius-sm);
      padding:var(--rhc-space-max-md);min-block-size:4rem;
    }
    .rte-editable[contenteditable='false']{background:var(--rhc-color-cool-grey-100)}
    .rte-editable :is(p){margin:0 0 var(--rhc-space-max-sm)}
    .rte-editable :is(ul,ol){margin:0 0 var(--rhc-space-max-sm);padding-inline-start:1.4em}
    /* Placeholder chips read as fill-in fields: auto-resolvable (grey, filled server-side)
       vs manual (yellow, still needs a value). The read-only preview adds error/warning states.
       Chips are created imperatively (createChip) inside contenteditable, so they never receive
       Angular's _ngcontent scoping attribute — ::ng-deep is required or the rules won't match them.
       Braces use unicode escapes; a literal { in a CSS content string breaks the style parser. */
    :host ::ng-deep .rte-chip{
      border:1px dashed var(--rhc-color-border-default);border-radius:var(--rhc-border-radius-sm);
      padding:0 0.3em;white-space:nowrap;
    }
    :host ::ng-deep .rte-chip[data-auto='true']{background:var(--rhc-color-cool-grey-100)}
    :host ::ng-deep .rte-chip[data-auto='false']{background:var(--rhc-color-geel-100)}
    :host ::ng-deep .rte-chip::before{content:'\\7B';opacity:0.6;font-weight:700;margin-inline-end:0.1em}
    :host ::ng-deep .rte-chip::after{content:'\\7D';opacity:0.6;font-weight:700;margin-inline-start:0.1em}
  `],
  template: `
    @if (editable()) {
      <div class="rte-toolbar" role="toolbar" [attr.aria-label]="toolbarLabel()">
        <button type="button" class="btn btn-outline-secondary btn-sm" (mousedown)="$event.preventDefault()" (click)="format('bold')" [attr.aria-label]="boldLabel()"><b>B</b></button>
        <button type="button" class="btn btn-outline-secondary btn-sm" (mousedown)="$event.preventDefault()" (click)="format('italic')" [attr.aria-label]="italicLabel()"><i>I</i></button>
        <button type="button" class="btn btn-outline-secondary btn-sm" (mousedown)="$event.preventDefault()" (click)="format('underline')" [attr.aria-label]="underlineLabel()"><u>U</u></button>
        <span class="rte-sep" aria-hidden="true"></span>
        <button type="button" class="btn btn-outline-secondary btn-sm" (mousedown)="$event.preventDefault()" (click)="list('bullet')" [attr.aria-label]="bulletListLabel()">•</button>
        <button type="button" class="btn btn-outline-secondary btn-sm" (mousedown)="$event.preventDefault()" (click)="list('number')" [attr.aria-label]="numberListLabel()">1.</button>
        @if (placeholders().length) {
          <span class="rte-sep" aria-hidden="true"></span>
          <label>
            <span class="app-text-subtle">{{ insertLabel() }}</span>
            <select #ins (change)="insert(ins.value); ins.value = ''">
              <option value="" selected>{{ insertPrompt() }}</option>
              @for (p of placeholders(); track p.key) {
                <option [value]="p.key">{{ p.label }}</option>
              }
            </select>
          </label>
        }
      </div>
    }
    <div #editor class="rte-editable" [attr.contenteditable]="editable()" (input)="emit()" (keydown)="onKeydown($event)"
         role="textbox" aria-multiline="true" [attr.aria-label]="fieldLabel()"></div>
  `,
})
export class RichTextEditorComponent {
  content = input<RichTextBlock>(emptyBlock());
  placeholders = input<readonly PlaceholderOption[]>([]);
  editable = input(true);
  contentChanged = output<RichTextBlock>();

  // Localizable-by-default copy (shared-UI convention).
  fieldLabel = input($localize`:@@richTextEditor.field:Tekst`);
  toolbarLabel = input($localize`:@@richTextEditor.toolbar:Opmaak`);
  boldLabel = input($localize`:@@richTextEditor.bold:Vet`);
  italicLabel = input($localize`:@@richTextEditor.italic:Cursief`);
  underlineLabel = input($localize`:@@richTextEditor.underline:Onderstreept`);
  insertLabel = input($localize`:@@richTextEditor.insert:Veld invoegen:`);
  insertPrompt = input($localize`:@@richTextEditor.insertPrompt:Kies…`);
  bulletListLabel = input($localize`:@@richTextEditor.bulletList:Opsomming`);
  numberListLabel = input($localize`:@@richTextEditor.numberList:Genummerde lijst`);

  private editorEl = viewChild<ElementRef<HTMLElement>>('editor');
  private lastEmitted = '';

  private labelFor = (key: string) => this.placeholders().find((p) => p.key === key)?.label ?? key;
  private autoFor = (key: string) => this.placeholders().find((p) => p.key === key)?.autoResolvable ?? false;

  constructor() {
    // Render when content arrives/changes from OUTSIDE. Skip our own emitted value
    // flowing back (structural compare) so the caret isn't reset while typing.
    effect(() => {
      const content = this.content();
      const el = this.editorEl()?.nativeElement;
      if (!el) return;
      const serialized = JSON.stringify(content);
      if (serialized === this.lastEmitted) return;
      renderInto(el, content, this.labelFor, this.autoFor);
      this.lastEmitted = serialized;
    });
  }

  protected emit() {
    const el = this.editorEl()?.nativeElement;
    if (!el) return;
    const block = readBlock(el);
    this.lastEmitted = JSON.stringify(block);
    this.contentChanged.emit(block);
  }

  protected format(cmd: 'bold' | 'italic' | 'underline') {
    const el = this.editorEl()?.nativeElement;
    if (!el) return;
    el.focus();
    // ponytail: execCommand is deprecated but universally supported and zero-dependency;
    // if a browser drops it, this component is the one place to swap in a range-based impl.
    el.ownerDocument.execCommand(cmd);
    this.emit();
  }

  /** Bullet / numbered lists via execCommand — same deprecated-but-universal path as
      bold/italic (ponytail-noted on `format`); readBlock reads the resulting <ul>/<ol>. */
  protected list(kind: 'bullet' | 'number') {
    const el = this.editorEl()?.nativeElement;
    if (!el) return;
    el.focus();
    el.ownerDocument.execCommand(kind === 'bullet' ? 'insertUnorderedList' : 'insertOrderedList');
    this.emit();
  }

  /** Ctrl/Cmd+B/I/U → our format() (+ preventDefault) so serialization runs and behaviour
      is consistent across browsers rather than relying on the native handler. */
  protected onKeydown(e: KeyboardEvent) {
    if (e.key === 'Backspace' || e.key === 'Delete') {
      this.deleteAdjacentChip(e);
      return;
    }
    if (!(e.ctrlKey || e.metaKey) || e.altKey) return;
    const cmd = { b: 'bold', i: 'italic', u: 'underline' }[e.key.toLowerCase()] as 'bold' | 'italic' | 'underline' | undefined;
    if (!cmd) return;
    e.preventDefault();
    this.format(cmd);
  }

  /** Backspace/Delete next to a contenteditable=false chip removes it ourselves —
      browsers otherwise leave these atomic chips undeletable. Selections fall through. */
  private deleteAdjacentChip(e: KeyboardEvent) {
    const el = this.editorEl()?.nativeElement;
    if (!el) return;
    const sel = el.ownerDocument.getSelection();
    if (!sel || !sel.isCollapsed || !sel.rangeCount) return;
    const range = sel.getRangeAt(0);
    if (!el.contains(range.startContainer)) return;
    const chip = adjacentChip(range.startContainer, range.startOffset, e.key === 'Backspace' ? -1 : 1);
    if (!chip) return;
    e.preventDefault();
    chip.remove();
    this.emit();
  }

  protected insert(key: string) {
    const el = this.editorEl()?.nativeElement;
    if (!key || !el) return;
    el.focus();
    const chip = createChip(el.ownerDocument, key, this.labelFor(key), this.autoFor(key));
    const sel = el.ownerDocument.getSelection();
    if (sel && sel.rangeCount && el.contains(sel.anchorNode)) {
      const range = sel.getRangeAt(0);
      range.deleteContents();
      range.insertNode(chip);
      range.setStartAfter(chip);
      range.collapse(true);
      sel.removeAllRanges();
      sel.addRange(range);
    } else {
      (el.lastElementChild ?? el).appendChild(chip);
    }
    this.emit();
  }
}
