import { WebsocketProvider } from 'y-websocket';
import * as Y from 'yjs';
import { COLLABORATION_PROTOCOL_VERSION, COLLABORATION_REVIEW_CAPABILITY_VERSION } from './collaboration-capabilities';

export { COLLABORATION_PROTOCOL_VERSION } from './collaboration-capabilities';
const MAX_RECONNECT_DELAY_MS = 8_000;

type ProviderStatus = 'connecting' | 'connected' | 'disconnected';
type StatusListener = (status: ProviderStatus, synced: boolean) => void;

function relayBaseUrl(): string {
  if (typeof window === 'undefined') return '';
  const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
  return `${protocol}//${window.location.host}/collab-live`;
}

export async function deriveCollaborationRoomKey(
  roomId: string,
  secret: string,
): Promise<string> {
  const material = new TextEncoder().encode(
    `interactive-collaboration-v${COLLABORATION_PROTOCOL_VERSION}:${roomId}:${secret}`,
  );
  const digest = await crypto.subtle.digest('SHA-256', material);
  return Array.from(new Uint8Array(digest), (value) => value.toString(16).padStart(2, '0')).join('');
}

/**
 * Thin Learnitive lifecycle wrapper around the upstream y-websocket provider.
 *
 * BF17-BF24 implemented the Yjs wire protocol, awareness and reconnect logic
 * in Learnitive itself. BF25 deliberately delegates those responsibilities to the
 * mature Yjs provider architecture. Learnitive retains only room
 * derivation, status projection and lifecycle ownership here.
 */
export class LearnitiveWebSocketProvider {
  readonly awareness: WebsocketProvider['awareness'];
  readonly upstream: WebsocketProvider;
  readonly roomFingerprint: string;
  private destroyed = false;
  private status: ProviderStatus = 'disconnected';
  private synced = false;
  private readonly statusListeners = new Set<StatusListener>();

  private constructor(
    readonly doc: Y.Doc,
    readonly roomKey: string,
    readonly projectId: string,
    hostGrant?: string,
  ) {
    this.roomFingerprint = roomKey.slice(0, 8);
    this.upstream = new WebsocketProvider(relayBaseUrl(), roomKey, doc, {
      connect: false,
      maxBackoffTime: MAX_RECONNECT_DELAY_MS,
      params: {
        client: String(doc.clientID),
        review: String(COLLABORATION_REVIEW_CAPABILITY_VERSION),
        project: projectId,
        ...(hostGrant ? { hostGrant } : {}),
      },
    });
    this.awareness = this.upstream.awareness;
    this.upstream.on('status', this.onStatus);
    this.upstream.on('sync', this.onSync);
    this.setStatus('connecting', false);
    this.upstream.connect();
  }

  static async create(input: {
    roomId: string;
    secret: string;
    doc: Y.Doc;
    roomKey?: string;
    projectId: string;
    hostGrant?: string;
  }): Promise<LearnitiveWebSocketProvider> {
    const key = input.roomKey ?? await deriveCollaborationRoomKey(
      input.roomId,
      input.secret,
    );
    return new LearnitiveWebSocketProvider(input.doc, key, input.projectId, input.hostGrant);
  }

  get connectionStatus(): ProviderStatus {
    return this.status;
  }

  get connected(): boolean {
    return this.upstream.wsconnected;
  }

  get isSynced(): boolean {
    return this.upstream.synced || this.synced;
  }

  subscribeStatus(listener: StatusListener): () => void {
    this.statusListeners.add(listener);
    listener(this.status, this.isSynced);
    return () => this.statusListeners.delete(listener);
  }

  async waitForSync(timeoutMs = 12_000): Promise<boolean> {
    if (this.isSynced) return true;
    return new Promise((resolve) => {
      let settled = false;
      let timer: ReturnType<typeof setTimeout> | undefined;
      let unsubscribe: (() => void) | undefined;
      const finish = (value: boolean) => {
        if (settled) return;
        settled = true;
        if (timer) clearTimeout(timer);
        unsubscribe?.();
        resolve(value);
      };
      unsubscribe = this.subscribeStatus((_status, synced) => {
        if (synced) finish(true);
      });
      timer = setTimeout(() => finish(false), timeoutMs);
    });
  }

  async waitForOutboundDrain(timeoutMs = 3_000): Promise<boolean> {
    const started = Date.now();
    while (Date.now() - started < timeoutMs) {
      const socket = this.upstream.ws;
      if (!socket || socket.readyState !== WebSocket.OPEN) return false;
      if (socket.bufferedAmount === 0) {
        await new Promise((resolve) => setTimeout(resolve, 25));
        if (socket.bufferedAmount === 0) return true;
      }
      await new Promise((resolve) => setTimeout(resolve, 10));
    }
    return false;
  }

  destroy(): void {
    if (this.destroyed) return;
    this.destroyed = true;
    this.upstream.off('status', this.onStatus);
    this.upstream.off('sync', this.onSync);
    this.upstream.disconnect();
    this.upstream.destroy();
    this.setStatus('disconnected', false);
    this.statusListeners.clear();
  }

  private readonly onStatus = (event: { status: ProviderStatus }) => {
    if (this.destroyed) return;
    this.setStatus(event.status, event.status === 'connected' ? this.upstream.synced : false);
  };

  private readonly onSync = (synced: boolean) => {
    if (this.destroyed) return;
    this.setStatus(this.upstream.wsconnected ? 'connected' : this.status, synced);
  };

  private setStatus(status: ProviderStatus, synced: boolean): void {
    const changed = status !== this.status || synced !== this.synced;
    this.status = status;
    this.synced = synced;
    if (!changed) return;
    for (const listener of this.statusListeners) listener(this.status, this.synced);
  }
}
