import type { UserExportPreset } from './preset.types';
import {
  parseUserPresetJson,
  serializeUserPreset,
  validateUserPreset,
} from './preset-validation';

export interface UserPresetPersistence {
  load(): Promise<readonly UserExportPreset[]>;
  save(presets: readonly UserExportPreset[]): Promise<void>;
}

export class UserPresetRegistry {
  readonly #persistence: UserPresetPersistence;
  #presets = new Map<string, UserExportPreset>();

  constructor(persistence: UserPresetPersistence) {
    this.#persistence = persistence;
  }

  async initialize(): Promise<void> {
    const loaded = await this.#persistence.load();
    const validated = loaded.map((preset) => {
      const result = validateUserPreset(preset);
      if (!result.valid || !result.preset) {
        throw new TypeError(
          `Stored preset is invalid: ${result.errors.join('; ')}`
        );
      }
      return result.preset;
    });
    this.#presets = new Map(validated.map((preset) => [preset.id, preset]));
  }

  list(): readonly UserExportPreset[] {
    return Object.freeze(
      [...this.#presets.values()].sort((left, right) =>
        left.name.localeCompare(right.name)
      )
    );
  }

  get(id: string): UserExportPreset | undefined {
    return this.#presets.get(id);
  }

  async save(preset: UserExportPreset): Promise<void> {
    const result = validateUserPreset(preset);
    if (!result.valid || !result.preset) {
      throw new TypeError(`Invalid preset: ${result.errors.join('; ')}`);
    }
    this.#presets.set(result.preset.id, result.preset);
    await this.#persist();
  }

  async clone(
    id: string,
    newId: string,
    newName: string
  ): Promise<UserExportPreset> {
    const original = this.#presets.get(id);
    if (!original) {
      throw new RangeError(`Unknown preset: ${id}`);
    }
    const cloned = { ...original, id: newId, name: newName };
    await this.save(cloned);
    return this.#presets.get(newId) as UserExportPreset;
  }

  async rename(id: string, name: string): Promise<void> {
    const preset = this.#presets.get(id);
    if (!preset) {
      throw new RangeError(`Unknown preset: ${id}`);
    }
    await this.save({ ...preset, name });
  }

  async delete(id: string): Promise<boolean> {
    const deleted = this.#presets.delete(id);
    if (deleted) {
      await this.#persist();
    }
    return deleted;
  }

  async reset(): Promise<void> {
    this.#presets.clear();
    await this.#persist();
  }

  export(id: string): string {
    const preset = this.#presets.get(id);
    if (!preset) {
      throw new RangeError(`Unknown preset: ${id}`);
    }
    return serializeUserPreset(preset);
  }

  async import(json: string): Promise<UserExportPreset> {
    const preset = parseUserPresetJson(json);
    await this.save(preset);
    return preset;
  }

  async #persist(): Promise<void> {
    await this.#persistence.save(this.list());
  }
}

export class MemoryUserPresetPersistence implements UserPresetPersistence {
  #presets: readonly UserExportPreset[] = [];

  async load(): Promise<readonly UserExportPreset[]> {
    return this.#presets;
  }

  async save(presets: readonly UserExportPreset[]): Promise<void> {
    this.#presets = structuredClone(presets);
  }
}

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

/**
 * Stores only validated typed preset documents. A dedicated database avoids
 * version races with durable job metadata stores that may be opened first.
 */
export class IndexedDbUserPresetPersistence implements UserPresetPersistence {
  readonly #databaseName: string;
  readonly #storeName: string;
  readonly #factory: IDBFactory;
  #database?: Promise<IDBDatabase>;

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

  async load(): Promise<readonly UserExportPreset[]> {
    const request = (await this.#store('readonly')).get('all');
    const value = await requestResult<unknown>(request);
    if (value === undefined) {
      return Object.freeze([]);
    }
    if (!Array.isArray(value)) {
      throw new TypeError('Stored user presets are invalid');
    }
    return Object.freeze(
      value.map((preset) => {
        const result = validateUserPreset(preset);
        if (!result.valid || !result.preset) {
          throw new TypeError(
            `Stored preset is invalid: ${result.errors.join('; ')}`
          );
        }
        return result.preset;
      })
    );
  }

  async save(presets: readonly UserExportPreset[]): Promise<void> {
    const validated = presets.map((preset) => {
      const result = validateUserPreset(preset);
      if (!result.valid || !result.preset) {
        throw new TypeError(`Invalid preset: ${result.errors.join('; ')}`);
      }
      return result.preset;
    });
    const store = await this.#store('readwrite');
    const completion = transactionComplete(store.transaction);
    await requestResult<IDBValidKey>(
      store.put(structuredClone(validated), 'all')
    );
    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);
        }
      };
      request.onsuccess = () => resolve(request.result);
      request.onerror = () =>
        reject(request.error ?? new Error('Could not open preset storage'));
      request.onblocked = () =>
        reject(new Error('Preset storage 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('Preset storage request failed'));
  });
}

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