import { Inject, Injectable } from "@nestjs/common";
import Redis from "ioredis";
import { Logger } from "nestjs-pino";

/**
 * Core cache service
 * 
 * Provides Redis-backed caching infrastructure for the entire application.
 * This is a low-level service - domain-specific cache services should wrap this
 * to provide higher-level caching patterns (CDC invalidation, request coalescing, etc).
 * 
 * Features:
 * - JSON serialization/deserialization
 * - TTL support (optional)
 * - Pattern-based operations (scan, delete, count)
 * - Memory usage tracking
 * - Automatic error handling and logging
 */
@Injectable()
export class CacheService {
  constructor(
    @Inject("REDIS_CLIENT") private readonly redis: Redis,
    @Inject(Logger) private readonly logger: Logger
  ) {}

  /**
   * Get a cached value by key
   * Returns null if key doesn't exist or value cannot be parsed
   */
  async get<T>(key: string): Promise<T | null> {
    const value = await this.redis.get(key);
    if (!value) {
      return null;
    }

    try {
      return JSON.parse(value) as T;
    } catch (error) {
      this.logger.warn({ key, error }, "Failed to parse cached value; evicting entry");
      await this.redis.del(key);
      return null;
    }
  }

  /**
   * Set a cached value
   * @param key Cache key
   * @param value Value to cache (will be JSON serialized)
   * @param ttlSeconds Optional TTL in seconds (null = no expiry)
   */
  async set(key: string, value: unknown, ttlSeconds?: number): Promise<void> {
    const serialized = JSON.stringify(value);
    if (ttlSeconds !== undefined) {
      const ttl = Math.max(0, Math.floor(ttlSeconds));
      if (ttl > 0) {
        await this.redis.set(key, serialized, "EX", ttl);
      } else {
        // Allow callers to request immediate expiry without leaking stale values
        await this.redis.set(key, serialized, "PX", 1);
      }
      return;
    }

    await this.redis.set(key, serialized);
  }

  /**
   * Delete a single cache key
   */
  async del(key: string): Promise<void> {
    await this.redis.del(key);
  }

  /**
   * Delete all keys matching a pattern
   * Uses SCAN for safe operation on large datasets
   * @param pattern Redis pattern (e.g., "orders:*", "catalog:product:*")
   */
  async delPattern(pattern: string): Promise<void> {
    const pipeline = this.redis.pipeline();
    let pending = 0;

    const flush = async () => {
      if (pending === 0) {
        return;
      }
      await pipeline.exec();
      pending = 0;
    };

    await this.scanPattern(pattern, async keys => {
      keys.forEach(key => {
        pipeline.del(key);
        pending += 1;
      });
      if (pending >= 1000) {
        await flush();
      }
    });

    await flush();
  }

  /**
   * Count keys matching a pattern
   * @param pattern Redis pattern (e.g., "orders:*")
   */
  async countByPattern(pattern: string): Promise<number> {
    let total = 0;
    await this.scanPattern(pattern, keys => {
      total += keys.length;
    });
    return total;
  }

  /**
   * Get total memory usage of keys matching a pattern
   * @param pattern Redis pattern (e.g., "orders:*")
   * @returns Total memory usage in bytes
   */
  async memoryUsageByPattern(pattern: string): Promise<number> {
    let total = 0;
    await this.scanPattern(pattern, async keys => {
      const pipeline = this.redis.pipeline();
      keys.forEach(key => {
        pipeline.memory("USAGE", key);
      });
      const results = await pipeline.exec();
      if (!results) {
        return;
      }
      results.forEach(result => {
        if (!result) {
          return;
        }
        const [error, usage] = result as [Error | null, unknown];
        if (!error && typeof usage === "number") {
          total += usage;
        }
      });
    });
    return total;
  }

  /**
   * Check if a key exists
   */
  async exists(key: string): Promise<boolean> {
    return (await this.redis.exists(key)) === 1;
  }

  /**
   * Build a structured cache key
   * @param prefix Key prefix (e.g., "orders", "catalog")
   * @param userId User/account identifier
   * @param parts Additional key parts
   * @returns Colon-separated key (e.g., "orders:user123:summary")
   */
  buildKey(prefix: string, userId: string, ...parts: string[]): string {
    return [prefix, userId, ...parts].join(":");
  }

  /**
   * Get or set pattern: Fetch from cache, or call fetcher and cache result
   * @param key Cache key
   * @param fetcher Function to fetch fresh data on cache miss
   * @param ttlSeconds TTL in seconds (default: 300)
   */
  async getOrSet<T>(key: string, fetcher: () => Promise<T>, ttlSeconds: number = 300): Promise<T> {
    const cached = await this.get<T>(key);
    if (cached !== null) {
      return cached;
    }

    const fresh = await fetcher();
    await this.set(key, fresh, ttlSeconds);
    return fresh;
  }

  /**
   * Scan keys matching a pattern and invoke callback for each batch
   * Uses cursor-based iteration for safe operation on large datasets
   * @param pattern Redis pattern
   * @param onKeys Callback invoked for each batch of matching keys
   */
  private async scanPattern(
    pattern: string,
    onKeys: (keys: string[]) => Promise<void> | void
  ): Promise<void> {
    let cursor = "0";
    do {
      const [next, keys] = (await this.redis.scan(
        cursor,
        "MATCH",
        pattern,
        "COUNT",
        1000
      )) as unknown as [string, string[]];
      cursor = next;
      if (keys && keys.length) {
        await onKeys(keys);
      }
    } while (cursor !== "0");
  }
}
