/* eslint-disable @typescript-eslint/no-explicit-any */
/**
 * Options for configuring the InFlight decorator
 */
export interface InFlightOptions<T extends (...args: any[]) => any> {
  /**
   * Generate a cache key from the method arguments.
   * If not provided, uses JSON.stringify on all arguments.
   * If omitted entirely, the decorator will not differentiate between calls with different arguments.
   */
  keyGenerator?: (...args: Parameters<T>) => string;
}

/**
 * Decorator that prevents multiple simultaneous calls to the same async method.
 * All concurrent calls will receive the same Promise result.
 *
 * @param options Configuration options for the decorator
 * @example
 * ```typescript
 * class MyService {
 *   // Basic usage - all calls share the same in-flight request
 *   @InFlight()
 *   async fetchData(): Promise<Data> {
 *     return await api.getData();
 *   }
 *
 *   // With key generator - calls with different arguments can execute simultaneously
 *   @InFlight({
 *     keyGenerator: (userId: string) => userId
 *   })
 *   async fetchUser(userId: string): Promise<User> {
 *     return await api.getUser(userId);
 *   }
 * }
 * ```
 */
export function InFlight<T extends (...args: any[]) => Promise<any>>(
  options: InFlightOptions<T> = {},
): MethodDecorator {
  // If keyGenerator is explicitly provided (even if undefined), use keyed mode
  const useKeys = 'keyGenerator' in options;

  const simpleInFlightMap = new WeakMap<object, Promise<any>>();
  const keyedInFlightMap = new WeakMap<object, Map<string, Promise<any>>>();

  return function (
    _target: any,
    _propertyKey: string | symbol,
    descriptor: PropertyDescriptor,
  ): PropertyDescriptor {
    const originalMethod = descriptor.value;

    descriptor.value = async function (
      this: any,
      ...args: Parameters<T>
    ): Promise<ReturnType<T>> {
      if (!useKeys) {
        // Simple mode: one in-flight request per instance
        const existingRequest = simpleInFlightMap.get(this);
        if (existingRequest) {
          return existingRequest;
        }

        const promise = originalMethod
          .apply(this, args)
          .then((result: any) => {
            simpleInFlightMap.delete(this);
            return result;
          })
          .catch((error: any) => {
            simpleInFlightMap.delete(this);
            throw error;
          });

        simpleInFlightMap.set(this, promise);
        return promise;
      } else {
        // Keyed mode: separate in-flight requests per key
        if (!keyedInFlightMap.has(this)) {
          keyedInFlightMap.set(this, new Map());
        }
        const instanceMap = keyedInFlightMap.get(this);
        if (!instanceMap) {
          throw new Error('In-flight map not initialized properly');
        }

        const argsWithoutAbortSignal = args.filter(
          (arg) => !(arg instanceof AbortSignal),
        ) as Parameters<T>;

        const key = options.keyGenerator
          ? options.keyGenerator(...argsWithoutAbortSignal)
          : JSON.stringify(argsWithoutAbortSignal);

        const existingRequest = instanceMap.get(key);
        if (existingRequest) {
          return existingRequest;
        }

        const promise = originalMethod
          .apply(this, args)
          .then((result: any) => {
            instanceMap.delete(key);
            return result;
          })
          .catch((error: any) => {
            instanceMap.delete(key);
            throw error;
          });

        instanceMap.set(key, promise);
        return promise;
      }
    };

    return descriptor;
  };
}
