import { Injectable, Logger, Optional } from '@nestjs/common';
import { RedisService } from '../../infrastructure/redis/redis.service';

const NULL_SENTINEL = '__CACHE_NULL__';
const DEFAULT_TTL = 300;
const MIN_TTL = 30;

/**
 * Key naming convention:
 *   module:entity:id  →  e.g. config:app:theme, workspace:dashboard:userId
 *   module:entity     →  e.g. safety:sensitive-words
 */
export function cacheKey(module: string, entity: string, id?: string): string {
  return id ? `${module}:${entity}:${id}` : `${module}:${entity}`;
}

@Injectable()
export class CacheService {
  private readonly logger = new Logger(CacheService.name);
  private hits = 0;
  private misses = 0;

  constructor(@Optional() private readonly redis?: RedisService) {}

  private get store() {
    return this.redis;
  }

  private isAvailable(): boolean {
    return !!(this.redis?.isHealthy());
  }

  async get<T>(key: string): Promise<T | null> {
    if (!this.isAvailable()) return null;

    try {
      const raw = await this.redis!.get(key);
      if (raw === null) { this.misses++; return null; }
      if (raw === NULL_SENTINEL) { this.hits++; return null; }
      this.hits++;
      return JSON.parse(raw) as T;
    } catch (err: any) {
      this.logger.warn(`Cache get error for ${key}: ${err.message}`);
      return null;
    }
  }

  async set(key: string, value: any, ttl?: number): Promise<void> {
    if (!this.isAvailable()) return;

    const finalTtl = ttl ?? DEFAULT_TTL;
    try {
      await this.redis!.set(key, JSON.stringify(value), Math.max(finalTtl, MIN_TTL));
    } catch (err: any) {
      this.logger.warn(`Cache set error for ${key}: ${err.message}`);
    }
  }

  async setNull(key: string, ttl?: number): Promise<void> {
    if (!this.isAvailable()) return;
    // Shorter TTL for null values to reduce staleness
    const nullTtl = Math.min(ttl ?? DEFAULT_TTL, 60);
    try {
      await this.redis!.set(key, NULL_SENTINEL, Math.max(nullTtl, MIN_TTL));
    } catch (err: any) {
      this.logger.warn(`Cache setNull error for ${key}: ${err.message}`);
    }
  }

  async del(key: string): Promise<void> {
    if (!this.isAvailable()) return;

    try {
      await this.redis!.del(key);
    } catch (err: any) {
      this.logger.warn(`Cache del error for ${key}: ${err.message}`);
    }
  }

  /**
   * Cache-aside pattern with null-value protection (cache penetration).
   * If factory resolves to null, a short-lived null sentinel is cached.
   */
  async wrap<T>(key: string, factory: () => Promise<T | null>, ttl?: number): Promise<T | null> {
    if (!this.isAvailable()) return factory();

    const cached = await this.get<T | null>(key);
    if (cached !== null) return cached;

    // Check for null sentinel
    try {
      const raw = await this.redis!.get(key);
      if (raw === NULL_SENTINEL) { this.hits++; return null; }
    } catch {}

    const value = await factory();

    if (value === null || value === undefined) {
      await this.setNull(key, ttl);
    } else {
      await this.set(key, value, ttl);
    }

    return value;
  }

  /**
   * Delete all keys matching a pattern (uses SCAN in production-safe manner)
   */
  async flushPattern(pattern: string): Promise<number> {
    if (!this.isAvailable()) return 0;

    try {
      const keys = await this.redis!.keys(`cache:${pattern}`);
      let count = 0;
      for (const key of keys) {
        await this.redis!.del(key);
        count++;
      }
      this.logger.log(`Flushed ${count} keys matching cache:${pattern}`);
      return count;
    } catch (err: any) {
      this.logger.warn(`Cache flushPattern error: ${err.message}`);
      return 0;
    }
  }

  async flushAll(): Promise<number> {
    if (!this.isAvailable()) return 0;

    try {
      const keys = await this.redis!.keys('cache:*');
      let count = 0;
      for (const key of keys) {
        await this.redis!.del(key);
        count++;
      }
      this.logger.log(`Flushed ${count} cache keys`);
      return count;
    } catch (err: any) {
      this.logger.warn(`Cache flushAll error: ${err.message}`);
      return 0;
    }
  }

  getStats() {
    return {
      hits: this.hits,
      misses: this.misses,
      hitRate: this.hits + this.misses > 0
        ? Math.round((this.hits / (this.hits + this.misses)) * 100) / 100
        : 0,
      available: this.isAvailable(),
    };
  }

  resetStats() {
    this.hits = 0;
    this.misses = 0;
  }
}
