import { Injectable } from "@nestjs/common";
import { CacheService } from "@bff/infra/cache/cache.service";
import type { CacheBucketMetrics, CacheDependencies, CacheOptions } from "@bff/infra/cache/cache.types";

export interface CatalogCacheSnapshot {
  catalog: CacheBucketMetrics;
  static: CacheBucketMetrics;
  volatile: CacheBucketMetrics;
  eligibility: CacheBucketMetrics;
  invalidations: number;
}

export interface CatalogCacheOptions<T> {
  allowNull?: boolean;
  resolveDependencies?: (
    value: T
  ) => CacheDependencies | Promise<CacheDependencies | undefined> | undefined;
}

/**
 * Catalog cache service
 * 
 * Uses CDC (Change Data Capture) for real-time cache invalidation with
 * product dependency tracking for granular invalidation.
 * 
 * Features:
 * - CDC-driven invalidation: No TTL, cache persists until CDC event
 * - Product dependency tracking: Granular invalidation by product IDs
 * - Request coalescing: Prevents thundering herd on cache miss
 * - Metrics tracking: Monitors hits, misses, and invalidations
 * 
 * Cache buckets:
 * - catalog: Product catalog data (CDC-driven)
 * - static: Static reference data (CDC-driven)
 * - eligibility: Account eligibility data (CDC-driven)
 * - volatile: Frequently changing data (60s TTL)
 */
@Injectable()
export class CatalogCacheService {
  // CDC-driven invalidation: null TTL means cache persists until explicit invalidation
  private readonly CATALOG_TTL: number | null = null;
  private readonly STATIC_TTL: number | null = null;
  private readonly ELIGIBILITY_TTL: number | null = null;
  private readonly VOLATILE_TTL = 60; // Volatile data still uses TTL

  private readonly metrics: CatalogCacheSnapshot = {
    catalog: { hits: 0, misses: 0 },
    static: { hits: 0, misses: 0 },
    volatile: { hits: 0, misses: 0 },
    eligibility: { hits: 0, misses: 0 },
    invalidations: 0,
  };

  // Request coalescing: Prevents duplicate API calls when multiple users
  // request the same data after CDC invalidation
  private readonly inflightRequests = new Map<string, Promise<unknown>>();

  constructor(private readonly cache: CacheService) {}

  /**
   * Get or fetch catalog data (CDC-driven cache, no TTL)
   */
  async getCachedCatalog<T>(
    key: string,
    fetchFn: () => Promise<T>,
    options?: CatalogCacheOptions<T>
  ): Promise<T> {
    return this.getOrSet("catalog", key, this.CATALOG_TTL, fetchFn, options);
  }

  /**
   * Get or fetch static catalog data (CDC-driven cache, no TTL)
   */
  async getCachedStatic<T>(key: string, fetchFn: () => Promise<T>): Promise<T> {
    return this.getOrSet("static", key, this.STATIC_TTL, fetchFn);
  }

  /**
   * Get or fetch volatile catalog data (60s TTL)
   */
  async getCachedVolatile<T>(key: string, fetchFn: () => Promise<T>): Promise<T> {
    return this.getOrSet("volatile", key, this.VOLATILE_TTL, fetchFn);
  }

  /**
   * Get or fetch eligibility data (CDC-driven cache, no TTL)
   */
  async getCachedEligibility<T>(key: string, fetchFn: () => Promise<T>): Promise<T> {
    return this.getOrSet("eligibility", key, this.ELIGIBILITY_TTL, fetchFn, {
      allowNull: true,
    });
  }

  /**
   * Build cache key for catalog data
   */
  buildCatalogKey(catalogType: string, ...parts: string[]): string {
    return `catalog:${catalogType}:${parts.join(":")}`;
  }

  buildEligibilityKey(_catalogType: string, accountId: string): string {
    return `catalog:eligibility:${accountId}`;
  }

  /**
   * Invalidate catalog cache by pattern
   */
  async invalidateCatalog(catalogType: string): Promise<void> {
    this.metrics.invalidations++;
    await this.cache.delPattern(`catalog:${catalogType}:*`);
    await this.flushProductDependencyIndex();
  }

  /**
   * Invalidate eligibility caches for an account
   */
  async invalidateEligibility(accountId: string): Promise<void> {
    if (!accountId) return;
    this.metrics.invalidations++;
    await this.cache.del(this.buildEligibilityKey("", accountId));
  }

  /**
   * Invalidate all catalog cache entries
   */
  async invalidateAllCatalogs(): Promise<void> {
    this.metrics.invalidations++;
    await this.cache.delPattern("catalog:*");
    await this.flushProductDependencyIndex();
  }

  /**
   * Get TTL configuration for monitoring
   */
  getTtlConfiguration(): {
    catalogSeconds: number | null;
    eligibilitySeconds: number | null;
    staticSeconds: number | null;
    volatileSeconds: number;
  } {
    return {
      catalogSeconds: this.CATALOG_TTL ?? null,
      eligibilitySeconds: this.ELIGIBILITY_TTL ?? null,
      staticSeconds: this.STATIC_TTL ?? null,
      volatileSeconds: this.VOLATILE_TTL,
    };
  }

  /**
   * Get cache metrics for monitoring
   */
  getMetrics(): CatalogCacheSnapshot {
    return {
      catalog: { ...this.metrics.catalog },
      static: { ...this.metrics.static },
      volatile: { ...this.metrics.volatile },
      eligibility: { ...this.metrics.eligibility },
      invalidations: this.metrics.invalidations,
    };
  }

  /**
   * Set eligibility value for an account
   */
  async setEligibilityValue(
    accountId: string,
    eligibility: string | null | undefined
  ): Promise<void> {
    const key = this.buildEligibilityKey("", accountId);
    const payload =
      typeof eligibility === "string"
        ? { Id: accountId, Internet_Eligibility__c: eligibility }
        : null;
    if (this.ELIGIBILITY_TTL === null) {
      await this.cache.set(key, payload);
    } else {
      await this.cache.set(key, payload, this.ELIGIBILITY_TTL);
    }
  }

  private async getOrSet<T>(
    bucket: "catalog" | "static" | "volatile" | "eligibility",
    key: string,
    ttlSeconds: number | null,
    fetchFn: () => Promise<T>,
    options?: CatalogCacheOptions<T>
  ): Promise<T> {
    const allowNull = options?.allowNull ?? false;
    
    // Check Redis cache first
    const cached = await this.cache.get<T>(key);

    if (cached !== null) {
      if (allowNull || cached !== null) {
        this.metrics[bucket].hits++;
        return cached;
      }
    }

    // Check for in-flight request (prevents thundering herd)
    const existingRequest = this.inflightRequests.get(key);
    if (existingRequest) {
      return existingRequest as Promise<T>;
    }

    // Fetch fresh data
    this.metrics[bucket].misses++;

    const fetchPromise = (async () => {
      try {
        const fresh = await fetchFn();
        const valueToStore = allowNull ? (fresh ?? null) : fresh;
        const dependencies = options?.resolveDependencies
          ? await options.resolveDependencies(fresh)
          : undefined;

        // Clean up old dependencies before storing new value
        const existingDeps = await this.getDependencies(key);
        if (existingDeps) {
          await this.unlinkDependenciesForKey(key, existingDeps);
        }

        // Store value in Redis
        if (ttlSeconds === null) {
          await this.cache.set(key, valueToStore);
        } else {
          await this.cache.set(key, valueToStore, ttlSeconds);
        }

        // Store and link dependencies separately
        if (dependencies) {
          await this.storeDependencies(key, dependencies);
          await this.linkDependencies(key, dependencies);
        }

        return fresh;
      } finally {
        this.inflightRequests.delete(key);
      }
    })();

    this.inflightRequests.set(key, fetchPromise);
    return fetchPromise;
  }

  /**
   * Invalidate catalog entries by product IDs
   * Returns true if any entries were invalidated, false if no matches found
   */
  async invalidateProducts(productIds: string[]): Promise<boolean> {
    const uniqueIds = Array.from(new Set((productIds ?? []).filter(Boolean)));
    if (uniqueIds.length === 0) {
      return false;
    }

    const keysToInvalidate = new Set<string>();

    for (const productId of uniqueIds) {
      const indexKey = this.buildProductDependencyKey(productId);
      const index = await this.cache.get<{ keys?: string[] }>(indexKey);
      const keys = index?.keys ?? [];
      keys.forEach(k => keysToInvalidate.add(k));
      if (keys.length === 0) {
        continue;
      }
    }

    if (keysToInvalidate.size === 0) {
      return false;
    }

    for (const key of keysToInvalidate) {
      const deps = await this.getDependencies(key);
      if (deps) {
        await this.unlinkDependenciesForKey(key, deps);
      }
      await this.cache.del(key);
      await this.cache.del(this.buildDependencyMetaKey(key));
      this.metrics.invalidations++;
    }

    return true;
  }

  /**
   * Store dependencies metadata for a cache key
   */
  private async storeDependencies(key: string, dependencies: CacheDependencies): Promise<void> {
    const normalized = this.normalizeDependencies(dependencies);
    if (normalized) {
      const metaKey = this.buildDependencyMetaKey(key);
      await this.cache.set(metaKey, normalized);
    }
  }

  /**
   * Get dependencies metadata for a cache key
   */
  private async getDependencies(key: string): Promise<CacheDependencies | null> {
    const metaKey = this.buildDependencyMetaKey(key);
    return await this.cache.get<CacheDependencies>(metaKey);
  }

  /**
   * Build key for storing dependency metadata
   */
  private buildDependencyMetaKey(cacheKey: string): string {
    return `${cacheKey}:deps`;
  }

  private normalizeDependencies(dependencies: CacheDependencies): CacheDependencies | undefined {
    const productIds = dependencies.productIds?.filter(Boolean);
    if (!productIds || productIds.length === 0) {
      return undefined;
    }
    return { productIds: Array.from(new Set(productIds)) };
  }

  private async linkDependencies(key: string, dependencies: CacheDependencies): Promise<void> {
    const normalized = this.normalizeDependencies(dependencies);
    if (!normalized) {
      return;
    }

    if (normalized.productIds) {
      for (const productId of normalized.productIds) {
        const indexKey = this.buildProductDependencyKey(productId);
        const existing = (await this.cache.get<{ keys?: string[] }>(indexKey))?.keys ?? [];
        if (!existing.includes(key)) {
          existing.push(key);
        }
        await this.cache.set(indexKey, { keys: existing });
      }
    }
  }

  private async unlinkDependenciesForKey(
    key: string,
    dependencies: CacheDependencies
  ): Promise<void> {
    const normalized = this.normalizeDependencies(dependencies);
    if (!normalized) {
      return;
    }

    if (normalized.productIds) {
      for (const productId of normalized.productIds) {
        const indexKey = this.buildProductDependencyKey(productId);
        const existing = await this.cache.get<{ keys?: string[] }>(indexKey);
        if (!existing?.keys?.length) {
          continue;
        }
        const nextKeys = existing.keys.filter(k => k !== key);
        if (nextKeys.length === 0) {
          await this.cache.del(indexKey);
        } else {
          await this.cache.set(indexKey, { keys: nextKeys });
        }
      }
    }
  }

  private buildProductDependencyKey(productId: string): string {
    return `catalog:deps:product:${productId}`;
  }

  private async flushProductDependencyIndex(): Promise<void> {
    await this.cache.delPattern("catalog:deps:product:*");
  }
}

export interface CatalogCacheOptions<T> {
  allowNull?: boolean;
  resolveDependencies?: (
    value: T
  ) => CacheDependencies | Promise<CacheDependencies | undefined> | undefined;
}
