import { type CachePrefix, CacheService, type CacheTTL } from '../cache.service';

/**
 * Metadata key for @Cacheable decorator options.
 */
export const CACHEABLE_METADATA = Symbol('CACHEABLE_METADATA');

export interface CacheableOptions {
  /** Cache key prefix — use CachePrefix enum values. */
  prefix: CachePrefix;
  /** TTL in seconds — use CacheTTL constants. */
  ttl: (typeof CacheTTL)[keyof typeof CacheTTL];
  /** Resource label for Prometheus metrics. */
  resource: string;
  /**
   * Extract cache key parts from method arguments.
   * Return an array of strings that will be joined with the prefix.
   * Defaults to using the first argument's string representation.
   */
  keyFrom?: (...args: unknown[]) => (string | number | undefined)[];
}

/**
 * Declarative cache-aside decorator for query handler `execute()` methods.
 *
 * Usage:
 * ```ts
 * @Cacheable({ prefix: CachePrefix.LISTING, ttl: CacheTTL.LISTING_DETAIL, resource: 'listing' })
 * async execute(query: GetListingQuery): Promise<ListingDto> { ... }
 * ```
 *
 * Requires the class to have a `cacheService: CacheService` property
 * (injected via constructor).
 */
export function Cacheable(options: CacheableOptions): MethodDecorator {
  return function (_target: object, _propertyKey: string | symbol, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value as (...args: unknown[]) => Promise<unknown>;

    descriptor.value = async function (this: { cacheService: CacheService }, ...args: unknown[]) {
      const keyParts = options.keyFrom
        ? options.keyFrom(...args)
        : [String(args[0] ?? 'default')];

      const cacheKey = CacheService.buildKey(options.prefix, ...keyParts);

      return this.cacheService.getOrSet(
        cacheKey,
        () => originalMethod.apply(this, args),
        options.ttl,
        options.resource,
      );
    };

    return descriptor;
  };
}
