import { Injectable, type OnModuleInit } from '@nestjs/common';
import { InjectMetric } from '@willsoto/nestjs-prometheus';
import { type Counter } from 'prom-client';
import { type LoggerService } from './logger.service';
import { type RedisService } from './redis.service';

export const CACHE_HIT_TOTAL = 'cache_hit_total';
export const CACHE_MISS_TOTAL = 'cache_miss_total';

export const CacheTTL = {
  /** Listing detail — moderate TTL, invalidated on mutation */
  LISTING_DETAIL: 300, // 5 min
  /** Search results — short TTL, invalidated on listing mutations */
  SEARCH_RESULTS: 120, // 2 min
  /** District stats — moderate TTL, invalidated on listing events */
  DISTRICT_STATS: 300, // 5 min
  /** Market report — moderate TTL, invalidated on listing events */
  MARKET_REPORT: 900, // 15 min
  /** Heatmap data — moderate TTL, invalidated on listing events */
  HEATMAP: 300, // 5 min
  /** Price trend — long TTL, historical data changes infrequently */
  MARKET_DATA: 1800, // 30 min
  /** User profile — moderate TTL, invalidated on mutation */
  USER_PROFILE: 600, // 10 min
  /** User quota — very short TTL, invalidated on usage metering and plan changes */
  USER_QUOTA: 60, // 1 min
} as const;

export enum CachePrefix {
  LISTING = 'cache:listing',
  SEARCH = 'cache:search',
  GEO_SEARCH = 'cache:geo_search',
  MARKET_REPORT = 'cache:market:report',
  MARKET_TREND = 'cache:market:trend',
  MARKET_HEATMAP = 'cache:market:heatmap',
  MARKET_DISTRICT = 'cache:market:district',
  USER_PROFILE = 'cache:user:profile',
  USER_QUOTA = 'cache:user:quota',
}

@Injectable()
export class CacheService implements OnModuleInit {
  constructor(
    private readonly redis: RedisService,
    private readonly logger: LoggerService,
    @InjectMetric(CACHE_HIT_TOTAL) private readonly cacheHitCounter: Counter,
    @InjectMetric(CACHE_MISS_TOTAL) private readonly cacheMissCounter: Counter,
  ) {}

  onModuleInit(): void {
    this.logger.log('CacheService initialized', 'CacheService');
  }

  /**
   * Cache-aside: get from cache, or execute loader and store result.
   */
  async getOrSet<T>(
    key: string,
    loader: () => Promise<T>,
    ttlSeconds: number,
    resource: string,
  ): Promise<T> {
    try {
      const cached = await this.redis.get(key);
      if (cached !== null) {
        this.cacheHitCounter.inc({ resource });
        return JSON.parse(cached) as T;
      }
    } catch (err) {
      this.logger.warn(`Cache read error for ${key}: ${(err as Error).message}`, 'CacheService');
    }

    this.cacheMissCounter.inc({ resource });
    const result = await loader();

    try {
      await this.redis.set(key, JSON.stringify(result), ttlSeconds);
    } catch (err) {
      this.logger.warn(`Cache write error for ${key}: ${(err as Error).message}`, 'CacheService');
    }

    return result;
  }

  /** Invalidate a single cache key. */
  async invalidate(key: string): Promise<void> {
    try {
      await this.redis.del(key);
    } catch (err) {
      this.logger.warn(`Cache invalidate error for ${key}: ${(err as Error).message}`, 'CacheService');
    }
  }

  /** Invalidate all keys matching a prefix using SCAN (non-blocking). */
  async invalidateByPrefix(prefix: string): Promise<void> {
    try {
      const client = this.redis.getClient();
      let cursor = '0';
      do {
        const [nextCursor, keys] = await client.scan(cursor, 'MATCH', `${prefix}:*`, 'COUNT', 100);
        cursor = nextCursor;
        if (keys.length > 0) {
          await client.del(...keys);
        }
      } while (cursor !== '0');
    } catch (err) {
      this.logger.warn(`Cache prefix invalidate error for ${prefix}: ${(err as Error).message}`, 'CacheService');
    }
  }

  /** Build a deterministic cache key from prefix + parts. */
  static buildKey(prefix: CachePrefix, ...parts: (string | number | undefined)[]): string {
    const sanitized = parts
      .filter((p) => p !== undefined)
      .map((p) => String(p).toLowerCase().replace(/\s+/g, '_'));
    return `${prefix}:${sanitized.join(':')}`;
  }
}
