import type { JobMetadataStore, MediaJob } from './job.types';

export class MemoryJobMetadataStore implements JobMetadataStore {
  readonly #jobs = new Map<string, MediaJob>();

  async list(): Promise<readonly MediaJob[]> {
    return structuredClone(
      [...this.#jobs.values()].sort((left, right) =>
        left.createdAt.localeCompare(right.createdAt)
      )
    );
  }

  async put(job: MediaJob): Promise<void> {
    this.#jobs.set(job.id, structuredClone(job));
  }

  async delete(id: string): Promise<void> {
    this.#jobs.delete(id);
  }

  async clear(): Promise<void> {
    this.#jobs.clear();
  }
}

export interface IndexedDbJobStoreOptions {
  readonly databaseName?: string;
  readonly storeName?: string;
  readonly indexedDB?: IDBFactory;
}

export class IndexedDbJobMetadataStore implements JobMetadataStore {
  readonly #databaseName: string;
  readonly #storeName: string;
  readonly #factory: IDBFactory;
  #database?: Promise<IDBDatabase>;

  constructor(options: IndexedDbJobStoreOptions = {}) {
    const factory = options.indexedDB ?? globalThis.indexedDB;
    if (!factory) {
      throw new Error('IndexedDB is unavailable');
    }
    this.#factory = factory;
    this.#databaseName = options.databaseName ?? 'av-tools';
    this.#storeName = options.storeName ?? 'media-jobs';
  }

  async list(): Promise<readonly MediaJob[]> {
    const request = (await this.#store('readonly')).getAll();
    const result = await requestResult<MediaJob[]>(request);
    return Object.freeze(
      result.sort((left, right) =>
        left.createdAt.localeCompare(right.createdAt)
      )
    );
  }

  async put(job: MediaJob): Promise<void> {
    const store = await this.#store('readwrite');
    const completion = transactionComplete(store.transaction);
    const request = store.put(structuredClone(job));
    await requestResult<IDBValidKey>(request);
    await completion;
  }

  async delete(id: string): Promise<void> {
    const store = await this.#store('readwrite');
    const completion = transactionComplete(store.transaction);
    await requestResult<undefined>(store.delete(id));
    await completion;
  }

  async clear(): Promise<void> {
    const store = await this.#store('readwrite');
    const completion = transactionComplete(store.transaction);
    await requestResult<undefined>(store.clear());
    await completion;
  }

  async #store(mode: IDBTransactionMode): Promise<IDBObjectStore> {
    const database = await (this.#database ??= this.#open());
    return database
      .transaction(this.#storeName, mode)
      .objectStore(this.#storeName);
  }

  #open(): Promise<IDBDatabase> {
    return new Promise((resolve, reject) => {
      const request = this.#factory.open(this.#databaseName, 1);
      request.onupgradeneeded = () => {
        if (!request.result.objectStoreNames.contains(this.#storeName)) {
          request.result.createObjectStore(this.#storeName, { keyPath: 'id' });
        }
      };
      request.onsuccess = () => resolve(request.result);
      request.onerror = () =>
        reject(request.error ?? new Error('Could not open IndexedDB'));
      request.onblocked = () =>
        reject(new Error('IndexedDB upgrade is blocked'));
    });
  }
}

function requestResult<T>(request: IDBRequest<T>): Promise<T> {
  return new Promise((resolve, reject) => {
    request.onsuccess = () => resolve(request.result);
    request.onerror = () =>
      reject(request.error ?? new Error('IndexedDB request failed'));
  });
}

function transactionComplete(transaction: IDBTransaction): Promise<void> {
  return new Promise((resolve, reject) => {
    transaction.oncomplete = () => resolve();
    transaction.onerror = () =>
      reject(transaction.error ?? new Error('IndexedDB transaction failed'));
    transaction.onabort = () =>
      reject(transaction.error ?? new Error('IndexedDB transaction aborted'));
  });
}
