import type { Extension } from '@codemirror/state';
import { IndexeddbPersistence } from 'y-indexeddb';
import * as Y from 'yjs';
import { createCodeMirrorYjsBinding } from './codemirror-yjs-binding';
import {
  buildCollaborationJoinUrl,
  createCollaborationRoomDescriptor,
  parseCollaborationJoinLocation,
  type CollaborationRoomDescriptor,
} from './collaboration-link';
import { CollaborationPayloadChannel } from './collaboration-payload-channel';
import {
  CollaborationReviewStore,
  type SharedReviewAuthor,
  type SharedReviewChange,
  type SharedReviewChangeMetadata,
  type SharedReviewChangeSource,
  type SharedReviewComment,
  type SharedReviewSnapshot,
  type SharedReviewUndoSnapshot,
} from './collaboration-review';
import {
  COLLABORATION_PROTOCOL_VERSION,
  deriveCollaborationRoomKey,
  LearnitiveWebSocketProvider,
} from './learnitive-websocket-provider';
import {
  COLLABORATION_REVIEW_CAPABILITY,
  COLLABORATION_REVIEW_CAPABILITY_VERSION,
  COLLABORATION_REVIEW_SCHEMA_VERSION,
  COLLABORATION_REVIEW_TRANSACTION_META_KEY,
  COLLABORATION_REVIEW_UNDO_IDS_META_KEY,
  COLLABORATION_REVIEW_UNDO_SNAPSHOT_META_KEY,
  type CollaborationReviewMutationMeta,
} from './collaboration-capabilities';

export { createCodeMirrorYjsBinding } from './codemirror-yjs-binding';
export {
  buildCollaborationJoinUrl,
  createCollaborationRoomDescriptor,
  isValidCollaborationRoomId,
  isValidCollaborationSecret,
  parseCollaborationJoinLocation,
  type CollaborationRoomDescriptor,
} from './collaboration-link';
export { CollaborationPayloadChannel, type CollaborationPayloadRequest } from './collaboration-payload-channel';
export {
  COLLABORATION_REVIEW_MAINTENANCE_ORIGIN,
  COLLABORATION_REVIEW_SCHEMA_VERSION,
  type SharedReviewAuthor,
  type SharedReviewChange,
  type SharedReviewChangeApplicationMode,
  type SharedReviewChangeMetadata,
  type SharedReviewChangeSource,
  type SharedReviewChangeState,
  type SharedReviewChangeType,
  type SharedReviewComment,
  type SharedReviewEvent,
  type SharedReviewReply,
  type SharedReviewSnapshot,
  type SharedReviewUndoSnapshot,
} from './collaboration-review';
export { COLLABORATION_PROTOCOL_VERSION, deriveCollaborationRoomKey } from './learnitive-websocket-provider';
export {
  COLLABORATION_REVIEW_CAPABILITY,
  COLLABORATION_REVIEW_CAPABILITY_VERSION,
  type CollaborationReviewMutationMeta,
} from './collaboration-capabilities';

const PERSISTENCE_NAMESPACE = 'interactive-collaboration-v7';
export const COLLABORATIVE_SOURCE_EXTENSIONS = ['.tex', '.ltx', '.bib', '.bst', '.cls', '.sty', '.txt'] as const;
export const COLLABORATION_TEXT_ORIGIN = 'interactive-collaboration-edit';

export function collaborationTextOrigin(source: SharedReviewChangeSource = 'typing'): string {
  return source === 'typing' ? COLLABORATION_TEXT_ORIGIN : `${COLLABORATION_TEXT_ORIGIN}:${source}`;
}

const COLLABORATION_TRACKED_TEXT_ORIGINS = [
  COLLABORATION_TEXT_ORIGIN,
  'toolbar',
  'ai',
  'citation',
  'paper-memory',
  'tikz',
  'terminal',
  'research',
  'system',
].map((source) => source === COLLABORATION_TEXT_ORIGIN ? source : `${COLLABORATION_TEXT_ORIGIN}:${source}`);

export type CollaborationUser = {
  id: string;
  username: string;
  name: string;
  color: string;
  colorLight: string;
};

export type CollaborationPeer = CollaborationUser & { clientId: number };

export type CollaborationTextTransactionMeta = {
  local: boolean;
  origin: unknown;
};

export type CollaborationTextEdit = { from: number; to: number; text: string };

export type CollaborationAssetManifestEntry = { path: string; size: number; sha256: string };

export type CollaborationBinaryFileRecord = {
  size: number;
  sha256: string;
  sourceClientId: number;
  modifiedAt: number;
};

export type CollaborationJoinProgress = {
  phase: 'connecting' | 'verifying' | 'creating' | 'transferring' | 'finalizing';
  message: string;
  completedFiles?: number;
  totalFiles?: number;
  completedBytes?: number;
  totalBytes?: number;
};

export type CollaborationSessionOptions = CollaborationRoomDescriptor & {
  user?: CollaborationUser;
  persistenceKey: string;
  isHost: boolean;
  hostSessionId?: string;
  hostGrant?: string;
};

function normalizeDirectoryKey(path: string): string {
  const normalized = path.replace(/^\/+|\/+$/gu, '');
  return normalized ? `${normalized}/` : '';
}

const PERSISTENCE_REGISTRY_KEY = 'interactive-collaboration-persistence-registry';
const PERSISTENCE_RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
const PERSISTENCE_MAX_SESSIONS = 50;

type PersistenceRegistryEntry = { dbName: string; roomId: string; lastUsedAt: number };

function deleteIndexedDb(name: string): Promise<void> {
  return new Promise((resolve) => {
    try {
      const request = indexedDB.deleteDatabase(name);
      request.onsuccess = () => resolve();
      request.onerror = () => resolve();
      request.onblocked = () => resolve();
    } catch { resolve(); }
  });
}

async function registerAndCollectPersistence(dbName: string, roomId: string): Promise<void> {
  if (typeof localStorage === 'undefined' || typeof indexedDB === 'undefined') return;
  let entries: PersistenceRegistryEntry[] = [];
  try { entries = JSON.parse(localStorage.getItem(PERSISTENCE_REGISTRY_KEY) ?? '[]') as PersistenceRegistryEntry[]; } catch { entries = []; }
  const now = Date.now();
  entries = entries.filter((entry) => entry && typeof entry.dbName === 'string' && typeof entry.lastUsedAt === 'number' && entry.dbName !== dbName);
  entries.push({ dbName, roomId, lastUsedAt: now });
  entries.sort((a, b) => b.lastUsedAt - a.lastUsedAt);
  const keep = entries.filter((entry, index) => index < PERSISTENCE_MAX_SESSIONS && now - entry.lastUsedAt <= PERSISTENCE_RETENTION_MS);
  const keepNames = new Set(keep.map((entry) => entry.dbName));
  for (const entry of entries) if (!keepNames.has(entry.dbName)) await deleteIndexedDb(entry.dbName);
  localStorage.setItem(PERSISTENCE_REGISTRY_KEY, JSON.stringify(keep));
}

function forgetPersistenceRegistryEntry(dbName: string): void {
  if (typeof localStorage === 'undefined') return;
  try {
    const entries = JSON.parse(localStorage.getItem(PERSISTENCE_REGISTRY_KEY) ?? '[]') as PersistenceRegistryEntry[];
    localStorage.setItem(PERSISTENCE_REGISTRY_KEY, JSON.stringify(entries.filter((entry) => entry?.dbName !== dbName)));
  } catch { localStorage.removeItem(PERSISTENCE_REGISTRY_KEY); }
}


export class BrowserProjectCollaborationSession {
  readonly doc: Y.Doc;
  readonly provider: LearnitiveWebSocketProvider;
  private readonly files: Y.Map<Y.Text>;
  private readonly binaryFiles: Y.Map<CollaborationBinaryFileRecord>;
  private readonly directories: Y.Map<number>;
  private readonly meta: Y.Map<unknown>;
  private readonly review: CollaborationReviewStore;
  private readonly undoManagers = new Map<string, Y.UndoManager>();
  private readonly undoManagerCleanups = new Map<string, () => void>();
  private destroyed = false;

  private constructor(
    private readonly options: CollaborationSessionOptions,
    doc: Y.Doc,
    provider: LearnitiveWebSocketProvider,
    private readonly persistence: IndexeddbPersistence,
    private readonly persistenceDbName: string,
  ) {
    this.doc = doc;
    this.provider = provider;
    this.files = doc.getMap<Y.Text>('files');
    this.binaryFiles = doc.getMap<CollaborationBinaryFileRecord>('binary-files');
    this.directories = doc.getMap<number>('directories');
    this.meta = doc.getMap<unknown>('project-meta');
    this.review = new CollaborationReviewStore(doc, this.files, () => this.getLocalUser(), (path) => {
      if (path) this.undoManagers.get(path)?.stopCapturing();
      else for (const manager of this.undoManagers.values()) manager.stopCapturing();
    });
    if (options.user) this.setLocalUser(options.user);
    this.setLocalSessionIdentity();
  }

  static async create(options: CollaborationSessionOptions): Promise<BrowserProjectCollaborationSession> {
    const doc = new Y.Doc();
    const roomKey = await deriveCollaborationRoomKey(options.roomId, options.secret);
    const persistenceDbName = `${PERSISTENCE_NAMESPACE}:${options.persistenceKey}:${roomKey}`;
    const persistence = new IndexeddbPersistence(persistenceDbName, doc);
    await new Promise<void>((resolve) => {
      if (persistence.synced) return resolve();
      const onSynced = () => { persistence.off('synced', onSynced); resolve(); };
      persistence.on('synced', onSynced);
    });
    await registerAndCollectPersistence(persistenceDbName, options.roomId);
    if (options.isHost) {
      const meta = doc.getMap<unknown>('project-meta');
      doc.transact(() => {
        meta.set('collaborationProtocol', COLLABORATION_PROTOCOL_VERSION);
        meta.set('reviewSchemaVersion', COLLABORATION_REVIEW_SCHEMA_VERSION);
        meta.set('collaborationCapabilities', [COLLABORATION_REVIEW_CAPABILITY]);
        meta.set('roomId', options.roomId);
        const hostSessionId = options.hostSessionId ?? (typeof meta.get('hostSessionId') === 'string' ? String(meta.get('hostSessionId')) : crypto.randomUUID());
        meta.set('hostSessionId', hostSessionId);
        meta.set('hostStartedAt', Date.now());
        meta.set('sessionStatus', 'active');
      }, 'interactive-host-session');
    }
    const provider = await LearnitiveWebSocketProvider.create({ roomId: options.roomId, secret: options.secret, projectId: options.projectId, doc, roomKey, hostGrant: options.hostGrant });
    return new BrowserProjectCollaborationSession(options, doc, provider, persistence, persistenceDbName);
  }

  get roomId(): string { return this.options.roomId; }
  get awareness(): LearnitiveWebSocketProvider['awareness'] { return this.provider.awareness; }
  get connectionStatus(): 'connecting' | 'connected' | 'disconnected' { return this.provider.connectionStatus; }
  get connected(): boolean { return this.provider.connected; }
  get synced(): boolean { return this.provider.isSynced; }
  get roomFingerprint(): string { return this.provider.roomFingerprint; }
  get roomKey(): string { return this.provider.roomKey; }
  get hostSessionId(): string | undefined { const value = this.meta.get('hostSessionId'); return typeof value === 'string' ? value : undefined; }
  get projectStructureSealed(): boolean { return this.meta.get('structureSealed') === true; }
  get fileMap(): Y.Map<Y.Text> { return this.files; }

  subscribeConnection(listener: (status: 'connecting' | 'connected' | 'disconnected', synced: boolean) => void): () => void {
    return this.provider.subscribeStatus(listener);
  }
  waitForSync(timeoutMs?: number): Promise<boolean> { return this.provider.waitForSync(timeoutMs); }
  waitForOutboundDrain(timeoutMs?: number): Promise<boolean> { return this.provider.waitForOutboundDrain(timeoutMs); }
  createPayloadChannel(): CollaborationPayloadChannel { return new CollaborationPayloadChannel(this.provider.roomKey, this.doc.clientID); }

  setLocalUser(user: CollaborationUser): void { this.awareness.setLocalStateField('user', user); }
  getLocalUser(): CollaborationUser | undefined {
    const user = this.awareness.getLocalState()?.user as Partial<CollaborationUser> | undefined;
    if (!user?.id || !user.name || !user.username || !user.color || !user.colorLight) return undefined;
    return user as CollaborationUser;
  }
  clearLocalUser(): void { this.awareness.setLocalStateField('user', null); }
  clearLocalCursor(): void { this.awareness.setLocalStateField('cursor', null); }

  private setLocalSessionIdentity(): void {
    const hostSessionId = this.meta.get('hostSessionId');
    this.awareness.setLocalStateField('learnitiveSession', {
      protocolVersion: COLLABORATION_PROTOCOL_VERSION,
      reviewSchemaVersion: COLLABORATION_REVIEW_SCHEMA_VERSION,
      capabilities: [COLLABORATION_REVIEW_CAPABILITY],
      role: this.options.isHost ? 'host' : 'guest',
      roomId: this.options.roomId,
      ...(this.options.isHost && typeof hostSessionId === 'string' ? { hostSessionId } : {}),
    });
  }

  hasCompatibleHost(): boolean {
    if (this.isSessionStopped()) return false;
    const protocol = this.meta.get('collaborationProtocol');
    const reviewSchemaVersion = this.meta.get('reviewSchemaVersion');
    const capabilities = this.meta.get('collaborationCapabilities');
    const roomId = this.meta.get('roomId');
    const hostSessionId = this.meta.get('hostSessionId');
    if (
      protocol !== COLLABORATION_PROTOCOL_VERSION
      || reviewSchemaVersion !== COLLABORATION_REVIEW_SCHEMA_VERSION
      || !Array.isArray(capabilities)
      || !capabilities.includes(COLLABORATION_REVIEW_CAPABILITY)
      || roomId !== this.options.roomId
      || typeof hostSessionId !== 'string'
      || !hostSessionId
    ) return false;
    for (const [clientId, state] of this.awareness.getStates()) {
      if (clientId === this.doc.clientID) continue;
      const identity = state.learnitiveSession as { protocolVersion?: number; reviewSchemaVersion?: number; capabilities?: unknown; role?: string; roomId?: string; hostSessionId?: string } | undefined;
      if (
        identity?.protocolVersion === COLLABORATION_PROTOCOL_VERSION
        && identity.reviewSchemaVersion === COLLABORATION_REVIEW_SCHEMA_VERSION
        && Array.isArray(identity.capabilities)
        && identity.capabilities.includes(COLLABORATION_REVIEW_CAPABILITY)
        && identity.role === 'host'
        && identity.roomId === this.options.roomId
        && identity.hostSessionId === hostSessionId
      ) return true;
    }
    return false;
  }

  async waitForHost(timeoutMs = 10_000): Promise<boolean> {
    if (this.options.isHost || (this.hasCompatibleHost() && this.isProjectReady())) return true;
    return new Promise((resolve) => {
      let settled = false;
      const finish = (value: boolean) => {
        if (settled) return;
        settled = true;
        clearTimeout(timer);
        this.meta.unobserve(check);
        this.awareness.off('change', check);
        resolve(value);
      };
      const check = () => {
        if (this.isSessionStopped()) finish(false);
        else if (this.hasCompatibleHost() && this.isProjectReady()) finish(true);
      };
      this.meta.observe(check);
      this.awareness.on('change', check);
      const timer = setTimeout(() => finish(false), timeoutMs);
      check();
    });
  }

  stopSession(): void {
    if (!this.options.isHost || this.destroyed) return;
    this.doc.transact(() => {
      this.meta.set('sessionStatus', 'stopped');
      this.meta.set('sessionStoppedAt', Date.now());
      this.meta.set('projectReady', false);
    }, 'interactive-session-stop');
  }
  isSessionStopped(): boolean { return this.meta.get('sessionStatus') === 'stopped'; }

  getText(path: string): Y.Text {
    if (this.binaryFiles.has(path)) throw new Error(`Collaborative file is binary: ${path}`);
    const existing = this.files.get(path);
    if (existing) return existing;
    if (this.projectStructureSealed) throw new Error(`Project structure is sealed during live collaboration: ${path}`);
    const text = new Y.Text();
    this.files.set(path, text);
    return text;
  }
  hasFile(path: string): boolean { return this.files.has(path) || this.binaryFiles.has(path); }
  isTextFile(path: string): boolean { return this.files.has(path); }
  listFiles(): string[] { return [...new Set([...this.files.keys(), ...this.binaryFiles.keys()])].sort(); }
  listTextFiles(): string[] { return [...this.files.keys()].sort(); }
  listBinaryFiles(): string[] { return [...this.binaryFiles.keys()].sort(); }
  listDirectories(): string[] { return [...this.directories.keys()].sort(); }

  setInitialFile(path: string, content: string): void {
    if (this.projectStructureSealed) throw new Error('Project structure is sealed');
    if (this.hasFile(path)) return;
    const text = new Y.Text();
    if (content) text.insert(0, content);
    this.files.set(path, text);
    this.bumpSnapshotRevision();
  }
  setInitialBinaryFile(path: string, record: Omit<CollaborationBinaryFileRecord, 'modifiedAt'> & Partial<Pick<CollaborationBinaryFileRecord, 'modifiedAt'>>): void {
    if (this.projectStructureSealed) throw new Error('Project structure is sealed');
    if (this.hasFile(path)) return;
    this.binaryFiles.set(path, { ...record, modifiedAt: record.modifiedAt ?? Date.now() });
    this.bumpSnapshotRevision();
  }
  getBinaryFile(path: string): CollaborationBinaryFileRecord | undefined {
    const record = this.binaryFiles.get(path);
    return record ? { ...record } : undefined;
  }
  setInitialDirectory(path: string): void {
    if (this.projectStructureSealed) throw new Error('Project structure is sealed');
    const directory = normalizeDirectoryKey(path);
    if (!directory || this.directories.has(directory)) return;
    this.directories.set(directory, Date.now());
    this.bumpSnapshotRevision();
  }
  getSnapshotRevision(): number {
    const value = this.meta.get('snapshotRevision');
    return typeof value === 'number' && Number.isFinite(value) ? value : 0;
  }
  private bumpSnapshotRevision(): void { this.meta.set('snapshotRevision', this.getSnapshotRevision() + 1); }

  setRootFile(path: string | undefined): void { if (this.projectStructureSealed) throw new Error('Project structure is sealed'); if (path) this.meta.set('rootFile', path); else this.meta.delete('rootFile'); }
  getRootFile(): string | undefined { const value = this.meta.get('rootFile'); return typeof value === 'string' ? value : undefined; }
  setProjectName(name: string): void { if (this.projectStructureSealed) throw new Error('Project structure is sealed'); const value = name.trim().slice(0, 120); if (value) this.meta.set('projectName', value); }
  getProjectName(): string | undefined { const value = this.meta.get('projectName'); return typeof value === 'string' && value.trim() ? value.trim().slice(0, 120) : undefined; }
  setProjectReady(ready: boolean): void {
    this.doc.transact(() => {
      this.meta.set('projectReady', ready);
      if (ready) {
        this.meta.set('projectReadyAt', Date.now());
        this.meta.set('structureSealed', true);
        this.meta.set('snapshotManifest', {
          revision: this.getSnapshotRevision(),
          rootFile: this.getRootFile(),
          textFiles: this.listTextFiles(),
          binaryFiles: this.listBinaryFiles(),
          directories: this.listDirectories(),
        });
      } else this.meta.delete('projectReadyAt');
    }, 'interactive-project-ready');
  }
  isProjectReady(): boolean { return this.meta.get('projectReady') === true && !this.isSessionStopped(); }
  setAssetManifest(entries: readonly CollaborationAssetManifestEntry[]): void { if (this.projectStructureSealed) throw new Error('Project structure is sealed'); this.meta.set('assetManifest', entries.map((entry) => ({ ...entry }))); }

  getUndoManager(path: string): Y.UndoManager {
    let manager = this.undoManagers.get(path);
    if (!manager) {
      manager = new Y.UndoManager(this.getText(path), { captureTimeout: 1750 });
      for (const origin of COLLABORATION_TRACKED_TEXT_ORIGINS) manager.trackedOrigins.add(origin);
      const attachReviewIds = (event: { stackItem: { meta: Map<unknown, unknown> }; type: 'undo' | 'redo' }) => {
        if (event.type !== 'undo') return;
        const ids = this.review.consumeUndoChangeIds(path);
        if (!ids.length) return;
        const existing = event.stackItem.meta.get(COLLABORATION_REVIEW_UNDO_IDS_META_KEY);
        const merged = new Set<string>(Array.isArray(existing) ? existing.filter((item): item is string => typeof item === 'string') : []);
        for (const id of ids) merged.add(id);
        event.stackItem.meta.set(COLLABORATION_REVIEW_UNDO_IDS_META_KEY, [...merged]);
      };
      manager.on('stack-item-added', attachReviewIds);
      manager.on('stack-item-updated', attachReviewIds);
      this.undoManagerCleanups.set(path, () => {
        manager?.off('stack-item-added', attachReviewIds);
        manager?.off('stack-item-updated', attachReviewIds);
      });
      this.undoManagers.set(path, manager);
    }
    return manager;
  }
  createCodeMirrorExtension(path: string): Extension {
    this.clearLocalCursor();
    return createCodeMirrorYjsBinding(this.getText(path), this.awareness, this.getUndoManager(path));
  }
  canUndo(path: string): boolean { return this.getUndoManager(path).undoStack.length > 0; }
  canRedo(path: string): boolean { return this.getUndoManager(path).redoStack.length > 0; }
  undo(path: string): boolean {
    const manager = this.getUndoManager(path);
    const stackItem = manager.undoStack.at(-1) as { meta: Map<unknown, unknown> } | undefined;
    if (!stackItem) return false;
    const idsValue = stackItem.meta.get(COLLABORATION_REVIEW_UNDO_IDS_META_KEY);
    const ids = Array.isArray(idsValue) ? idsValue.filter((item): item is string => typeof item === 'string') : [];
    const snapshot = this.review.captureUndoRecords(path, ids);
    if (snapshot.records.length) stackItem.meta.set(COLLABORATION_REVIEW_UNDO_SNAPSHOT_META_KEY, snapshot);
    manager.undo();
    if (snapshot.records.length) this.review.removeUndoRecords(path, snapshot);
    return true;
  }
  redo(path: string): boolean {
    const manager = this.getUndoManager(path);
    const stackItem = manager.redoStack.at(-1) as { meta: Map<unknown, unknown> } | undefined;
    if (!stackItem) return false;
    const snapshot = stackItem.meta.get(COLLABORATION_REVIEW_UNDO_SNAPSHOT_META_KEY) as SharedReviewUndoSnapshot | undefined;
    manager.redo();
    if (snapshot?.records.length) this.review.restoreUndoRecords(path, snapshot);
    return true;
  }

  setReviewTrackChangesEnabled(enabled: boolean): void {
    if (this.review.getTrackChangesEnabled() !== enabled) this.stopReviewChangeCoalescing();
    this.review.setTrackChangesEnabled(enabled);
  }
  getReviewTrackChangesEnabled(): boolean { return this.review.getTrackChangesEnabled(); }
  stopReviewChangeCoalescing(path?: string): void { this.review.stopCoalescing(path); }
  getReviewSnapshot(): SharedReviewSnapshot { return this.review.getSnapshot(); }
  subscribeReview(listener: (snapshot: SharedReviewSnapshot) => void): () => void { return this.review.subscribe(listener); }
  createReviewComment(input: { filePath?: string; from?: number; to?: number; body: string; scope?: SharedReviewComment['scope']; quotedText?: string }): SharedReviewComment {
    return this.review.createComment(input);
  }
  proposeDeferredReviewChange(input: {
    filePath: string;
    from: number;
    to: number;
    originalText: string;
    proposedText: string;
    author: SharedReviewAuthor;
    source?: 'ai' | 'system';
    semanticKind?: string;
    operationId?: string;
    metadata?: SharedReviewChangeMetadata;
  }): SharedReviewChange { return this.review.proposeDeferredChange(input); }
  replyReviewComment(id: string, body: string): boolean { return this.review.reply(id, body); }
  editReviewComment(id: string, body: string): boolean { return this.review.editOwnComment(id, body); }
  deleteReviewComment(id: string): boolean { return this.review.deleteOwnComment(id); }
  setReviewCommentResolved(id: string, resolved: boolean): boolean { return this.review.setCommentResolved(id, resolved); }
  acceptReviewChange(id: string): boolean { return this.review.acceptChange(id); }
  rejectReviewChange(id: string): boolean { return this.review.rejectChange(id); }
  resolveReviewConflictKeepingCurrent(id: string): boolean { return this.review.resolveChangeKeepingCurrent(id); }
  resolveReviewConflictRestoringOriginal(id: string): boolean { return this.review.resolveChangeRestoringOriginal(id); }
  dismissReviewConflict(id: string): boolean { return this.review.dismissConflict(id); }

  getTextSnapshot(path: string): string | undefined { return this.files.get(path)?.toString(); }
  applyTextEdits(
    path: string,
    edits: readonly CollaborationTextEdit[],
    origin: unknown = COLLABORATION_TEXT_ORIGIN,
    reviewMutation?: CollaborationReviewMutationMeta,
  ): boolean {
    const text = this.files.get(path);
    if (!text || edits.length === 0) return false;
    if (origin !== COLLABORATION_TEXT_ORIGIN) this.stopReviewChangeCoalescing(path);
    const normalized = edits.map((edit) => {
      const from = Math.max(0, Math.min(text.length, Math.trunc(edit.from)));
      const to = Math.max(0, Math.min(text.length, Math.trunc(edit.to)));
      return { from: Math.min(from, to), to: Math.max(from, to), text: edit.text };
    }).sort((a, b) => b.from - a.from || b.to - a.to);
    let previousFrom = text.length + 1;
    for (const edit of normalized) { if (edit.to > previousFrom) return false; previousFrom = edit.from; }
    this.doc.transact((transaction) => {
      if (reviewMutation) transaction.meta.set(COLLABORATION_REVIEW_TRANSACTION_META_KEY, reviewMutation);
      for (const edit of normalized) {
        if (edit.to > edit.from) text.delete(edit.from, edit.to - edit.from);
        if (edit.text) text.insert(edit.from, edit.text);
      }
    }, origin);
    return true;
  }

  subscribeTextTransactions(path: string, listener: { before?: (meta: CollaborationTextTransactionMeta) => void; after?: (text: string, meta: CollaborationTextTransactionMeta) => void }): () => void {
    const text = this.files.get(path);
    if (!text) return () => undefined;
    const before = (transaction: Y.Transaction) => listener.before?.({ local: transaction.local, origin: transaction.origin });
    const after = (_event: Y.YTextEvent, transaction: Y.Transaction) => listener.after?.(text.toString(), { local: transaction.local, origin: transaction.origin });
    this.doc.on('beforeTransaction', before);
    text.observe(after);
    return () => { this.doc.off('beforeTransaction', before); text.unobserve(after); };
  }
  subscribeFiles(listener: (event: Y.YMapEvent<Y.Text>) => void): () => void { this.files.observe(listener); return () => this.files.unobserve(listener); }
  subscribeMeta(listener: () => void): () => void { this.meta.observe(listener); return () => this.meta.unobserve(listener); }
  subscribeAwareness(listener: () => void): () => void { this.awareness.on('change', listener); return () => this.awareness.off('change', listener); }

  getPeers(): CollaborationPeer[] {
    const peers: CollaborationPeer[] = [];
    for (const [clientId, state] of this.awareness.getStates()) {
      if (clientId === this.doc.clientID) continue;
      const user = state.user as Partial<CollaborationUser> | undefined;
      if (!user?.id || !user.name || !user.username || !user.color || !user.colorLight) continue;
      peers.push({ clientId, id: user.id, username: user.username, name: user.name, color: user.color, colorLight: user.colorLight });
    }
    return peers.sort((a, b) => a.name.localeCompare(b.name));
  }

  async getDiagnostics(): Promise<{
    protocolVersion: number; reviewSchemaVersion: number; reviewCapabilityVersion: number; roomFingerprint: string; yjsClientId: number;
    connectionStatus: 'connecting' | 'connected' | 'disconnected'; connected: boolean; synced: boolean;
    hostMarkerPresent: boolean; projectReady: boolean; sessionStopped: boolean; awarenessPeerCount: number; stateVectorFingerprint: string;
  }> {
    const stateVector = new Uint8Array(Y.encodeStateVector(this.doc));
    const digest = await crypto.subtle.digest('SHA-256', stateVector);
    const stateVectorFingerprint = Array.from(new Uint8Array(digest).slice(0, 4), (value) => value.toString(16).padStart(2, '0')).join('');
    return {
      protocolVersion: COLLABORATION_PROTOCOL_VERSION,
      reviewSchemaVersion: COLLABORATION_REVIEW_SCHEMA_VERSION,
      reviewCapabilityVersion: COLLABORATION_REVIEW_CAPABILITY_VERSION,
      roomFingerprint: this.roomFingerprint,
      yjsClientId: this.doc.clientID,
      connectionStatus: this.connectionStatus,
      connected: this.connected,
      synced: this.synced,
      hostMarkerPresent: this.options.isHost || this.hasCompatibleHost(),
      projectReady: this.isProjectReady(),
      sessionStopped: this.isSessionStopped(),
      awarenessPeerCount: Math.max(0, this.awareness.getStates().size - 1),
      stateVectorFingerprint,
    };
  }

  async persistNow(): Promise<void> { await this.persistence.set('learnitiveLastFlushAt', Date.now()); }

  async destroy(options: { clearPersistence?: boolean } = {}): Promise<void> {
    if (this.destroyed) return;
    this.destroyed = true;
    this.clearLocalCursor();
    this.clearLocalUser();
    this.review.destroy();
    for (const cleanup of this.undoManagerCleanups.values()) cleanup();
    this.undoManagerCleanups.clear();
    this.undoManagers.clear();
    this.provider.destroy();
    await this.persistence.destroy();
    this.doc.destroy();
    if (options.clearPersistence) {
      forgetPersistenceRegistryEntry(this.persistenceDbName);
      await deleteIndexedDb(this.persistenceDbName);
    }
  }
}

export { Y };
