import pTimeout from 'p-timeout';
import type { MutexInterface } from 'async-mutex';

import type { Locks } from './locks';
import * as exceptions from '../errors';
import { events } from '../events/index';
import type { CacheStack } from './stack/cache-stack';
import type { GetSetFactory } from '../types/helpers';
import type { CacheStackWriter } from './stack/cache-stack-writer';
import type { CacheEntryOptions } from './cache-entry/cache-entry-options';

/**
 * Factory Runner is responsible for executing factories
 */
export class FactoryRunner {
  #stack: CacheStack;
  #stackWriter: CacheStackWriter;
  #locks: Locks;

  constructor(stack: CacheStack, stackWriter: CacheStackWriter, locks: Locks) {
    this.#stack = stack;
    this.#stackWriter = stackWriter;
    this.#locks = locks;
  }

  async saveBackgroundFactoryResult(
    key: string,
    factoryResult: unknown,
    options: CacheEntryOptions,
    lockReleaser: MutexInterface.Releaser,
  ) {
    await this.#stackWriter.set(key, factoryResult, options);
    this.#locks.release(key, lockReleaser);
  }

  async writeFactoryResult(
    key: string,
    item: unknown,
    options: CacheEntryOptions,
    lockReleaser: MutexInterface.Releaser,
  ) {
    await this.#stackWriter.set(key, item, options);

    this.#stack.emit(new events.CacheMiss(key, this.#stack.name));
    this.#stack.logger.trace({ key, cache: this.#stack.name, opId: options.id }, 'cache miss');
    this.#locks.release(key, lockReleaser);
  }

  async run(
    key: string,
    factory: GetSetFactory,
    hasFallback: boolean,
    options: CacheEntryOptions,
    lockReleaser: MutexInterface.Releaser,
  ) {
    const timeoutDuration = options.factoryTimeout(hasFallback);
    const timeoutException =
      timeoutDuration === options.timeouts?.hard
        ? exceptions.E_FACTORY_HARD_TIMEOUT
        : exceptions.E_FACTORY_SOFT_TIMEOUT;

    const promisifiedFactory = async () => {
      return await factory({ setTtl: (ttl) => options.setLogicalTtl(ttl) });
    };

    const factoryPromise = promisifiedFactory();

    const factoryResult = await pTimeout(factoryPromise, {
      milliseconds: timeoutDuration ?? Number.POSITIVE_INFINITY,
      fallback: async () => {
        factoryPromise
          .then((result) => this.saveBackgroundFactoryResult(key, result, options, lockReleaser))
          .catch(() => {})
          .finally(() => this.#locks.release(key, lockReleaser));

        throw new timeoutException();
      },
    });

    await this.writeFactoryResult(key, factoryResult, options, lockReleaser);
    return factoryResult;
  }
}
