import type { Anno, PageState, YeetDoc, DocMeta } from './model';

export type ToolId =
  | 'select'
  | 'edit-text'
  | 'edit-image'
  | 'redact'
  | 'text'
  | 'draw'
  | 'rect'
  | 'ellipse'
  | 'line'
  | 'arrow'
  | 'highlight'
  | 'underline'
  | 'strike'
  | 'note'
  | 'image'
  | 'signature'
  | 'link'
  | 'field-text'
  | 'field-checkbox'
  | 'field-dropdown'
  | 'field-radio';

export type StoreEvent =
  | 'doc'
  | 'annos'
  | 'selection'
  | 'tool'
  | 'zoom'
  | 'page'
  | 'meta'
  | 'history'
  | 'settings';

type Snapshot = { annos: Anno[]; pages: PageState[]; meta: DocMeta };

/** Central editor state + a tiny pub/sub + snapshot-based undo/redo. */
export class Store {
  doc: YeetDoc | null = null;
  tool: ToolId = 'select';
  selection: string[] = [];
  zoom = 1;
  currentPage = 0;
  /** After placing an item with a drawing tool, switch back to the Select tool. */
  autoCursor = (() => { try { return localStorage.getItem('yeet:autoCursor') !== '0'; } catch { return true; } })();
  /** Place the last-used signature directly, skipping the picker. */
  reuseSignature = (() => { try { return localStorage.getItem('yeet:reuseSignature') === '1'; } catch { return false; } })();
  /** Lock aspect ratio when resizing (hold Shift to temporarily invert). */
  uniformScale = (() => { try { return localStorage.getItem('yeet:uniformScale') === '1'; } catch { return false; } })();
  /** Show soft-fill autofill suggestions while typing in text boxes / fields. */
  autofillEnabled = (() => { try { return localStorage.getItem('yeet:autofillEnabled') !== '0'; } catch { return true; } })();
  /** Up to 10 user-managed autofill terms (names with spaces are allowed). */
  autofillTerms: string[] = (() => {
    try { const v = JSON.parse(localStorage.getItem('yeet:autofillTerms') || '[]'); return Array.isArray(v) ? v.map((s) => String(s)).slice(0, 10) : []; }
    catch { return []; }
  })();
  /** Wired by the overlay: opens the signature picker and places on the current page. */
  openSignatureManager: (() => void) | null = null;
  /** While true, the properties panel skips rebuilding (used when committing an
   *  on-canvas inline edit so a click on a panel control isn't consumed by it). */
  silent = false;
  /** Wired by the properties panel: force a rebuild (used to refresh after a
   *  silent commit once any in-flight click has been handled). */
  refreshPanel: (() => void) | null = null;
  /** Sticky style used when creating new annotations of a given tool. */
  toolStyle = {
    color: '#12141b',
    accent: '#2f6bff',
    strokeWidth: 3,
    fontSize: 16,
    fontFamily: 'Helvetica',
    bold: false,
    italic: false,
    underline: false,
    highlight: '#ffe27a',
  };

  private listeners = new Map<StoreEvent, Set<(payload?: any) => void>>();
  private undoStack: Snapshot[] = [];
  private redoStack: Snapshot[] = [];
  private maxHistory = 60;

  on(ev: StoreEvent, cb: (payload?: any) => void): () => void {
    if (!this.listeners.has(ev)) this.listeners.set(ev, new Set());
    this.listeners.get(ev)!.add(cb);
    return () => this.listeners.get(ev)!.delete(cb);
  }
  emit(ev: StoreEvent, payload?: any) {
    this.listeners.get(ev)?.forEach((cb) => cb(payload));
  }
  /** Drop every subscriber. The store is a page-level singleton but the editor
   *  (its only subscriber) is torn down and rebuilt from scratch on every
   *  mount — without this, each mount's Viewer/Overlay/ContentEditor/etc.
   *  re-subscribe on top of the previous mount's listeners, so events (tool
   *  changes, hotkeys, undo/redo) fire once per accumulated mount instead of
   *  once. Call at the very start of mountEditor(), before anything re-subscribes. */
  clearListeners() {
    this.listeners.clear();
  }
  /** Reset document-scoped state to a blank slate. "Close document" only
   *  navigates back to the landing page — it never clears `doc` itself — so
   *  without this, reopening the editor with no file leaves the previous
   *  document's reference (and selection/tool/history) dangling on this
   *  singleton: the empty-state card paints, but anything reading store.doc
   *  directly (status bar, filename pill, page rail) still sees a "real"
   *  document. Call at the very start of mountEditor(), same as clearListeners(). */
  reset() {
    this.doc = null;
    this.tool = 'select';
    this.selection = [];
    this.zoom = 1;
    this.currentPage = 0;
    this.undoStack = [];
    this.redoStack = [];
    this.openSignatureManager = null;
    this.refreshPanel = null;
    this.silent = false;
  }

  setDoc(doc: YeetDoc) {
    this.doc = doc;
    this.selection = [];
    this.undoStack = [];
    this.redoStack = [];
    this.currentPage = 0;
    this.emit('doc', doc);
  }

  setTool(tool: ToolId) {
    if (this.tool === tool) return;
    this.tool = tool;
    if (tool !== 'select') this.selection = [];
    this.emit('tool', tool);
    this.emit('selection', this.selection);
  }

  setZoom(z: number) {
    this.zoom = z;
    this.emit('zoom', z);
  }

  setAutoCursor(v: boolean) {
    this.autoCursor = v;
    try { localStorage.setItem('yeet:autoCursor', v ? '1' : '0'); } catch { /* ignore */ }
    this.emit('settings');
  }

  setReuseSignature(v: boolean) {
    this.reuseSignature = v;
    try { localStorage.setItem('yeet:reuseSignature', v ? '1' : '0'); } catch { /* ignore */ }
    this.emit('settings');
  }

  setUniformScale(v: boolean) {
    this.uniformScale = v;
    try { localStorage.setItem('yeet:uniformScale', v ? '1' : '0'); } catch { /* ignore */ }
    this.emit('settings');
  }

  setAutofillEnabled(v: boolean) {
    this.autofillEnabled = v;
    try { localStorage.setItem('yeet:autofillEnabled', v ? '1' : '0'); } catch { /* ignore */ }
    this.emit('settings');
  }

  /** Persist the term list — trimmed, de-blanked, capped at 10. */
  setAutofillTerms(list: string[]) {
    this.autofillTerms = list.map((s) => s.trim()).filter(Boolean).slice(0, 10);
    try { localStorage.setItem('yeet:autofillTerms', JSON.stringify(this.autofillTerms)); } catch { /* ignore */ }
    this.emit('settings');
  }

  /** Called by the overlay after creating an annotation with a tool. */
  finishCreate(id: string) {
    this.select([id]);
    if (this.autoCursor) this.setTool('select');
  }

  setCurrentPage(i: number) {
    if (this.currentPage === i) return;
    this.currentPage = i;
    this.emit('page', i);
  }

  select(ids: string[]) {
    this.selection = ids;
    this.emit('selection', ids);
  }
  toggleSelect(id: string, additive: boolean) {
    if (additive) {
      this.selection = this.selection.includes(id)
        ? this.selection.filter((x) => x !== id)
        : [...this.selection, id];
    } else {
      this.selection = [id];
    }
    this.emit('selection', this.selection);
  }
  clearSelection() {
    if (!this.selection.length) return;
    this.selection = [];
    this.emit('selection', this.selection);
  }

  get selectedAnnos(): Anno[] {
    if (!this.doc) return [];
    const set = new Set(this.selection);
    return this.doc.annos.filter((a) => set.has(a.id));
  }

  annosForPage(pageId: string): Anno[] {
    return this.doc ? this.doc.annos.filter((a) => a.page === pageId) : [];
  }

  /** Fired on a real user checkpoint (not on undo/redo). Wired by the editor to
   *  bias which undo stack (annotation vs native byte-edit) Cmd+Z prefers. */
  onCheckpoint: (() => void) | null = null;

  // ---- mutations (call checkpoint() before a discrete user action) ----
  checkpoint() {
    if (!this.doc) return;
    this.undoStack.push(this.snapshot());
    if (this.undoStack.length > this.maxHistory) this.undoStack.shift();
    this.redoStack = [];
    this.onCheckpoint?.();
    this.emit('history');
  }

  addAnno(a: Anno) {
    this.doc!.annos.push(a);
    this.emit('annos', { added: a.id });
  }
  updateAnno(id: string, patch: Partial<Anno>) {
    const a = this.doc!.annos.find((x) => x.id === id);
    if (!a) return;
    Object.assign(a, patch);
    this.emit('annos', { updated: id });
  }
  updateSelected(patch: Partial<Anno>) {
    for (const id of this.selection) this.updateAnno(id, patch);
  }
  removeAnnos(ids: string[]) {
    const set = new Set(ids);
    this.doc!.annos = this.doc!.annos.filter((a) => !set.has(a.id));
    this.selection = this.selection.filter((id) => !set.has(id));
    this.emit('annos', { removed: ids });
    this.emit('selection', this.selection);
  }

  setMeta(patch: Partial<DocMeta>) {
    Object.assign(this.doc!.meta, patch);
    this.emit('meta', this.doc!.meta);
  }

  replacePages(pages: PageState[]) {
    this.doc!.pages = pages;
    this.emit('page', this.currentPage);
    this.emit('doc', this.doc);
  }

  private snapshot(): Snapshot {
    return {
      annos: structuredClone(this.doc!.annos),
      pages: structuredClone(this.doc!.pages),
      meta: structuredClone(this.doc!.meta),
    };
  }
  private restore(s: Snapshot) {
    this.doc!.annos = s.annos;
    this.doc!.pages = s.pages;
    this.doc!.meta = s.meta;
    this.selection = this.selection.filter((id) => this.doc!.annos.some((a) => a.id === id));
    this.emit('annos', {});
    this.emit('page', this.currentPage);
    this.emit('meta', this.doc!.meta);
    this.emit('selection', this.selection);
    this.emit('doc', this.doc);
  }

  canUndo() { return this.undoStack.length > 0; }
  canRedo() { return this.redoStack.length > 0; }

  undo() {
    if (!this.doc || !this.undoStack.length) return;
    this.redoStack.push(this.snapshot());
    this.restore(this.undoStack.pop()!);
    this.emit('history');
  }
  redo() {
    if (!this.doc || !this.redoStack.length) return;
    this.undoStack.push(this.snapshot());
    this.restore(this.redoStack.pop()!);
    this.emit('history');
  }
}

export const store = new Store();
