import { BaseService, type BaseServiceContext } from '@learnitive/base-utils';
import { SERVICE_NAME, WORKSPACE_STORAGE_TYPE } from '@learnitive/constants';
import {
  BrowserProjectCollaborationSession,
  COLLABORATION_REVIEW_MAINTENANCE_ORIGIN,
  COLLABORATION_REVIEW_CAPABILITY_VERSION,
  COLLABORATION_PROTOCOL_VERSION,
  COLLABORATION_REVIEW_SCHEMA_VERSION,
  collaborationTextOrigin,
  COLLABORATIVE_SOURCE_EXTENSIONS,
  buildCollaborationJoinUrl,
  createCollaborationRoomDescriptor,
  deriveCollaborationRoomKey,
  parseCollaborationJoinLocation,
  type CollaborationAssetManifestEntry,
  type CollaborationBinaryFileRecord,
  type CollaborationJoinProgress,
  type CollaborationPayloadChannel,
  type CollaborationPayloadRequest,
  type CollaborationPeer,
  type CollaborationRoomDescriptor,
  type CollaborationTextEdit,
  type CollaborationTextTransactionMeta,
  type CollaborationUser,
  type SharedReviewAuthor,
  type SharedReviewChange,
  type SharedReviewChangeMetadata,
  type SharedReviewChangeSource,
  type SharedReviewComment,
  type SharedReviewSnapshot,
} from '@learnitive/project-collaboration';
import {
  copyBytesToArrayBuffer,
  isLearnitiveEditableTextPath,
  projectDirectoryPath,
  projectFilePath,
  requestPersistentBrowserStorage,
  safeProjectFilePath,
  supportsOpfsProjectStorage,
} from '@learnitive/project-storage';
import { projectModeForProfile } from '@learnitive/project-runtime';
import type { WorkspaceInfo } from '@learnitive/types';
import { WsFilePath, WsPath } from '@learnitive/ws-path';
import { atom } from 'jotai';
import type { FileSystemService } from './file-system-service';
import type { LatexSourceService } from './latex-source-service';
import type { ProjectStorageService } from './project-storage-service';
import type { ProjectSemanticService } from './project-semantic-service';
import type { WorkspaceOpsService } from './workspace-ops-service';

const USER_COLORS = [
  ['#ef4444', '#ef444433'], ['#14b8a6', '#14b8a633'], ['#f59e0b', '#f59e0b33'],
  ['#a855f7', '#a855f733'], ['#22c55e', '#22c55e33'], ['#3b82f6', '#3b82f633'],
  ['#ec4899', '#ec489933'], ['#eab308', '#eab30833'], ['#06b6d4', '#06b6d433'],
  ['#8b5cf6', '#8b5cf633'], ['#f97316', '#f9731633'], ['#10b981', '#10b98133'],
  ['#6366f1', '#6366f133'], ['#d946ef', '#d946ef33'], ['#84cc16', '#84cc1633'],
  ['#0ea5e9', '#0ea5e933'], ['#f43f5e', '#f43f5e33'], ['#64748b', '#64748b33'],
  ['#7c3aed', '#7c3aed33'], ['#059669', '#05966933'],
] as const;
const COLLAB_USER_ID_KEY = 'interactive-collaboration-user-id';
const COLLAB_USER_NAME_KEY = 'interactive-collaboration-name';
const COLLAB_USER_COLOR_KEY = 'interactive-collaboration-color';
const JOINED_COLLABORATION_METADATA_KEY = 'learnitiveCollaboration';
const RECONNECT_DESCRIPTOR_PREFIX = 'interactive-collaboration-descriptor:';
const SNAPSHOT_BASELINE_PREFIX = 'interactive-collaboration-snapshot:';
const RECOVERY_DIRECTORY = '.interactive-local-recovery';
const SNAPSHOT_BASELINE_RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
const TEXT_ENCODER = new TextEncoder();
const STRICT_TEXT_DECODER = new TextDecoder('utf-8', { fatal: true });

async function sha256Hex(data: Uint8Array): Promise<string> {
  const digest = await crypto.subtle.digest('SHA-256', copyBytesToArrayBuffer(data));
  return Array.from(new Uint8Array(digest), (value) => value.toString(16).padStart(2, '0')).join('');
}

function decodeCollaborativeText(path: string, data: Uint8Array): string | undefined {
  if (!isLearnitiveEditableTextPath(path) || data.includes(0)) return undefined;
  try { return STRICT_TEXT_DECODER.decode(data); } catch { return undefined; }
}

function wsPathFor(wsName: string, path: string): string {
  return WsFilePath.fromString(`${wsName}:${path}`).wsPath;
}

function isTx7SourcePath(path: string): boolean {
  const lower = path.toLowerCase();
  return COLLABORATIVE_SOURCE_EXTENSIONS.some((extension) => lower.endsWith(extension));
}

type CollaborationHostGrantResponse = { grant: string; expiresAt: string };
type CollaborationAuthResponse = { authenticated: false } | { authenticated: true; csrfToken: string };

async function requestCollaborationGrant(roomId: string, secret: string, projectId: string, mode: 'host' | 'join' = 'host'): Promise<string> {
  const authResponse = await fetch('/api/auth/me', { credentials: 'same-origin', cache: 'no-store', headers: { Accept: 'application/json' } });
  const auth = await authResponse.json().catch(() => ({ authenticated: false })) as CollaborationAuthResponse;
  if (!authResponse.ok || !auth.authenticated) throw new Error('Live collaboration requires a signed-in Learnitive account.');
  const roomKey = await deriveCollaborationRoomKey(roomId, secret);
  const grantResponse = await fetch('/api/collaboration/host-grant', {
    method: 'POST',
    credentials: 'same-origin',
    cache: 'no-store',
    headers: { Accept: 'application/json', 'Content-Type': 'application/json', 'x-learnitive-csrf': auth.csrfToken },
    body: JSON.stringify({ roomKey, reviewVersion: COLLABORATION_REVIEW_CAPABILITY_VERSION, projectId, mode }),
  });
  const payload = await grantResponse.json().catch(() => ({})) as Partial<CollaborationHostGrantResponse> & { error?: string };
  if (!grantResponse.ok || typeof payload.grant !== 'string' || !payload.grant) {
    if (grantResponse.status === 403 && mode === 'host') throw new Error('An active Pro plan is required to start and host a collaboration session.');
    if (grantResponse.status === 401) throw new Error('Live collaboration requires a signed-in Learnitive account.');
    throw new Error('Unable to authorize this collaboration session. Please try again.');
  }
  return payload.grant;
}

function singleTextDiff(current: string, next: string): CollaborationTextEdit | undefined {
  if (current === next) return undefined;
  let prefix = 0;
  const maxPrefix = Math.min(current.length, next.length);
  while (prefix < maxPrefix && current.charCodeAt(prefix) === next.charCodeAt(prefix)) prefix += 1;
  let currentEnd = current.length;
  let nextEnd = next.length;
  while (currentEnd > prefix && nextEnd > prefix && current.charCodeAt(currentEnd - 1) === next.charCodeAt(nextEnd - 1)) {
    currentEnd -= 1;
    nextEnd -= 1;
  }
  return { from: prefix, to: currentEnd, text: next.slice(prefix, nextEnd) };
}

export type ProjectCollaborationState = {
  wsName?: string;
  active: boolean;
  connected: boolean;
  connectionPhase: 'connecting' | 'synchronizing' | 'live' | 'reconnecting' | 'offline';
  isHost: boolean;
  roomId?: string;
  shareUrl?: string;
  userName?: string;
  userColor?: string;
  userColorLight?: string;
  peers: readonly CollaborationPeer[];
  statusMessage?: string;
  documentRevision: number;
};

const EMPTY_STATE: ProjectCollaborationState = {
  active: false,
  connected: false,
  connectionPhase: 'offline',
  isHost: false,
  peers: [],
  documentRevision: 0,
};

type ActiveSession = {
  wsName: string;
  isHost: boolean;
  descriptor: CollaborationRoomDescriptor;
  shareUrl: string;
  session: BrowserProjectCollaborationSession;
  payloadChannel: CollaborationPayloadChannel;
  cleanups: Array<() => void>;
  fileCleanups: Map<string, () => void>;
  storageWrites: Map<string, Promise<void>>;
  semanticRefreshTimer?: ReturnType<typeof setTimeout>;
};

type JoinedCollaborationMetadata = {
  protocolVersion: number;
  reviewSchemaVersion: number;
  roomId: string;
  source: 'joined';
};

type ReconnectDescriptor = CollaborationRoomDescriptor & {
  protocolVersion: number;
  reviewSchemaVersion: number;
  role: 'host' | 'guest';
  hostSessionId?: string;
};

type SnapshotBaseline = {
  roomId: string;
  capturedAt: number;
  files: Record<string, string>;
  directories: string[];
};

export type JoinedCollaborationProject = { wsName: string; rootFile?: string; reused: boolean };

function joinedCollaborationMetadata(workspace: WorkspaceInfo): JoinedCollaborationMetadata | undefined {
  const value = workspace.metadata?.[JOINED_COLLABORATION_METADATA_KEY];
  if (typeof value !== 'object' || value === null) return undefined;
  const candidate = value as Partial<JoinedCollaborationMetadata>;
  if (typeof candidate.protocolVersion !== 'number'
    || typeof candidate.reviewSchemaVersion !== 'number'
    || typeof candidate.roomId !== 'string'
    || candidate.source !== 'joined') return undefined;
  return {
    protocolVersion: candidate.protocolVersion,
    reviewSchemaVersion: candidate.reviewSchemaVersion,
    roomId: candidate.roomId,
    source: 'joined',
  };
}

/**
 * TX7 collaboration orchestration. Y.Text is authoritative while the session
 * is active. CodeMirror binds directly through y-codemirror.next and this
 * service only mirrors Y.Text outward to Learnitive source/storage consumers.
 */
export class ProjectCollaborationService extends BaseService {
  static deps = ['fileSystem', 'latexSource', 'projectStorage', 'projectSemantic', 'workspaceOps'] as const;
  readonly $state = atom<ProjectCollaborationState>(EMPTY_STATE);
  private active?: ActiveSession;

  constructor(
    context: BaseServiceContext,
    private readonly dependencies: {
      fileSystem: FileSystemService;
      latexSource: LatexSourceService;
      projectStorage: ProjectStorageService;
      projectSemantic: ProjectSemanticService;
      workspaceOps: WorkspaceOpsService;
    },
  ) {
    super(SERVICE_NAME.projectCollaborationService, context, dependencies);
  }

  async hookMount(): Promise<void> {
    this.addCleanup(() => { void this.leave(); });
  }

  isActive(wsName?: string): boolean {
    return Boolean(this.active && (wsName === undefined || this.active.wsName === wsName));
  }

  isSourcePathCollaborative(wsName: string, path: string): boolean {
    const active = this.active;
    return Boolean(active && active.wsName === wsName && isTx7SourcePath(path) && active.session.isTextFile(path));
  }

  getCodeMirrorExtension(wsName: string, path: string) {
    const active = this.active;
    if (!active || active.wsName !== wsName || !isTx7SourcePath(path) || !active.session.isTextFile(path)) return undefined;
    return active.session.createCodeMirrorExtension(path);
  }

  clearCollaborativeCursor(wsName: string): void {
    if (this.active?.wsName === wsName) this.active.session.clearLocalCursor();
  }

  getCollaborativeText(wsName: string, path: string): string | undefined {
    const active = this.active;
    if (!active || active.wsName !== wsName || !active.session.isTextFile(path)) return undefined;
    return active.session.getTextSnapshot(path);
  }

  getCanonicalSourceText(wsName: string, path: string): string | undefined {
    const collaborative = this.getCollaborativeText(wsName, path);
    if (collaborative !== undefined) return collaborative;
    return this.dependencies.latexSource.getSnapshot(wsPathFor(wsName, path))?.text;
  }

  applyTextEdits(
    wsName: string,
    path: string,
    edits: readonly CollaborationTextEdit[],
    options: { reviewSource?: SharedReviewChangeSource; trackReview?: boolean; semanticKind?: string; operationId?: string } = {},
  ): boolean {
    const active = this.active;
    if (!active || active.wsName !== wsName || !isTx7SourcePath(path) || !active.session.isTextFile(path)) return false;
    const source = options.reviewSource ?? 'typing';
    const origin = options.trackReview === false
      ? COLLABORATION_REVIEW_MAINTENANCE_ORIGIN
      : collaborationTextOrigin(source);
    const reviewMutation = options.trackReview === false ? undefined : {
      source,
      ...(options.semanticKind ? { semanticKind: options.semanticKind } : {}),
      operationId: options.operationId ?? crypto.randomUUID(),
    };
    return active.session.applyTextEdits(path, edits, origin, reviewMutation);
  }

  replaceCollaborativeText(
    wsName: string,
    path: string,
    nextText: string,
    options: { reviewSource?: SharedReviewChangeSource; trackReview?: boolean; semanticKind?: string; operationId?: string } = {},
  ): boolean {
    const active = this.active;
    if (!active || active.wsName !== wsName || !active.session.isTextFile(path)) return false;
    const current = active.session.getTextSnapshot(path);
    if (current === undefined) return false;
    const edit = singleTextDiff(current, nextText);
    if (!edit) return true;
    const source = options.reviewSource ?? 'typing';
    const origin = options.trackReview === false
      ? COLLABORATION_REVIEW_MAINTENANCE_ORIGIN
      : collaborationTextOrigin(source);
    const reviewMutation = options.trackReview === false ? undefined : {
      source,
      ...(options.semanticKind ? { semanticKind: options.semanticKind } : {}),
      operationId: options.operationId ?? crypto.randomUUID(),
    };
    return active.session.applyTextEdits(path, [edit], origin, reviewMutation);
  }

  subscribeTextTransactions(
    wsName: string,
    path: string,
    listener: { before?: (meta: CollaborationTextTransactionMeta) => void; after?: (text: string, meta: CollaborationTextTransactionMeta) => void },
  ): () => void {
    const active = this.active;
    if (!active || active.wsName !== wsName || !active.session.isTextFile(path)) return () => undefined;
    return active.session.subscribeTextTransactions(path, listener);
  }

  canUndoCollaborativeText(wsName: string, path: string): boolean {
    const active = this.active;
    return Boolean(active && active.wsName === wsName && active.session.isTextFile(path) && active.session.canUndo(path));
  }
  canRedoCollaborativeText(wsName: string, path: string): boolean {
    const active = this.active;
    return Boolean(active && active.wsName === wsName && active.session.isTextFile(path) && active.session.canRedo(path));
  }
  undoCollaborativeText(wsName: string, path: string): boolean {
    const active = this.active;
    return Boolean(active && active.wsName === wsName && active.session.isTextFile(path) && active.session.undo(path));
  }
  redoCollaborativeText(wsName: string, path: string): boolean {
    const active = this.active;
    return Boolean(active && active.wsName === wsName && active.session.isTextFile(path) && active.session.redo(path));
  }

  getLocalCollaborationUser(wsName: string): CollaborationUser | undefined {
    const active = this.active;
    return active?.wsName === wsName ? active.session.getLocalUser() : undefined;
  }
  setReviewTrackChangesEnabled(wsName: string, enabled: boolean): void {
    const active = this.active;
    if (active?.wsName === wsName) active.session.setReviewTrackChangesEnabled(enabled);
  }
  stopReviewChangeCoalescing(wsName: string, path?: string): void {
    const active = this.active;
    if (active?.wsName === wsName) active.session.stopReviewChangeCoalescing(path);
  }
  getSharedReviewSnapshot(wsName: string): SharedReviewSnapshot | undefined {
    const active = this.active;
    return active?.wsName === wsName ? active.session.getReviewSnapshot() : undefined;
  }
  subscribeSharedReview(wsName: string, listener: (snapshot: SharedReviewSnapshot) => void): () => void {
    const active = this.active;
    return active?.wsName === wsName ? active.session.subscribeReview(listener) : () => undefined;
  }
  createSharedReviewComment(wsName: string, input: { filePath?: string; from?: number; to?: number; body: string; scope?: SharedReviewComment['scope']; quotedText?: string }): SharedReviewComment | undefined {
    const active = this.active;
    return active?.wsName === wsName ? active.session.createReviewComment(input) : undefined;
  }
  proposeDeferredSharedReviewChange(wsName: string, input: {
    filePath: string;
    from: number;
    to: number;
    originalText: string;
    proposedText: string;
    author: SharedReviewAuthor;
    source?: 'ai' | 'system';
    semanticKind?: string;
    operationId?: string;
    metadata?: SharedReviewChangeMetadata;
  }): SharedReviewChange | undefined {
    const active = this.active;
    return active?.wsName === wsName ? active.session.proposeDeferredReviewChange(input) : undefined;
  }
  replySharedReviewComment(wsName: string, id: string, body: string): boolean {
    return Boolean(this.active?.wsName === wsName && this.active.session.replyReviewComment(id, body));
  }
  editSharedReviewComment(wsName: string, id: string, body: string): boolean {
    return Boolean(this.active?.wsName === wsName && this.active.session.editReviewComment(id, body));
  }
  deleteSharedReviewComment(wsName: string, id: string): boolean {
    return Boolean(this.active?.wsName === wsName && this.active.session.deleteReviewComment(id));
  }
  setSharedReviewCommentResolved(wsName: string, id: string, resolved: boolean): boolean {
    return Boolean(this.active?.wsName === wsName && this.active.session.setReviewCommentResolved(id, resolved));
  }
  acceptSharedReviewChange(wsName: string, id: string): boolean {
    return Boolean(this.active?.wsName === wsName && this.active.session.acceptReviewChange(id));
  }
  rejectSharedReviewChange(wsName: string, id: string): boolean {
    return Boolean(this.active?.wsName === wsName && this.active.session.rejectReviewChange(id));
  }
  keepCurrentSharedReviewConflict(wsName: string, id: string): boolean {
    return Boolean(this.active?.wsName === wsName && this.active.session.resolveReviewConflictKeepingCurrent(id));
  }
  restoreOriginalSharedReviewConflict(wsName: string, id: string): boolean {
    return Boolean(this.active?.wsName === wsName && this.active.session.resolveReviewConflictRestoringOriginal(id));
  }
  dismissSharedReviewConflict(wsName: string, id: string): boolean {
    return Boolean(this.active?.wsName === wsName && this.active.session.dismissReviewConflict(id));
  }

  async start(
    wsName: string,
    input: { userName?: string; userId?: string; username?: string } = {},
  ): Promise<string> {
    const workspace = await this.dependencies.workspaceOps.getWorkspaceInfo(wsName);
    const projectId = typeof workspace?.metadata?.projectId === 'string' ? workspace.metadata.projectId : '';
    if (!projectId) throw new Error('Live collaboration requires a local Learnitive project identity.');
    const descriptor = createCollaborationRoomDescriptor(projectId);
    const hostGrant = await requestCollaborationGrant(descriptor.roomId, descriptor.secret, descriptor.projectId, 'host');
    await this.leave();
    await this.dependencies.latexSource.flushWorkspace(wsName);
    const user = this.getAndStoreUserIdentity(input);
    const session = await BrowserProjectCollaborationSession.create({
      ...descriptor,
      user,
      persistenceKey: `${wsName}:${descriptor.roomId}`,
      isHost: true,
      hostGrant,
    });
    session.setProjectName(wsName);
    session.setProjectReady(false);

    const provider = await this.dependencies.projectStorage.getProvider(wsName);
    const entries = await provider.listEntries();
    const binaryManifest: CollaborationAssetManifestEntry[] = [];
    for (const entry of entries) {
      if (entry.path === RECOVERY_DIRECTORY || entry.path.startsWith(`${RECOVERY_DIRECTORY}/`)) continue;
      if (entry.kind === 'directory') { session.setInitialDirectory(entry.path); continue; }
      if (entry.kind !== 'file') continue;
      const data = await provider.readFile(entry.path);
      if (!data) continue;
      const text = decodeCollaborativeText(entry.path, data);
      if (text !== undefined) {
        session.setInitialFile(entry.path, text);
      } else {
        const sha256 = await sha256Hex(data);
        session.setInitialBinaryFile(entry.path, { size: data.byteLength, sha256, sourceClientId: session.doc.clientID });
        binaryManifest.push({ path: entry.path, size: data.byteLength, sha256 });
      }
    }
    let profile = await this.dependencies.projectStorage.getProjectProfile(wsName);
    if (!profile) {
      const detected = await this.dependencies.projectStorage.detectProjectTargets(wsName).catch(() => []);
      if (detected[0]?.profile) profile = await this.dependencies.projectStorage.updateProjectProfile(wsName, detected[0].profile);
    }
    const mode = projectModeForProfile(profile);
    if (mode?.id === 'latex') {
      const config = await this.dependencies.projectStorage.getProjectConfig(wsName);
      session.setRootFile(config.rootFile);
    } else {
      session.setRootFile(profile?.entry || mode?.defaultEntryFile);
    }
    session.setAssetManifest(binaryManifest);

    if (!(await session.waitForSync())) {
      await session.destroy();
      throw new Error('Collaboration relay did not synchronize. Check /collab-live and try again.');
    }
    const shareUrl = buildCollaborationJoinUrl(descriptor);
    await this.activate(wsName, true, descriptor, shareUrl, session);
    session.setProjectReady(true);
    this.storeReconnectDescriptor(wsName, {
      ...descriptor,
      protocolVersion: COLLABORATION_PROTOCOL_VERSION,
      reviewSchemaVersion: COLLABORATION_REVIEW_SCHEMA_VERSION,
      role: 'host',
      hostSessionId: session.hostSessionId,
    });
    this.publishState();
    return shareUrl;
  }

  async joinNewProject(
    shareUrl: string,
    input: {
      userName?: string;
      userId?: string;
      username?: string;
      requestAnonymousName?: (projectName: string) => Promise<string | undefined>;
      onProgress?: (progress: CollaborationJoinProgress) => void;
    } = {},
  ): Promise<JoinedCollaborationProject> {
    let descriptor: CollaborationRoomDescriptor | undefined;
    try {
      const url = new URL(shareUrl, typeof window === 'undefined' ? 'https://www.learnitive.com' : window.location.origin);
      descriptor = parseCollaborationJoinLocation({ pathname: url.pathname, hash: url.hash });
    } catch { descriptor = undefined; }
    if (!descriptor) throw new Error('Invalid or expired collaboration project link.');
    await this.leave();
    input.onProgress?.({ phase: 'connecting', message: 'Connecting' });
    const joinGrant = await requestCollaborationGrant(descriptor.roomId, descriptor.secret, descriptor.projectId, 'join');
    const session = await BrowserProjectCollaborationSession.create({
      ...descriptor,
      persistenceKey: `joined:${descriptor.roomId}`,
      isHost: false,
      hostGrant: joinGrant,
    });

    let createdWorkspace: string | undefined;
    let activated = false;
    try {
      if (!(await session.waitForSync())) throw new Error('Collaboration relay did not synchronize. Check /collab-live and try again.');
      input.onProgress?.({ phase: 'verifying', message: 'Loading project' });
      if (!(await session.waitForHost())) {
        throw new Error(`Unable to join project. The host is offline or is using an incompatible collaboration-review version (room ${session.roomFingerprint}).`);
      }
      if (session.isSessionStopped()) throw new Error('This collaboration session has ended. Ask the host for a new project link.');

      let userName = input.userName?.trim();
      if (!userName && !localStorage.getItem(COLLAB_USER_NAME_KEY) && input.requestAnonymousName) {
        userName = (await input.requestAnonymousName(session.getProjectName() ?? 'Shared project'))?.trim();
        if (!userName) throw new Error('A collaborator name is required to join this project.');
      }
      session.setLocalUser(this.getAndStoreUserIdentity({ ...input, userName }));

      input.onProgress?.({ phase: 'creating', message: 'Loading project' });
      const allWorkspaces = await this.dependencies.workspaceOps.getAllWorkspaces();
      const existing = allWorkspaces.find((workspace) => {
        const metadata = joinedCollaborationMetadata(workspace);
        return metadata?.protocolVersion === COLLABORATION_PROTOCOL_VERSION
          && metadata.reviewSchemaVersion === COLLABORATION_REVIEW_SCHEMA_VERSION
          && metadata.roomId === descriptor.roomId;
      });
      const reused = Boolean(existing);
      const wsName = existing?.name ?? await this.createJoinedWorkspace(session, descriptor.roomId);
      if (!existing) createdWorkspace = wsName;

      await this.activate(wsName, false, descriptor, buildCollaborationJoinUrl(descriptor), session, {
        materialize: true,
        onProgress: input.onProgress,
      });
      activated = true;
      this.storeReconnectDescriptor(wsName, {
        ...descriptor,
        protocolVersion: COLLABORATION_PROTOCOL_VERSION,
        reviewSchemaVersion: COLLABORATION_REVIEW_SCHEMA_VERSION,
        role: 'guest',
      });
      await this.dependencies.workspaceOps.updateWorkspaceMetadata(wsName, (metadata) => ({
        ...(metadata ?? {}),
        [JOINED_COLLABORATION_METADATA_KEY]: {
          protocolVersion: COLLABORATION_PROTOCOL_VERSION,
          reviewSchemaVersion: COLLABORATION_REVIEW_SCHEMA_VERSION,
          roomId: descriptor.roomId,
          source: 'joined',
        } satisfies JoinedCollaborationMetadata,
      }));
      input.onProgress?.({ phase: 'finalizing', message: `Opening ${session.getRootFile() ?? 'project'}`, completedFiles: session.listFiles().length, totalFiles: session.listFiles().length });
      return { wsName, rootFile: session.getRootFile(), reused };
    } catch (error) {
      if (activated || this.active?.session === session) await this.leave().catch(() => undefined);
      else await session.destroy().catch(() => undefined);
      if (createdWorkspace) await this.removeJoinedWorkspace(createdWorkspace);
      throw error;
    }
  }

  async reconnectJoinedProject(
    wsName: string,
    input: { userName?: string; userId?: string; username?: string } = {},
  ): Promise<boolean> {
    if (this.isActive(wsName)) return true;
    const descriptor = this.readReconnectDescriptor(wsName);
    if (!descriptor) return false;
    const isHost = descriptor.role === 'host';
    let hostGrant: string | undefined;
    try { hostGrant = await requestCollaborationGrant(descriptor.roomId, descriptor.secret, descriptor.projectId, isHost ? 'host' : 'join'); }
    catch { return false; }
    const session = await BrowserProjectCollaborationSession.create({
      roomId: descriptor.roomId,
      secret: descriptor.secret,
      user: this.getAndStoreUserIdentity(input),
      persistenceKey: isHost ? `${wsName}:${descriptor.roomId}` : `joined:${descriptor.roomId}`,
      isHost,
      ...(isHost && descriptor.hostSessionId ? { hostSessionId: descriptor.hostSessionId } : {}),
      ...(hostGrant ? { hostGrant } : {}),
    });
    try {
      if (!(await session.waitForSync()) || session.isSessionStopped()) {
        await session.destroy();
        return false;
      }
      if (!isHost && !(await session.waitForHost())) {
        await session.destroy();
        return false;
      }
      await this.activate(
        wsName,
        isHost,
        { roomId: descriptor.roomId, secret: descriptor.secret },
        buildCollaborationJoinUrl(descriptor),
        session,
        { materialize: !isHost },
      );
      if (isHost) session.setProjectReady(true);
      return true;
    } catch {
      await session.destroy().catch(() => undefined);
      return false;
    }
  }

  async stop(): Promise<void> {
    const active = this.active;
    if (!active) return;
    if (active.isHost) {
      active.session.stopSession();
      await active.session.persistNow();
      await active.session.waitForOutboundDrain();
    }
    await this.leave({ forget: true, clearPersistence: true });
  }

  async leave(options: { forget?: boolean; clearPersistence?: boolean } = {}): Promise<void> {
    const active = this.active;
    if (!active) return;
    this.active = undefined;
    for (const cleanup of active.cleanups.splice(0)) cleanup();
    for (const cleanup of active.fileCleanups.values()) cleanup();
    active.fileCleanups.clear();
    if (active.semanticRefreshTimer) clearTimeout(active.semanticRefreshTimer);
    try {
      await this.flushSessionToStorage(active);
      if (options.forget) localStorage.removeItem(`${RECONNECT_DESCRIPTOR_PREFIX}${active.wsName}`);
    } finally {
      active.payloadChannel.destroy();
      await active.session.destroy({ clearPersistence: options.clearPersistence }).catch(() => undefined);
      this.store.set(this.$state, EMPTY_STATE);
    }
  }

  async copySafeDiagnostics(wsName: string): Promise<string> {
    const active = this.active;
    if (!active || active.wsName !== wsName) throw new Error('No active collaboration session for this workspace');
    return JSON.stringify({ release: 'R14.28.20 Build Fix 51 / TX7.2.3', role: active.isHost ? 'host' : 'guest', ...(await active.session.getDiagnostics()) }, null, 2);
  }

  async flushToStorage(wsName: string): Promise<void> {
    const active = this.active;
    if (!active || active.wsName !== wsName) { await this.dependencies.latexSource.flushWorkspace(wsName); return; }
    await this.flushSessionToStorage(active);
    await active.session.persistNow();
    await this.dependencies.latexSource.flushWorkspace(wsName);
  }

  assertProjectStructureMutable(wsName: string, operation = 'change the project structure'): void {
    if (this.isActive(wsName)) throw new Error(`Project structure is locked while live collaboration is active. Leave collaboration before you ${operation}.`);
  }

  assertDisruptiveGitAllowed(wsName: string, operation: string): void {
    if (this.isActive(wsName)) throw new Error(`Leave the collaboration session before you ${operation}`);
  }

  private async activate(
    wsName: string,
    isHost: boolean,
    descriptor: CollaborationRoomDescriptor,
    shareUrl: string,
    session: BrowserProjectCollaborationSession,
    options: { materialize?: boolean; onProgress?: (progress: CollaborationJoinProgress) => void } = {},
  ): Promise<void> {
    const payloadChannel = session.createPayloadChannel();
    if (!(await payloadChannel.connect())) {
      payloadChannel.destroy();
      throw new Error('Project transfer channel did not connect. Check /collab-live and try again.');
    }
    const active: ActiveSession = {
      wsName, isHost, descriptor, shareUrl, session, payloadChannel,
      cleanups: [], fileCleanups: new Map(), storageWrites: new Map(),
    };
    this.active = active;
    active.cleanups.push(
      session.subscribeConnection(() => this.publishState()),
      session.subscribeAwareness(() => this.publishState()),
      session.subscribeMeta(() => {
        if (!isHost && session.isSessionStopped()) { void this.leave({ forget: true, clearPersistence: true }); return; }
        void this.syncSessionMetadata(active);
        this.publishState();
      }),
      payloadChannel.subscribeRequests((request) => { if (isHost) void this.servePayloadRequest(active, request); }),
    );
    for (const path of session.listTextFiles()) this.attachFile(active, path);
    if (options.materialize) await this.materializeFullProject(active, options.onProgress);
    else await this.flushSessionToStorage(active);
    await this.syncSessionMetadata(active);
    this.dependencies.fileSystem.refreshFileTree();
    this.publishState();
  }

  private attachFile(active: ActiveSession, path: string): void {
    if (active.fileCleanups.has(path) || !isTx7SourcePath(path) || !active.session.isTextFile(path)) return;
    const ytext = active.session.getText(path);
    const wsPath = wsPathFor(active.wsName, path);
    let raf: number | undefined;
    let pending = false;
    const flushProjection = () => {
      raf = undefined;
      if (!pending || this.active !== active) return;
      pending = false;
      const text = ytext.toString();
      const snapshot = this.dependencies.latexSource.getSnapshot(wsPath);
      if (snapshot && snapshot.text !== text) {
        this.dependencies.latexSource.update(wsPath, text, 'collaboration', { history: 'ignore' });
      } else if (!snapshot) {
        this.queueStorageWrite(active, path, text);
      }
      this.scheduleSemanticRefresh(active);
      this.publishState(true);
    };
    const onYText = () => {
      pending = true;
      if (typeof requestAnimationFrame === 'function') {
        if (raf === undefined) raf = requestAnimationFrame(flushProjection);
      } else queueMicrotask(flushProjection);
    };
    ytext.observe(onYText);
    active.fileCleanups.set(path, () => {
      ytext.unobserve(onYText);
      if (raf !== undefined && typeof cancelAnimationFrame === 'function') cancelAnimationFrame(raf);
    });
    pending = true;
    flushProjection();
  }

  private scheduleSemanticRefresh(active: ActiveSession): void {
    if (active.semanticRefreshTimer) clearTimeout(active.semanticRefreshTimer);
    active.semanticRefreshTimer = setTimeout(() => {
      active.semanticRefreshTimer = undefined;
      if (this.active === active) void this.dependencies.projectSemantic.refresh(active.wsName);
    }, 350);
  }

  private queueStorageWrite(active: ActiveSession, path: string, text: string): void {
    const previous = active.storageWrites.get(path) ?? Promise.resolve();
    const next = previous.catch(() => undefined).then(async () => {
      if (this.active !== active) return;
      const provider = await this.dependencies.projectStorage.getProvider(active.wsName);
      await provider.writeFile(projectFilePath(path), TEXT_ENCODER.encode(text));
    }).catch((error) => this.logger.error(`Unable to persist collaborative file ${path}`, error));
    active.storageWrites.set(path, next);
  }

  private async flushSessionToStorage(active: ActiveSession): Promise<void> {
    const provider = await this.dependencies.projectStorage.getProvider(active.wsName);
    for (const directory of active.session.listDirectories()) await provider.createDirectory(projectDirectoryPath(directory)).catch(() => undefined);
    for (const path of active.session.listTextFiles()) {
      const text = active.session.getText(path).toString();
      const sourcePath = wsPathFor(active.wsName, path);
      const snapshot = this.dependencies.latexSource.getSnapshot(sourcePath);
      if (!snapshot || snapshot.text !== text) this.dependencies.latexSource.update(sourcePath, text, 'collaboration', { history: 'ignore' });
      await provider.writeFile(projectFilePath(path), TEXT_ENCODER.encode(text));
    }
    await Promise.all(active.storageWrites.values());
    this.dependencies.fileSystem.refreshFileTree();
  }

  private async servePayloadRequest(active: ActiveSession, request: CollaborationPayloadRequest): Promise<void> {
    if (this.active !== active) return;
    const parsed = safeProjectFilePath(request.path);
    if (!parsed.ok) return;
    const provider = await this.dependencies.projectStorage.getProvider(active.wsName);
    const bytes = await provider.readFile(parsed.path).catch(() => undefined);
    if (!bytes || bytes.byteLength !== request.size || await sha256Hex(bytes) !== request.sha256) return;
    await active.payloadChannel.sendFile(request, bytes);
  }

  private async fetchBinaryFile(active: ActiveSession, path: string, record: CollaborationBinaryFileRecord): Promise<void> {
    const parsed = safeProjectFilePath(path);
    if (!parsed.ok) return;
    const provider = await this.dependencies.projectStorage.getProvider(active.wsName);
    const local = await provider.readFile(parsed.path).catch(() => undefined);
    if (local?.byteLength === record.size && await sha256Hex(local) === record.sha256) return;
    const preferredIsPresent = active.session.awareness.getStates().has(record.sourceClientId);
    let bytes: Uint8Array;
    try {
      bytes = await active.payloadChannel.requestFile({ path: parsed.path, sha256: record.sha256, size: record.size, ...(preferredIsPresent ? { preferredClientId: record.sourceClientId } : {}) });
    } catch {
      bytes = await active.payloadChannel.requestFile({ path: parsed.path, sha256: record.sha256, size: record.size });
    }
    if (bytes.byteLength !== record.size || await sha256Hex(bytes) !== record.sha256) throw new Error(`Transferred project file failed integrity verification: ${parsed.path}`);
    await provider.writeFile(parsed.path, bytes);
  }

  private async materializeFullProject(active: ActiveSession, onProgress?: (progress: CollaborationJoinProgress) => void): Promise<void> {
    const provider = await this.dependencies.projectStorage.getProvider(active.wsName);
    const textFiles = active.session.listTextFiles();
    const binaryFiles = active.session.listBinaryFiles();
    const allFiles = [...textFiles, ...binaryFiles].sort();
    const incomingHashes: Record<string, string> = {};
    for (const path of textFiles) incomingHashes[path] = await sha256Hex(TEXT_ENCODER.encode(active.session.getText(path).toString()));
    for (const path of binaryFiles) {
      const record = active.session.getBinaryFile(path);
      if (record) incomingHashes[path] = record.sha256;
    }
    const baseline = this.readSnapshotBaseline(active.session.roomId);
    const existingEntries = await provider.listEntries();
    const targetFiles = new Set(allFiles);
    const recoveryRoot = `${RECOVERY_DIRECTORY}/${new Date().toISOString().replace(/[:.]/gu, '-')}`;
    let recoveryCreated = false;
    const ensureRecoveryDirectory = async (path: string) => {
      const parts = path.split('/').filter(Boolean);
      let current = '';
      for (const part of parts) {
        current = current ? `${current}/${part}` : part;
        await provider.createDirectory(projectDirectoryPath(current)).catch(() => undefined);
      }
    };
    const backupFile = async (path: string, bytes: Uint8Array) => {
      const parent = path.includes('/') ? path.slice(0, path.lastIndexOf('/')) : '';
      await ensureRecoveryDirectory(parent ? `${recoveryRoot}/${parent}` : recoveryRoot);
      await provider.writeFile(projectFilePath(`${recoveryRoot}/${path}`), bytes);
      recoveryCreated = true;
    };

    for (const entry of existingEntries) {
      if (entry.path === RECOVERY_DIRECTORY || entry.path.startsWith(`${RECOVERY_DIRECTORY}/`)) continue;
      if (entry.kind !== 'file') continue;
      const bytes = await provider.readFile(entry.path).catch(() => undefined);
      if (!bytes) continue;
      const localHash = await sha256Hex(bytes);
      const incomingHash = incomingHashes[entry.path];
      const previousHash = baseline?.files?.[entry.path];
      const wouldDelete = !targetFiles.has(entry.path);
      const locallyDiverged = previousHash ? localHash !== previousHash : Boolean(incomingHash && localHash !== incomingHash);
      if (wouldDelete || (locallyDiverged && incomingHash && localHash !== incomingHash)) await backupFile(entry.path, bytes);
    }

    let completedFiles = 0;
    const report = (message: string) => onProgress?.({ phase: 'transferring', message, completedFiles, totalFiles: allFiles.length });
    report(recoveryCreated ? 'Preserving local changes' : 'Loading project');
    for (const directory of active.session.listDirectories().sort((a, b) => a.length - b.length)) await provider.createDirectory(projectDirectoryPath(directory)).catch(() => undefined);
    for (const entry of existingEntries) {
      if (entry.kind === 'file' && !targetFiles.has(entry.path) && entry.path !== RECOVERY_DIRECTORY && !entry.path.startsWith(`${RECOVERY_DIRECTORY}/`)) {
        await provider.deleteFile(entry.path).catch(() => undefined);
      }
    }
    for (const path of textFiles) {
      await provider.writeFile(projectFilePath(path), TEXT_ENCODER.encode(active.session.getText(path).toString()));
      completedFiles += 1; report(`Loading ${path}`);
    }
    for (const path of binaryFiles) {
      const record = active.session.getBinaryFile(path);
      if (record) await this.fetchBinaryFile(active, path, record);
      completedFiles += 1; report(`Loading ${path}`);
    }
    const targetDirectories = new Set(active.session.listDirectories());
    const afterEntries = await provider.listEntries();
    const extraDirectories = afterEntries.filter((entry) => entry.kind === 'directory' && !targetDirectories.has(entry.path) && entry.path !== RECOVERY_DIRECTORY && !entry.path.startsWith(`${RECOVERY_DIRECTORY}/`)).sort((a, b) => b.path.length - a.path.length);
    for (const directory of extraDirectories) {
      const hadChild = existingEntries.some((entry) => entry.path.startsWith(`${directory.path}/`));
      if (!hadChild) {
        await ensureRecoveryDirectory(`${recoveryRoot}/${directory.path}`);
        recoveryCreated = true;
      }
      await provider.deleteDirectory(projectDirectoryPath(directory.path)).catch(() => undefined);
    }
    this.storeSnapshotBaseline(active.session.roomId, { roomId: active.session.roomId, capturedAt: Date.now(), files: incomingHashes, directories: active.session.listDirectories() });
    this.dependencies.fileSystem.refreshFileTree();
  }

  private async syncSessionMetadata(active: ActiveSession): Promise<void> {
    const rootFile = active.session.getRootFile();
    let profile = await this.dependencies.projectStorage.getProjectProfile(active.wsName);
    if (!profile) {
      const detected = await this.dependencies.projectStorage.detectProjectTargets(active.wsName).catch(() => []);
      if (detected[0]?.profile) profile = await this.dependencies.projectStorage.updateProjectProfile(active.wsName, detected[0].profile);
    }
    const mode = projectModeForProfile(profile);
    if (mode?.id === 'latex') {
      if (!rootFile) return;
      await this.dependencies.projectStorage.updateProjectConfig(active.wsName, (current) => ({ ...current, rootFile })).catch((error) => this.logger.warn(`Unable to apply collaborative root file ${rootFile}`, error));
      return;
    }
    if (profile && rootFile) {
      await this.dependencies.projectStorage.updateProjectProfile(active.wsName, { ...profile, entry: rootFile }).catch((error) => this.logger.warn(`Unable to apply collaborative project entry ${rootFile}`, error));
    }
  }

  private async createJoinedWorkspace(session: BrowserProjectCollaborationSession, roomId: string): Promise<string> {
    const advertisedName = session.getProjectName();
    const preferred = advertisedName && WsPath.safeFromParts(advertisedName, '').ok ? advertisedName : `Shared Project ${session.roomFingerprint}`;
    const wsName = await this.uniqueWorkspaceName(preferred);
    const type = supportsOpfsProjectStorage() ? WORKSPACE_STORAGE_TYPE.BrowserOPFS : WORKSPACE_STORAGE_TYPE.Browser;
    const created = await this.dependencies.workspaceOps.createWorkspaceInfo({
      name: wsName,
      type,
      metadata: {
        product: 'learnitive',
        [JOINED_COLLABORATION_METADATA_KEY]: {
          protocolVersion: COLLABORATION_PROTOCOL_VERSION,
          reviewSchemaVersion: COLLABORATION_REVIEW_SCHEMA_VERSION,
          roomId,
          source: 'joined',
        } satisfies JoinedCollaborationMetadata,
      },
    }, { bypassProjectLimit: true });
    if (!created) throw new Error('Unable to create a local project for the collaboration session');
    await requestPersistentBrowserStorage().catch(() => false);
    return wsName;
  }

  private async uniqueWorkspaceName(base: string): Promise<string> {
    const normalized = base.trim().slice(0, 120) || 'Shared Project';
    for (let suffix = 1; suffix < 10_000; suffix += 1) {
      const candidate = suffix === 1 ? normalized : `${normalized} ${suffix}`;
      if (!WsPath.safeFromParts(candidate, '').ok) continue;
      if (!(await this.dependencies.workspaceOps.getWorkspaceInfo(candidate))) return candidate;
    }
    throw new Error('Unable to allocate a local project name for the collaboration session');
  }

  private async removeJoinedWorkspace(wsName: string): Promise<void> {
    try {
      const provider = await this.dependencies.projectStorage.getProvider(wsName);
      const entries = await provider.listEntries();
      for (const entry of entries) if (entry.kind === 'file') await provider.deleteFile(entry.path).catch(() => undefined);
      for (const entry of entries.filter((entry) => entry.kind === 'directory').sort((a, b) => b.path.length - a.path.length)) await provider.deleteDirectory(projectDirectoryPath(entry.path)).catch(() => undefined);
    } catch { /* metadata cleanup remains authoritative */ }
    await this.dependencies.workspaceOps.deleteWorkspaceInfo(wsName).catch(() => undefined);
  }

  private publishState(documentChanged = false): void {
    const active = this.active;
    if (!active) { this.store.set(this.$state, EMPTY_STATE); return; }
    const peers = active.session.getPeers();
    const localUser = active.session.awareness.getLocalState()?.user as CollaborationUser | undefined;
    if (localUser) this.resolveColorCollision(active, localUser, peers);
    const previous = this.store.get(this.$state);
    const connectionPhase = active.session.synced && active.session.connected
      ? 'live'
      : active.session.connectionStatus === 'connected'
        ? 'synchronizing'
        : previous.active ? 'reconnecting' : 'connecting';
    this.store.set(this.$state, {
      wsName: active.wsName,
      active: true,
      connected: connectionPhase === 'live',
      connectionPhase,
      isHost: active.isHost,
      roomId: active.session.roomId,
      shareUrl: active.shareUrl,
      userName: localUser?.name,
      userColor: localUser?.color,
      userColorLight: localUser?.colorLight,
      peers,
      statusMessage: connectionPhase === 'live'
        ? (active.isHost && peers.length === 0 ? 'Session ready — share the project link' : undefined)
        : connectionPhase === 'reconnecting' ? 'Reconnecting to collaboration relay' : 'Synchronizing collaboration',
      documentRevision: previous.documentRevision + (documentChanged ? 1 : 0),
    });
  }

  private resolveColorCollision(active: ActiveSession, localUser: CollaborationUser, peers: readonly CollaborationPeer[]): void {
    const conflicts = peers.filter((peer) => peer.color === localUser.color);
    if (!conflicts.length) return;
    const winningId = Math.min(active.session.doc.clientID, ...conflicts.map((peer) => peer.clientId));
    if (active.session.doc.clientID === winningId) return;
    const used = new Set(peers.map((peer) => peer.color));
    const replacement = USER_COLORS.find(([color]) => !used.has(color));
    if (replacement && replacement[0] !== localUser.color) active.session.setLocalUser({ ...localUser, color: replacement[0], colorLight: replacement[1] });
  }

  private getAndStoreUserIdentity(input: { userName?: string; userId?: string; username?: string }): CollaborationUser {
    const suppliedName = input.userName?.trim().slice(0, 40);
    const storedName = localStorage.getItem(COLLAB_USER_NAME_KEY)?.trim().slice(0, 40);
    const name = suppliedName || storedName || `User-${Math.random().toString(36).slice(2, 6)}`;
    const storedId = localStorage.getItem(COLLAB_USER_ID_KEY);
    const id = input.userId ? `account:${input.userId}` : storedId || `anon:${crypto.randomUUID()}`;
    const username = (input.username?.trim() || name).slice(0, 80);
    let pair = USER_COLORS.find(([color]) => color === localStorage.getItem(COLLAB_USER_COLOR_KEY));
    pair ??= USER_COLORS[Math.floor(Math.random() * USER_COLORS.length)] ?? USER_COLORS[0];
    if (!input.userId) localStorage.setItem(COLLAB_USER_ID_KEY, id);
    localStorage.setItem(COLLAB_USER_NAME_KEY, name);
    localStorage.setItem(COLLAB_USER_COLOR_KEY, pair[0]);
    return { id, username, name, color: pair[0], colorLight: pair[1] };
  }

  private storeReconnectDescriptor(wsName: string, descriptor: ReconnectDescriptor): void {
    localStorage.setItem(`${RECONNECT_DESCRIPTOR_PREFIX}${wsName}`, JSON.stringify(descriptor));
  }

  private readReconnectDescriptor(wsName: string): ReconnectDescriptor | undefined {
    try {
      const value = JSON.parse(localStorage.getItem(`${RECONNECT_DESCRIPTOR_PREFIX}${wsName}`) ?? 'null') as Partial<ReconnectDescriptor> | null;
      if (!value
        || value.protocolVersion !== COLLABORATION_PROTOCOL_VERSION
        || value.reviewSchemaVersion !== COLLABORATION_REVIEW_SCHEMA_VERSION
        || !value.roomId
        || !value.secret
        || (value.role !== 'host' && value.role !== 'guest')) return undefined;
      return value as ReconnectDescriptor;
    } catch { return undefined; }
  }

  private readSnapshotBaseline(roomId: string): SnapshotBaseline | undefined {
    try {
      const value = JSON.parse(localStorage.getItem(`${SNAPSHOT_BASELINE_PREFIX}${roomId}`) ?? 'null') as SnapshotBaseline | null;
      if (!value || value.roomId !== roomId || !value.files || Date.now() - Number(value.capturedAt || 0) > SNAPSHOT_BASELINE_RETENTION_MS) return undefined;
      return value;
    } catch { return undefined; }
  }

  private storeSnapshotBaseline(roomId: string, baseline: SnapshotBaseline): void {
    localStorage.setItem(`${SNAPSHOT_BASELINE_PREFIX}${roomId}`, JSON.stringify(baseline));
    const now = Date.now();
    for (let index = localStorage.length - 1; index >= 0; index -= 1) {
      const key = localStorage.key(index);
      if (!key?.startsWith(SNAPSHOT_BASELINE_PREFIX) || key === `${SNAPSHOT_BASELINE_PREFIX}${roomId}`) continue;
      try {
        const candidate = JSON.parse(localStorage.getItem(key) ?? 'null') as SnapshotBaseline | null;
        if (!candidate || now - Number(candidate.capturedAt || 0) > SNAPSHOT_BASELINE_RETENTION_MS) localStorage.removeItem(key);
      } catch { localStorage.removeItem(key); }
    }
  }

}
