export const CacheTimeToLive = {
  oneMinute: 60 * 1000, // 1 minute
  fiveMinutes: 5 * 60 * 1000, // 5 minutes
  tenMinutes: 10 * 60 * 1000, // 10 minutes
  thirtyMinutes: 30 * 60 * 1000, // 30 minutes
  oneHour: 60 * 60 * 1000, // 1 hour
} as const;

export type CacheTimeToLive =
  (typeof CacheTimeToLive)[keyof typeof CacheTimeToLive];

/* eslint-disable @typescript-eslint/no-explicit-any */
/**
 * Options for configuring the Cache decorator
 */
export interface CacheOptions<T extends (...args: any[]) => any> {
  /**
   * Generate a cache key from the method arguments.
   * If not provided, uses JSON.stringify on all arguments.
   */
  keyGenerator?: (...args: Parameters<T>) => string;

  /**
   * Time in milliseconds to keep the result cached.
   * If not provided, cache never expires.
   */
  ttl?: number | CacheTimeToLive;
}

/**
 * Decorator that caches the results of both sync and async method calls.
 * Results are cached based on method arguments and expire after the specified TTL.
 *
 * @param options Configuration options for the decorator
 * @example
 * ```typescript
 * class DataService {
 *   // Async function caching
 *   @Cache({
 *     ttl: 5 * 60 * 1000, // Cache for 5 minutes
 *     keyGenerator: (params: QueryParams) => params.query
 *   })
 *   async searchData(params: QueryParams): Promise<SearchResult> {
 *     return await api.search(params);
 *   }
 *
 *   // Sync function caching (heavy calculations)
 *   @Cache({
 *     ttl: 10 * 60 * 1000, // Cache for 10 minutes
 *     keyGenerator: (n: number) => n.toString()
 *   })
 *   fibonacci(n: number): number {
 *     if (n <= 1) return n;
 *     return this.fibonacci(n - 1) + this.fibonacci(n - 2);
 *   }
 * }
 * ```
 */
export function Cache<T extends (...args: any[]) => any>(
  options: CacheOptions<T> = {},
): MethodDecorator {
  const cacheMap = new WeakMap<
    object,
    Map<string, { result: any; expiry?: number; isAsync: boolean }>
  >();

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

    descriptor.value = function (
      this: any,
      ...args: Parameters<T>
    ): ReturnType<T> {
      // Initialize cache for this instance if needed
      if (!cacheMap.has(this)) {
        cacheMap.set(this, new Map());
      }
      const instanceCache = cacheMap.get(this);
      if (!instanceCache) {
        throw new Error('Cache map not initialized properly');
      }

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

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

      // Check cache first
      const cached = instanceCache.get(key);
      if (cached) {
        // If no TTL or not expired, return cached result
        if (!cached.expiry || cached.expiry > Date.now()) {
          // For async functions, wrap cached result in a Promise
          if (cached.isAsync) {
            return Promise.resolve(cached.result) as ReturnType<T>;
          }
          return cached.result;
        }
        // Clean up expired cache entry
        instanceCache.delete(key);
      }

      // Execute original method
      const result = originalMethod.apply(this, args);

      // Handle both sync and async functions
      // Use more robust Promise detection for testing environments
      const isPromise =
        result instanceof Promise ||
        (result &&
          typeof result.then === 'function' &&
          typeof result.catch === 'function');

      if (isPromise) {
        // Async function: only cache successful results
        const promise = result
          .then((value: any) => {
            instanceCache.set(key, {
              result: value,
              expiry: options.ttl ? Date.now() + options.ttl : undefined,
              isAsync: true,
            });
            return value;
          })
          .catch((error: any) => {
            // Don't cache errors - ensure cache is clean
            instanceCache.delete(key);
            throw error;
          });

        return promise as ReturnType<T>;
      } else {
        // Sync function: cache result directly
        instanceCache.set(key, {
          result,
          expiry: options.ttl ? Date.now() + options.ttl : undefined,
          isAsync: false,
        });
        return result;
      }
    };

    return descriptor;
  };
}
