/**
 * IndexedDB Persistence for Yjs
 * Provides offline-first storage with automatic sync
 */

import * as Y from 'yjs';

export interface IDBPOptions {
  dbName?: string;
  storeName?: string;
  autoSave?: boolean;
  saveIntervalMs?: number;
}

/**
 * IndexedDB persistence provider for Yjs documents
 * Enables offline editing with automatic sync when online
 */
export class IDBPersistence {
  private db: IDBDatabase | null = null;
  private doc: Y.Doc;
  private options: Required<IDBPOptions>;
  private saveTimer: ReturnType<typeof setTimeout> | null = null;
  private isOnline: boolean = true;
  private pendingUpdates: Uint8Array[] = [];

  constructor(doc: Y.Doc, options: IDBPOptions = {}) {
    this.doc = doc;
    this.options = {
      dbName: options.dbName || 'frenocorp-yjs',
      storeName: options.storeName || 'documents',
      autoSave: options.autoSave ?? true,
      saveIntervalMs: options.saveIntervalMs || 5000,
    };

    this.init();
  }

  /**
   * Initialize IndexedDB connection
   */
  private async init(): Promise<void> {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.options.dbName, 1);

      request.onerror = () => reject(request.error);
      request.onsuccess = () => {
        this.db = request.result;
        resolve();
      };

      request.onupgradeneeded = (event) => {
        const db = (event.target as IDBOpenDBRequest).result;
        
        // Create object store for documents
        if (!db.objectStoreNames.contains(this.options.storeName)) {
          const store = db.createObjectStore(this.options.storeName, {
            keyPath: 'docName',
          });
          store.createIndex('updatedAt', 'updatedAt', { unique: false });
        }
      };
    });
  }

  /**
   * Save document state to IndexedDB
   */
  async save(docName: string): Promise<void> {
    if (!this.db) {
      throw new Error('IndexedDB not initialized');
    }

    const state = Y.encodeStateAsUpdate(this.doc);
    const timestamp = Date.now();

    return new Promise((resolve, reject) => {
      const transaction = this.db!.transaction([this.options.storeName], 'readwrite');
      const store = transaction.objectStore(this.options.storeName);

      const request = store.put({
        docName,
        state: Array.from(state),
        updatedAt: timestamp,
      });

      request.onsuccess = () => resolve();
      request.onerror = () => reject(request.error);
    });
  }

  /**
   * Load document state from IndexedDB
   */
  async load(docName: string): Promise<Uint8Array | null> {
    if (!this.db) {
      throw new Error('IndexedDB not initialized');
    }

    return new Promise((resolve, reject) => {
      const transaction = this.db!.transaction([this.options.storeName], 'readonly');
      const store = transaction.objectStore(this.options.storeName);

      const request = store.get(docName);

      request.onsuccess = () => {
        const result = request.result;
        if (result && result.state) {
          resolve(new Uint8Array(result.state));
        } else {
          resolve(null);
        }
      };
      request.onerror = () => reject(request.error);
    });
  }

  /**
   * Apply saved state to document
   */
  async loadAndApply(docName: string): Promise<boolean> {
    const state = await this.load(docName);
    if (state) {
      Y.applyUpdate(this.doc, state);
      return true;
    }
    return false;
  }

  /**
   * Start auto-save timer
   */
  startAutoSave(docName: string): void {
    if (!this.options.autoSave) {
      return;
    }

    this.stopAutoSave();
    
    this.saveTimer = setInterval(() => {
      this.save(docName).catch((err) => {
        console.error('[IDBPersistence] Auto-save failed:', err);
      });
    }, this.options.saveIntervalMs);
  }

  /**
   * Stop auto-save timer
   */
  stopAutoSave(): void {
    if (this.saveTimer) {
      clearInterval(this.saveTimer);
      this.saveTimer = null;
    }
  }

  /**
   * Queue update for later sync (offline mode)
   */
  queueUpdate(update: Uint8Array): void {
    this.pendingUpdates.push(update);
    console.log(`[IDBPersistence] Update queued for sync. Queue size: ${this.pendingUpdates.length}`);
  }

  /**
   * Flush queued updates
   */
  async flushUpdates(docName: string): Promise<void> {
    if (this.pendingUpdates.length === 0) {
      return;
    }

    console.log(`[IDBPersistence] Flushing ${this.pendingUpdates.length} queued updates`);

    // Apply all pending updates to document
    this.pendingUpdates.forEach((update) => {
      Y.applyUpdate(this.doc, update);
    });

    // Save merged state
    await this.save(docName);
    this.pendingUpdates = [];
  }

  /**
   * Set online/offline status
   */
  setOnline(online: boolean): void {
    const wasOffline = !this.isOnline;
    this.isOnline = online;

    if (online && wasOffline) {
      console.log('[IDBPersistence] Back online, flushing queued updates');
      // Will flush updates when docName is provided
    } else if (!online) {
      console.log('[IDBPersistence] Went offline, queuing updates');
    }
  }

  /**
   * Check if currently online
   */
  isOnlineStatus(): boolean {
    return this.isOnline;
  }

  /**
   * Get pending update count
   */
  getPendingUpdateCount(): number {
    return this.pendingUpdates.length;
  }

  /**
   * Clear all stored data
   */
  async clear(docName: string): Promise<void> {
    if (!this.db) {
      throw new Error('IndexedDB not initialized');
    }

    return new Promise((resolve, reject) => {
      const transaction = this.db!.transaction([this.options.storeName], 'readwrite');
      const store = transaction.objectStore(this.options.storeName);

      const request = store.delete(docName);

      request.onsuccess = () => resolve();
      request.onerror = () => reject(request.error);
    });
  }

  /**
   * Get last saved timestamp
   */
  async getLastSavedTime(docName: string): Promise<number | null> {
    if (!this.db) {
      throw new Error('IndexedDB not initialized');
    }

    return new Promise((resolve, reject) => {
      const transaction = this.db!.transaction([this.options.storeName], 'readonly');
      const store = transaction.objectStore(this.options.storeName);

      const request = store.get(docName);

      request.onsuccess = () => {
        const result = request.result;
        resolve(result?.updatedAt || null);
      };
      request.onerror = () => reject(request.error);
    });
  }

  /**
   * Destroy persistence provider
   */
  destroy(): void {
    this.stopAutoSave();
    if (this.db) {
      this.db.close();
      this.db = null;
    }
  }
}

/**
 * Create IndexedDB persistence provider
 */
export function createIDBPersistence(
  doc: Y.Doc,
  options: IDBPOptions = {}
): IDBPersistence {
  return new IDBPersistence(doc, options);
}
