/**
 * Service for managing folder-file associations in IndexedDB.
 * File blobs are stored in the main stirling-pdf-files database (fileStorage).
 * This service only maintains folder record metadata: which file IDs belong to
 * which folders and their processing status.
 */

import { FolderFileMetadata, FolderRecord } from "@app/types/watchedFolders";

const FOLDER_CHANGE_EVENT = "folder-storage-changed";

class WatchedFolderFileStorage {
  private dbName = "stirling-pdf-folder-files";
  private dbVersion = 3;
  private recordsStore = "folderRecords";
  private db: IDBDatabase | null = null;

  async init(): Promise<void> {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.dbName, this.dbVersion);

      request.onerror = () => {
        reject(new Error("Failed to open folder files database"));
      };

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

      request.onupgradeneeded = (event) => {
        const db = (event.target as IDBOpenDBRequest).result;
        if (!db.objectStoreNames.contains(this.recordsStore)) {
          db.createObjectStore(this.recordsStore, { keyPath: "folderId" });
        }
        if (db.objectStoreNames.contains("folderOutputFiles")) {
          db.deleteObjectStore("folderOutputFiles");
        }
        if (db.objectStoreNames.contains("folderInputFiles")) {
          db.deleteObjectStore("folderInputFiles");
        }
      };
    });
  }

  private async ensureDB(): Promise<IDBDatabase> {
    if (!this.db) {
      await this.init();
    }
    if (!this.db) {
      throw new Error("Folder files database not initialized");
    }
    return this.db;
  }

  private dispatchChange(folderId: string): void {
    window.dispatchEvent(
      new CustomEvent(FOLDER_CHANGE_EVENT, { detail: { folderId } }),
    );
  }

  onFolderChange(listener: (folderId: string) => void): () => void {
    const handler = (e: Event) => {
      listener((e as CustomEvent).detail.folderId);
    };
    window.addEventListener(FOLDER_CHANGE_EVENT, handler);
    return () => window.removeEventListener(FOLDER_CHANGE_EVENT, handler);
  }

  async getFolderData(folderId: string): Promise<FolderRecord | null> {
    const db = await this.ensureDB();
    return new Promise((resolve, reject) => {
      const transaction = db.transaction([this.recordsStore], "readonly");
      const store = transaction.objectStore(this.recordsStore);
      const request = store.get(folderId);
      request.onsuccess = () => resolve(request.result || null);
      request.onerror = () => reject(new Error("Failed to get folder data"));
    });
  }

  async addFileToFolder(
    folderId: string,
    fileId: string,
    metadata?: Partial<FolderFileMetadata>,
  ): Promise<void> {
    const db = await this.ensureDB();
    const now = new Date();
    return new Promise((resolve, reject) => {
      // Single readwrite transaction for both read and write — prevents lost-update
      // races when multiple files are added to the same folder concurrently.
      const transaction = db.transaction([this.recordsStore], "readwrite");
      const store = transaction.objectStore(this.recordsStore);
      const getRequest = store.get(folderId);
      getRequest.onsuccess = () => {
        const record: FolderRecord = getRequest.result || {
          folderId,
          files: {},
          lastUpdated: Date.now(),
        };
        record.files[fileId] = { addedAt: now, status: "pending", ...metadata };
        record.lastUpdated = Date.now();
        const putRequest = store.put(record);
        putRequest.onsuccess = () => {
          this.dispatchChange(folderId);
          resolve();
        };
        putRequest.onerror = () =>
          reject(new Error("Failed to add file to folder"));
      };
      getRequest.onerror = () =>
        reject(new Error("Failed to read folder for add"));
    });
  }

  async updateFileMetadata(
    folderId: string,
    fileId: string,
    updates: Partial<FolderFileMetadata>,
  ): Promise<void> {
    const db = await this.ensureDB();
    return new Promise((resolve, reject) => {
      // Single readwrite transaction — prevents lost-update races during concurrent
      // pipeline runs where multiple files update their status simultaneously.
      const transaction = db.transaction([this.recordsStore], "readwrite");
      const store = transaction.objectStore(this.recordsStore);
      const getRequest = store.get(folderId);
      getRequest.onsuccess = () => {
        const existing: FolderRecord | undefined = getRequest.result;
        if (!existing) {
          resolve();
          return;
        }
        existing.files[fileId] = { ...existing.files[fileId], ...updates };
        existing.lastUpdated = Date.now();
        const putRequest = store.put(existing);
        putRequest.onsuccess = () => {
          this.dispatchChange(folderId);
          resolve();
        };
        putRequest.onerror = () =>
          reject(new Error("Failed to update file metadata"));
      };
      getRequest.onerror = () =>
        reject(new Error("Failed to read folder for update"));
    });
  }

  async removeFileFromFolder(folderId: string, fileId: string): Promise<void> {
    const db = await this.ensureDB();
    return new Promise((resolve, reject) => {
      const transaction = db.transaction([this.recordsStore], "readwrite");
      const store = transaction.objectStore(this.recordsStore);
      const getRequest = store.get(folderId);
      getRequest.onsuccess = () => {
        const existing: FolderRecord | undefined = getRequest.result;
        if (!existing) {
          resolve();
          return;
        }
        delete existing.files[fileId];
        existing.lastUpdated = Date.now();
        const putRequest = store.put(existing);
        putRequest.onsuccess = () => {
          this.dispatchChange(folderId);
          resolve();
        };
        putRequest.onerror = () =>
          reject(new Error("Failed to remove file from folder"));
      };
      getRequest.onerror = () =>
        reject(new Error("Failed to read folder for remove"));
    });
  }

  /**
   * Overwrite the entire folder record.
   * Pass `{ silent: true }` for mirror writes — these reflect a read,
   * not a user action, so dispatching change events would cause subscribers
   * (which themselves call getFolderData) to re-fetch in an infinite loop.
   */
  async setFolderData(
    folderId: string,
    record: FolderRecord,
    opts?: { silent?: boolean },
  ): Promise<void> {
    const db = await this.ensureDB();
    return new Promise((resolve, reject) => {
      const transaction = db.transaction([this.recordsStore], "readwrite");
      const store = transaction.objectStore(this.recordsStore);
      const request = store.put(record);
      request.onsuccess = () => {
        if (!opts?.silent) this.dispatchChange(folderId);
        resolve();
      };
      request.onerror = () => reject(new Error("Failed to set folder data"));
    });
  }

  async clearFolder(folderId: string): Promise<void> {
    const db = await this.ensureDB();
    return new Promise((resolve, reject) => {
      const transaction = db.transaction([this.recordsStore], "readwrite");
      const store = transaction.objectStore(this.recordsStore);
      const request = store.delete(folderId);
      request.onsuccess = () => {
        this.dispatchChange(folderId);
        resolve();
      };
      request.onerror = () => reject(new Error("Failed to clear folder"));
    });
  }
}

export const watchedFolderFileStorage = new WatchedFolderFileStorage();
