/**
 * CommentPinManager — Figma-style comment pins on the canvas.
 * Follows the PresenceManager pattern: overlay DOM layer with camera tracking.
 */

import * as Automerge from "@automerge/automerge";
import { getModuleApiBase } from "../shared/url-helpers";
import type { CommunitySync } from "./community-sync";
import type {
  CommentPinAnchor,
  CommentPinMessage,
  CommentPinData,
} from "../shared/comment-pin-types";

interface SpaceMember {
  did: string;
  displayName: string | null;
  username: string;
}

interface RNoteItem {
  id: string;
  title: string;
  type: string;
  updated_at: string;
}

export class CommentPinManager {
  #container: HTMLElement;
  #canvasContent: HTMLElement;
  #sync: CommunitySync;
  #spaceSlug: string;
  #panX = 0;
  #panY = 0;
  #scale = 1;
  #pinLayer: HTMLElement;
  #popover: HTMLElement;
  #placementMode = false;
  #showResolved = false;
  #openPinId: string | null = null;
  #members: SpaceMember[] | null = null;
  #mentionDropdown: HTMLElement | null = null;
  #notes: RNoteItem[] | null = null;
  #onDocPointerDown: (e: PointerEvent) => void = () => {};

  constructor(
    container: HTMLElement,
    canvasContent: HTMLElement,
    sync: CommunitySync,
    spaceSlug: string,
  ) {
    this.#container = container;
    this.#canvasContent = canvasContent;
    this.#sync = sync;
    this.#spaceSlug = spaceSlug;

    // Create pin overlay layer
    this.#pinLayer = document.createElement("div");
    this.#pinLayer.id = "comment-pins-layer";
    this.#pinLayer.style.cssText = `
      position: absolute; top: 0; left: 0;
      width: 100%; height: 100%;
      pointer-events: none; z-index: 9998; overflow: visible;
    `;
    this.#container.appendChild(this.#pinLayer);

    // Create shared popover
    this.#popover = document.createElement("div");
    this.#popover.id = "comment-pin-popover";
    this.#popover.style.cssText = `
      display: none; position: absolute; z-index: 10001;
      background: #1e1e2e; border: 1px solid #444; border-radius: 10px;
      width: 300px; max-height: 400px; overflow: hidden;
      box-shadow: 0 8px 32px rgba(0,0,0,0.5); pointer-events: auto;
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      color: #e0e0e0; font-size: 13px;
    `;
    this.#container.appendChild(this.#popover);

    // Listen for remote comment pin changes
    this.#sync.addEventListener("comment-pins-changed", () => this.#renderAllPins());
    this.#sync.addEventListener("synced", () => this.#renderAllPins());

    // Listen for shape transforms to track attached pins
    this.#canvasContent.addEventListener("folk-transform", (e: Event) => {
      const target = e.target as HTMLElement;
      if (target?.id) this.#refreshPinsForShape(target.id);
    });

    // Close popover on outside click
    this.#onDocPointerDown = (e: PointerEvent) => {
      if (this.#popover.style.display === "none") return;
      if (this.#popover.contains(e.target as Node)) return;
      // Don't close if clicking a pin marker
      if ((e.target as HTMLElement)?.closest?.(".comment-pin-marker")) return;
      this.closePopover();
    };
    document.addEventListener("pointerdown", this.#onDocPointerDown);
  }

  // ── Camera ──

  setCamera(panX: number, panY: number, scale: number) {
    this.#panX = panX;
    this.#panY = panY;
    this.#scale = scale;
    this.#repositionAllPins();
    this.#repositionPopover();

    // Hide pins when zoomed out very far
    this.#pinLayer.style.display = scale < 0.15 ? "none" : "";
  }

  // ── Placement Mode ──

  get placementMode() {
    return this.#placementMode;
  }

  enterPinPlacementMode() {
    this.#placementMode = true;
    this.#container.style.cursor = "crosshair";
  }

  exitPinPlacementMode() {
    this.#placementMode = false;
    this.#container.style.cursor = "";
  }

  // ── Pin CRUD ──

  createPin(anchor: CommentPinAnchor): string {
    const pinId = crypto.randomUUID();
    const did = this.#getLocalDID();
    const name = this.#getLocalUsername();

    const pinData: CommentPinData = {
      id: pinId,
      anchor,
      resolved: false,
      messages: [],
      createdAt: Date.now(),
      createdBy: did,
      createdByName: name,
    };

    const newDoc = Automerge.change(this.#sync.doc, "Add comment pin", (doc) => {
      if (!doc.commentPins) doc.commentPins = {};
      doc.commentPins[pinId] = JSON.parse(JSON.stringify(pinData));
    });
    this.#sync._applyDocChange(newDoc);

    this.#renderAllPins();
    this.#openPinPopover(pinId, true);
    return pinId;
  }

  createPinOnShape(shapeId: string) {
    const shape = document.getElementById(shapeId) as HTMLElement | null;
    if (!shape) return;

    const anchor: CommentPinAnchor = {
      type: "shape",
      shapeId,
      offsetX: 0,
      offsetY: 0,
    };
    this.createPin(anchor);
  }

  handleCanvasClick(worldX: number, worldY: number, clientX?: number, clientY?: number) {
    if (!this.#placementMode) return false;

    // Check if clicking on a shape — elementFromPoint needs viewport coords
    let shapeEl: HTMLElement | null = null;
    if (clientX !== undefined && clientY !== undefined) {
      shapeEl = document.elementFromPoint(clientX, clientY)
        ?.closest?.("folk-shape, folk-markdown, folk-wrapper, folk-slide, folk-prompt, folk-zine-gen, folk-image-gen, folk-video-gen, folk-embed, folk-chat, folk-rapp") as HTMLElement | null;
    }

    let anchor: CommentPinAnchor;
    if (shapeEl?.id) {
      const shapeData = this.#sync.doc.shapes?.[shapeEl.id];
      if (shapeData) {
        anchor = {
          type: "shape",
          shapeId: shapeEl.id,
          offsetX: worldX - shapeData.x,
          offsetY: worldY - shapeData.y,
        };
      } else {
        anchor = { type: "canvas", offsetX: worldX, offsetY: worldY };
      }
    } else {
      anchor = { type: "canvas", offsetX: worldX, offsetY: worldY };
    }

    this.createPin(anchor);
    this.exitPinPlacementMode();
    return true;
  }

  addMessage(pinId: string, text: string) {
    const did = this.#getLocalDID();
    const name = this.#getLocalUsername();

    // Extract @mentions
    const mentionedDids = this.#extractMentionDids(text);

    const msg: CommentPinMessage = {
      id: crypto.randomUUID(),
      authorId: did,
      authorName: name,
      text,
      mentionedDids: mentionedDids.length > 0 ? mentionedDids : undefined,
      createdAt: Date.now(),
    };

    const newDoc = Automerge.change(this.#sync.doc, "Add comment", (doc) => {
      if (doc.commentPins?.[pinId]) {
        doc.commentPins[pinId].messages.push(JSON.parse(JSON.stringify(msg)));
      }
    });
    this.#sync._applyDocChange(newDoc);

    // Fire-and-forget notification for mentions
    if (mentionedDids.length > 0) {
      this.#notifyMentions(pinId, did, name, mentionedDids);
    }

    // Fire-and-forget notification to all space members
    const pin = this.#sync.doc.commentPins?.[pinId];
    const isReply = (pin?.messages?.length ?? 0) > 1;
    this.#notifySpaceMembers(pinId, text, isReply, mentionedDids);

    // Re-render popover if open
    if (this.#openPinId === pinId) {
      this.#openPinPopover(pinId, false);
    }
    this.#renderAllPins();
  }

  resolvePin(pinId: string) {
    const newDoc = Automerge.change(this.#sync.doc, "Resolve comment pin", (doc) => {
      if (doc.commentPins?.[pinId]) {
        doc.commentPins[pinId].resolved = true;
      }
    });
    this.#sync._applyDocChange(newDoc);
    this.closePopover();
    this.#renderAllPins();
  }

  unresolvePin(pinId: string) {
    const newDoc = Automerge.change(this.#sync.doc, "Unresolve comment pin", (doc) => {
      if (doc.commentPins?.[pinId]) {
        doc.commentPins[pinId].resolved = false;
      }
    });
    this.#sync._applyDocChange(newDoc);
    this.#renderAllPins();
  }

  deletePin(pinId: string) {
    const newDoc = Automerge.change(this.#sync.doc, "Delete comment pin", (doc) => {
      if (doc.commentPins?.[pinId]) {
        delete doc.commentPins[pinId];
      }
    });
    this.#sync._applyDocChange(newDoc);
    this.closePopover();
    this.#renderAllPins();
  }

  linkNote(pinId: string, noteId: string, noteTitle: string) {
    const newDoc = Automerge.change(this.#sync.doc, "Link note to pin", (doc) => {
      if (doc.commentPins?.[pinId]) {
        doc.commentPins[pinId].linkedNoteId = noteId;
        doc.commentPins[pinId].linkedNoteTitle = noteTitle;
      }
    });
    this.#sync._applyDocChange(newDoc);
    if (this.#openPinId === pinId) this.#openPinPopover(pinId, false);
    this.#renderAllPins();
  }

  unlinkNote(pinId: string) {
    const newDoc = Automerge.change(this.#sync.doc, "Unlink note from pin", (doc) => {
      if (doc.commentPins?.[pinId]) {
        delete (doc.commentPins[pinId] as any).linkedNoteId;
        delete (doc.commentPins[pinId] as any).linkedNoteTitle;
      }
    });
    this.#sync._applyDocChange(newDoc);
    if (this.#openPinId === pinId) this.#openPinPopover(pinId, false);
    this.#renderAllPins();
  }

  toggleShowResolved() {
    this.#showResolved = !this.#showResolved;
    this.#renderAllPins();
    return this.#showResolved;
  }

  closePopover() {
    this.#popover.style.display = "none";
    this.#openPinId = null;
    this.#closeMentionDropdown();
  }

  openPinById(pinId: string) {
    const pins = this.#sync.doc.commentPins || {};
    if (!pins[pinId]) return;
    this.#renderAllPins();
    this.#openPinPopover(pinId, false);

    // Pan to the pin
    const pos = this.#getPinWorldPosition(pins[pinId]);
    if (pos) {
      // Dispatch a custom event so canvas.html can animate to position
      this.#container.dispatchEvent(
        new CustomEvent("comment-pin-navigate", { detail: { x: pos.x, y: pos.y } }),
      );
    }
  }

  // ── Rendering ──

  #renderAllPins() {
    const pins = this.#sync.doc.commentPins || {};
    const sortedPins = Object.values(pins).sort((a, b) => a.createdAt - b.createdAt);

    // Detect orphaned pins (shape deleted)
    const orphanedPins = new Set<string>();
    for (const pin of sortedPins) {
      if (pin.anchor.type === "shape" && pin.anchor.shapeId) {
        const shapeExists = this.#sync.doc.shapes?.[pin.anchor.shapeId];
        const shapeDeleted = shapeExists && (shapeExists as any).deleted === true;
        if (!shapeExists || shapeDeleted) {
          orphanedPins.add(pin.id);
        }
      }
    }

    // Clear existing markers
    this.#pinLayer.innerHTML = "";

    let displayIndex = 0;
    for (const pin of sortedPins) {
      displayIndex++;

      if (pin.resolved && !this.#showResolved) continue;

      const pos = this.#getPinWorldPosition(pin);
      if (!pos) continue;

      const screenX = pos.x * this.#scale + this.#panX;
      const screenY = pos.y * this.#scale + this.#panY;
      const isOrphaned = orphanedPins.has(pin.id);

      const marker = document.createElement("div");
      marker.className = "comment-pin-marker";
      marker.dataset.pinId = pin.id;
      marker.style.cssText = `
        position: absolute;
        left: ${screenX}px; top: ${screenY}px;
        width: 28px; height: 28px;
        transform: translate(-50%, -100%);
        pointer-events: auto; cursor: pointer;
        transition: left 0.05s, top 0.05s;
        z-index: ${pin.id === this.#openPinId ? 10000 : 9998};
      `;

      const bgColor = pin.resolved ? "#666" : isOrphaned ? "#ef4444" : "#8b5cf6";
      const border = isOrphaned ? "2px dashed #fbbf24" : "2px solid rgba(255,255,255,0.3)";
      const opacity = pin.resolved ? "0.5" : "1";

      marker.innerHTML = `
        <div style="
          width: 28px; height: 28px;
          background: ${bgColor}; border-radius: 50% 50% 50% 0;
          transform: rotate(-45deg);
          display: flex; align-items: center; justify-content: center;
          border: ${border}; opacity: ${opacity};
          box-shadow: 0 2px 8px rgba(0,0,0,0.3);
        ">
          <span style="
            transform: rotate(45deg);
            color: white; font-size: 11px; font-weight: 700;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
          ">${displayIndex}</span>
        </div>
        ${this.#scale >= 0.4 && pin.messages.length > 0 ? `
          <div style="
            position: absolute; top: -6px; right: -6px;
            background: #ef4444; color: white; font-size: 9px;
            min-width: 16px; height: 16px; border-radius: 8px;
            display: flex; align-items: center; justify-content: center;
            font-weight: 700; transform: rotate(45deg);
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
          ">${pin.messages.length}</div>
        ` : ""}
      `;

      marker.addEventListener("click", (e) => {
        e.stopPropagation();
        this.#openPinPopover(pin.id, false);
      });

      this.#pinLayer.appendChild(marker);
    }
  }

  #repositionAllPins() {
    const pins = this.#sync.doc.commentPins || {};
    const markers = this.#pinLayer.querySelectorAll(".comment-pin-marker") as NodeListOf<HTMLElement>;

    for (const marker of markers) {
      const pinId = marker.dataset.pinId;
      if (!pinId || !pins[pinId]) continue;

      const pos = this.#getPinWorldPosition(pins[pinId]);
      if (!pos) continue;

      const screenX = pos.x * this.#scale + this.#panX;
      const screenY = pos.y * this.#scale + this.#panY;
      marker.style.left = `${screenX}px`;
      marker.style.top = `${screenY}px`;

      // Hide labels when zoomed out
      const badge = marker.querySelector("div[style*='top: -6px']") as HTMLElement | null;
      if (badge) badge.style.display = this.#scale >= 0.4 ? "" : "none";
    }
  }

  #repositionPopover() {
    if (!this.#openPinId || this.#popover.style.display === "none") return;
    const pins = this.#sync.doc.commentPins || {};
    const pin = pins[this.#openPinId];
    if (!pin) return;

    const pos = this.#getPinWorldPosition(pin);
    if (!pos) return;

    const screenX = pos.x * this.#scale + this.#panX;
    const screenY = pos.y * this.#scale + this.#panY;
    this.#popover.style.left = `${screenX + 16}px`;
    this.#popover.style.top = `${screenY}px`;
  }

  #refreshPinsForShape(shapeId: string) {
    const pins = this.#sync.doc.commentPins || {};
    let needsReposition = false;

    for (const pin of Object.values(pins)) {
      if (pin.anchor.type === "shape" && pin.anchor.shapeId === shapeId) {
        needsReposition = true;
        break;
      }
    }

    if (needsReposition) {
      this.#repositionAllPins();
      this.#repositionPopover();
    }
  }

  #getPinWorldPosition(pin: CommentPinData): { x: number; y: number } | null {
    if (pin.anchor.type === "canvas") {
      return { x: pin.anchor.offsetX, y: pin.anchor.offsetY };
    }

    if (pin.anchor.type === "shape" && pin.anchor.shapeId) {
      const shapeData = this.#sync.doc.shapes?.[pin.anchor.shapeId];
      if (!shapeData || (shapeData as any).deleted) {
        // Orphaned — fall back to anchor offset as world coords
        return { x: pin.anchor.offsetX, y: pin.anchor.offsetY };
      }
      return {
        x: shapeData.x + pin.anchor.offsetX,
        y: shapeData.y + pin.anchor.offsetY,
      };
    }

    return null;
  }

  // ── Popover ──

  #openPinPopover(pinId: string, focusInput: boolean) {
    const pins = this.#sync.doc.commentPins || {};
    const pin = pins[pinId];
    if (!pin) return;

    this.#openPinId = pinId;
    const pos = this.#getPinWorldPosition(pin);
    if (!pos) return;

    const screenX = pos.x * this.#scale + this.#panX;
    const screenY = pos.y * this.#scale + this.#panY;

    // Sequential pin number
    const sortedPins = Object.values(pins).sort((a, b) => a.createdAt - b.createdAt);
    const pinIndex = sortedPins.findIndex((p) => p.id === pinId) + 1;

    const isOrphaned =
      pin.anchor.type === "shape" &&
      pin.anchor.shapeId &&
      (!this.#sync.doc.shapes?.[pin.anchor.shapeId] ||
        (this.#sync.doc.shapes[pin.anchor.shapeId] as any).deleted);

    let html = `<style>
      .cp-action { background: none; border: 1px solid #444; border-radius: 4px;
        padding: 2px 6px; cursor: pointer; color: #ccc; font-size: 13px; }
      .cp-action:hover { background: #333; }
      .cp-link-btn { background: none; border: 1px solid #444; border-radius: 6px;
        padding: 4px 8px; cursor: pointer; color: #a78bfa; font-size: 12px; width: 100%; text-align: left; }
      .cp-link-btn:hover { background: #2a2a3a; }
      .cp-note-item { padding: 6px 10px; cursor: pointer; font-size: 12px; border-bottom: 1px solid #333; }
      .cp-note-item:hover { background: #3a3a4a; }
      .cp-note-item:last-child { border-bottom: none; }
    </style>`;

    // Header
    html += `
      <div style="padding: 10px 12px; border-bottom: 1px solid #333; display: flex; align-items: center; justify-content: space-between;">
        <span style="font-weight: 700; color: #a78bfa;">Pin #${pinIndex}</span>
        <div style="display: flex; gap: 4px;">
          ${pin.resolved
            ? `<button class="cp-action" data-action="unresolve" title="Reopen">↩</button>`
            : `<button class="cp-action" data-action="resolve" title="Resolve">✓</button>`}
          <button class="cp-action" data-action="remind" title="Set reminder">📅</button>
          <button class="cp-action" data-action="delete" title="Delete" style="color: #ef4444;">✕</button>
        </div>
      </div>
    `;

    if (isOrphaned) {
      html += `<div style="padding: 6px 12px; background: #44200a; color: #fbbf24; font-size: 11px;">⚠ Attached shape was deleted</div>`;
    }

    // Linked rNote
    if (pin.linkedNoteId) {
      html += `
        <div style="padding: 8px 12px; background: #1a1a2e; border-bottom: 1px solid #333; display: flex; align-items: center; justify-content: space-between;">
          <a href="${getModuleApiBase("rnotes")}#${pin.linkedNoteId}" target="_blank"
             style="color: #a78bfa; text-decoration: none; font-size: 12px; font-weight: 600;">
            📝 ${this.#escapeHtml(pin.linkedNoteTitle || "Linked note")}
          </a>
          <button class="cp-action" data-action="unlink-note" title="Unlink" style="font-size: 10px;">✕</button>
        </div>
      `;
    }

    // Messages
    if (pin.messages.length > 0) {
      html += `<div style="max-height: 200px; overflow-y: auto; padding: 8px 12px;">`;
      for (const msg of pin.messages) {
        const time = new Date(msg.createdAt).toLocaleString(undefined, {
          month: "short", day: "numeric", hour: "numeric", minute: "2-digit",
        });
        html += `
          <div style="margin-bottom: 10px;">
            <div style="display: flex; justify-content: space-between; align-items: baseline;">
              <span style="font-weight: 600; color: #c4b5fd; font-size: 12px;">${this.#escapeHtml(msg.authorName)}</span>
              <span style="color: #666; font-size: 10px;">${time}</span>
            </div>
            <div style="margin-top: 3px; line-height: 1.4; word-break: break-word;">${this.#formatMessageText(msg.text)}</div>
          </div>
        `;
      }
      html += `</div>`;
    }

    // Input + link note row
    html += `
      <div style="padding: 8px 12px; border-top: 1px solid #333; position: relative;">
        <div style="display: flex; gap: 6px;">
          <input type="text" class="cp-input" placeholder="Add a note... (@ to mention)"
            style="flex: 1; background: #2a2a3a; border: 1px solid #444; border-radius: 6px;
                   padding: 6px 10px; color: #e0e0e0; font-size: 13px; outline: none;"
          />
          <button class="cp-send" style="
            background: #8b5cf6; color: white; border: none; border-radius: 6px;
            padding: 6px 12px; cursor: pointer; font-size: 13px; font-weight: 600;
          ">Send</button>
        </div>
        ${!pin.linkedNoteId ? `
          <div style="margin-top: 6px;">
            <button class="cp-link-btn" data-action="link-note">📝 Link existing rNote...</button>
            <div class="cp-note-picker" style="display: none; max-height: 150px; overflow-y: auto;
                 background: #2a2a3a; border: 1px solid #555; border-radius: 6px; margin-top: 4px;"></div>
          </div>
        ` : ""}
      </div>
    `;

    this.#popover.innerHTML = html;
    this.#popover.style.display = "block";
    this.#popover.style.left = `${screenX + 16}px`;
    this.#popover.style.top = `${screenY}px`;

    // Wire up actions
    this.#popover.querySelectorAll(".cp-action").forEach((btn) => {
      btn.addEventListener("click", (e) => {
        const action = (e.currentTarget as HTMLElement).dataset.action;
        if (action === "resolve") this.resolvePin(pinId);
        else if (action === "unresolve") this.unresolvePin(pinId);
        else if (action === "delete") this.deletePin(pinId);
        else if (action === "remind") this.#createReminder(pinId);
        else if (action === "unlink-note") this.unlinkNote(pinId);
      });
    });

    // Wire up input
    const input = this.#popover.querySelector(".cp-input") as HTMLInputElement;
    const sendBtn = this.#popover.querySelector(".cp-send") as HTMLButtonElement;

    const submitComment = () => {
      const text = input.value.trim();
      if (!text) return;
      this.addMessage(pinId, text);
      input.value = "";
    };

    sendBtn.addEventListener("click", submitComment);
    input.addEventListener("keydown", (e) => {
      if (e.key === "Enter" && !e.shiftKey) {
        e.preventDefault();
        submitComment();
      }
      if (e.key === "Escape") {
        this.closePopover();
      }
    });

    // @mention autocomplete
    input.addEventListener("input", () => this.#handleMentionInput(input));

    // Link note button
    const linkBtn = this.#popover.querySelector('[data-action="link-note"]') as HTMLButtonElement | null;
    const notePicker = this.#popover.querySelector(".cp-note-picker") as HTMLElement | null;
    if (linkBtn && notePicker) {
      linkBtn.addEventListener("click", async () => {
        if (notePicker.style.display !== "none") {
          notePicker.style.display = "none";
          return;
        }
        notePicker.innerHTML = `<div style="padding: 8px; color: #888; font-size: 12px;">Loading notes...</div>`;
        notePicker.style.display = "block";
        const notes = await this.#fetchNotes();
        if (notes.length === 0) {
          notePicker.innerHTML = `<div style="padding: 8px; color: #888; font-size: 12px;">No notes in this space</div>`;
          return;
        }
        notePicker.innerHTML = "";
        for (const note of notes) {
          const item = document.createElement("div");
          item.className = "cp-note-item";
          item.textContent = note.title || "Untitled";
          item.addEventListener("click", () => {
            this.linkNote(pinId, note.id, note.title || "Untitled");
          });
          notePicker.appendChild(item);
        }
      });
    }

    if (focusInput) {
      requestAnimationFrame(() => input.focus());
    }
  }

  // ── @Mention Autocomplete ──

  async #fetchMembers() {
    if (this.#members) return this.#members;
    try {
      const sess = JSON.parse(localStorage.getItem("encryptid_session") || "{}");
      const res = await fetch(`${getModuleApiBase("rspace")}/api/space-members`, {
        headers: sess?.accessToken ? { Authorization: `Bearer ${sess.accessToken}` } : {},
      });
      if (!res.ok) return [];
      const data = await res.json();
      this.#members = data.members || [];
      return this.#members!;
    } catch {
      return [];
    }
  }

  async #fetchNotes(): Promise<RNoteItem[]> {
    if (this.#notes) return this.#notes;
    try {
      const sess = JSON.parse(localStorage.getItem("encryptid_session") || "{}");
      const res = await fetch(`${getModuleApiBase("rnotes")}/api/notes?limit=50`, {
        headers: sess?.accessToken ? { Authorization: `Bearer ${sess.accessToken}` } : {},
      });
      if (!res.ok) return [];
      const data = await res.json();
      this.#notes = (data.notes || []).map((n: any) => ({
        id: n.id, title: n.title, type: n.type, updated_at: n.updated_at,
      }));
      return this.#notes!;
    } catch {
      return [];
    }
  }

  async #handleMentionInput(input: HTMLInputElement) {
    const val = input.value;
    const cursorPos = input.selectionStart || 0;

    // Find @ before cursor
    const before = val.slice(0, cursorPos);
    const atMatch = before.match(/@(\w*)$/);

    if (!atMatch) {
      this.#closeMentionDropdown();
      return;
    }

    const query = atMatch[1].toLowerCase();
    const members = await this.#fetchMembers();
    const filtered = members.filter(
      (m) =>
        m.username.toLowerCase().includes(query) ||
        (m.displayName && m.displayName.toLowerCase().includes(query)),
    );

    if (filtered.length === 0) {
      this.#closeMentionDropdown();
      return;
    }

    this.#showMentionDropdown(filtered, input, atMatch.index!);
  }

  #showMentionDropdown(
    members: SpaceMember[],
    input: HTMLInputElement,
    atIndex: number,
  ) {
    this.#closeMentionDropdown();

    const dropdown = document.createElement("div");
    dropdown.className = "cp-mention-dropdown";
    dropdown.style.cssText = `
      position: absolute; bottom: 100%; left: 12px; right: 12px;
      background: #2a2a3a; border: 1px solid #555; border-radius: 6px;
      max-height: 150px; overflow-y: auto; z-index: 10002;
    `;

    for (const m of members.slice(0, 8)) {
      const item = document.createElement("div");
      item.style.cssText = `
        padding: 6px 10px; cursor: pointer; font-size: 12px;
        display: flex; justify-content: space-between;
      `;
      item.innerHTML = `
        <span style="font-weight: 600;">${this.#escapeHtml(m.displayName || m.username)}</span>
        <span style="color: #888;">@${this.#escapeHtml(m.username)}</span>
      `;
      item.addEventListener("mousedown", (e) => {
        e.preventDefault(); // prevent input blur
        const val = input.value;
        const before = val.slice(0, atIndex);
        const after = val.slice(input.selectionStart || atIndex);
        input.value = `${before}@${m.username} ${after}`;
        input.focus();
        const newPos = atIndex + m.username.length + 2;
        input.setSelectionRange(newPos, newPos);
        this.#closeMentionDropdown();
      });
      item.addEventListener("mouseenter", () => {
        item.style.background = "#3a3a4a";
      });
      item.addEventListener("mouseleave", () => {
        item.style.background = "";
      });
      dropdown.appendChild(item);
    }

    const inputContainer = input.closest("div[style*='position: relative']");
    if (inputContainer) {
      inputContainer.appendChild(dropdown);
    }
    this.#mentionDropdown = dropdown;
  }

  #closeMentionDropdown() {
    if (this.#mentionDropdown) {
      this.#mentionDropdown.remove();
      this.#mentionDropdown = null;
    }
  }

  #extractMentionDids(text: string): string[] {
    const mentions = text.match(/@(\w+)/g);
    if (!mentions || !this.#members) return [];

    const dids: string[] = [];
    for (const mention of mentions) {
      const username = mention.slice(1).toLowerCase();
      const member = this.#members.find(
        (m) => m.username.toLowerCase() === username,
      );
      if (member) dids.push(member.did);
    }
    return dids;
  }

  // ── Notifications ──

  async #notifyMentions(
    pinId: string,
    authorDid: string,
    authorName: string,
    mentionedDids: string[],
  ) {
    const pins = this.#sync.doc.commentPins || {};
    const sortedPins = Object.values(pins).sort((a, b) => a.createdAt - b.createdAt);
    const pinIndex = sortedPins.findIndex((p) => p.id === pinId) + 1;

    try {
      const sess = JSON.parse(localStorage.getItem("encryptid_session") || "{}");
      await fetch(`${getModuleApiBase("rspace")}/api/comment-pins/notify-mention`, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          ...(sess?.accessToken ? { Authorization: `Bearer ${sess.accessToken}` } : {}),
        },
        body: JSON.stringify({
          pinId,
          authorDid,
          authorName,
          mentionedDids,
          pinIndex,
        }),
      });
    } catch {
      // Silent fail — notification is best-effort
    }
  }

  async #notifySpaceMembers(
    pinId: string,
    text: string,
    isReply: boolean,
    mentionedDids?: string[],
  ) {
    const did = this.#getLocalDID();
    const name = this.#getLocalUsername();
    const pins = this.#sync.doc.commentPins || {};
    const sortedPins = Object.values(pins).sort((a, b) => a.createdAt - b.createdAt);
    const pinIndex = sortedPins.findIndex((p) => p.id === pinId) + 1;

    try {
      const sess = JSON.parse(localStorage.getItem("encryptid_session") || "{}");
      await fetch(`${getModuleApiBase("rspace")}/api/comment-pins/notify`, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          ...(sess?.accessToken ? { Authorization: `Bearer ${sess.accessToken}` } : {}),
        },
        body: JSON.stringify({
          pinId,
          authorDid: did,
          authorName: name,
          text,
          pinIndex,
          isReply,
          mentionedDids: mentionedDids || [],
        }),
      });
    } catch {
      // Silent fail — notification is best-effort
    }
  }

  // ── Reminder ──

  async #createReminder(pinId: string) {
    const pins = this.#sync.doc.commentPins || {};
    const pin = pins[pinId];
    if (!pin) return;

    const sortedPins = Object.values(pins).sort((a, b) => a.createdAt - b.createdAt);
    const pinIndex = sortedPins.findIndex((p) => p.id === pinId) + 1;
    const firstMsg = pin.messages[0]?.text || "Comment pin";

    // Tomorrow 9 AM
    const tomorrow = new Date();
    tomorrow.setDate(tomorrow.getDate() + 1);
    tomorrow.setHours(9, 0, 0, 0);

    // Fetch user email
    let email: string | false = false;
    try {
      const sess = JSON.parse(localStorage.getItem("encryptid_session") || "{}");
      if (sess?.accessToken) {
        const r = await fetch("/auth/api/account/security", {
          headers: { Authorization: `Bearer ${sess.accessToken}` },
        });
        if (r.ok) {
          const d = await r.json();
          email = d.emailAddress || false;
        }
      }
    } catch {}

    try {
      const body: Record<string, any> = {
        title: `Comment #${pinIndex}: ${firstMsg.slice(0, 50)}`,
        remindAt: tomorrow.getTime(),
        allDay: true,
        syncToCalendar: true,
        sourceModule: "canvas-comments",
        sourceEntityId: pinId,
        sourceLabel: "Canvas Comment",
        sourceColor: "#8b5cf6",
      };
      if (email) body.notifyEmail = email;

      const res = await fetch(`${getModuleApiBase("rschedule")}/api/reminders`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(body),
      });

      if (res.ok) {
        // Show brief feedback in popover
        const fb = document.createElement("div");
        fb.style.cssText =
          "padding: 6px 12px; background: #1a3a1a; color: #4ade80; font-size: 12px; text-align: center;";
        fb.textContent = "✓ Reminder set for tomorrow";
        this.#popover.prepend(fb);
        setTimeout(() => fb.remove(), 2500);
      }
    } catch {
      // Silent fail
    }
  }

  // ── Helpers ──

  #getLocalDID(): string {
    try {
      const sess = JSON.parse(localStorage.getItem("encryptid_session") || "");
      return sess?.claims?.sub || sess?.claims?.did || "anonymous";
    } catch {
      return "anonymous";
    }
  }

  #getLocalUsername(): string {
    return localStorage.getItem("rspace-username") || "Anonymous";
  }

  #escapeHtml(text: string): string {
    const div = document.createElement("div");
    div.textContent = text;
    return div.innerHTML;
  }

  #formatMessageText(text: string): string {
    // Escape HTML first
    let safe = this.#escapeHtml(text);
    // Highlight @mentions
    safe = safe.replace(
      /@(\w+)/g,
      '<span style="color: #a78bfa; font-weight: 600;">@$1</span>',
    );
    return safe;
  }

  destroy() {
    document.removeEventListener("pointerdown", this.#onDocPointerDown);
    this.#pinLayer.remove();
    this.#popover.remove();
  }
}
