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

export interface ServicesCacheSnapshot {
  services: CacheBucketMetrics;
  static: CacheBucketMetrics;
  volatile: CacheBucketMetrics;
  eligibility: CacheBucketMetrics;
  verification: CacheBucketMetrics;
  invalidations: number;
}

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

interface GetOrSetParams<T> {
  bucket: "services" | "static" | "volatile" | "eligibility" | "verification";
  key: string;
  ttlSeconds: number | null;
  fetchFn: () => Promise<T>;
  options?: ServicesCacheOptions<T> | undefined;
}

interface LegacyCatalogCachePayload<T> {
  value: T;
  __catalogCache?: boolean;
  dependencies?: CacheDependencies;
}

/**
 * Services cache service
 *
 * Uses CDC (Change Data Capture) for real-time cache invalidation with
 * product dependency tracking for granular invalidation.
 *
 * Features:
 * - Event-driven invalidation: CDC / Platform Events invalidate caches on change
 * - Safety TTL: long TTL to self-heal if events are missed
 * - 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 (event-driven + safety TTL)
 * - static: Static reference data (event-driven + safety TTL)
 * - eligibility: Account eligibility data (event-driven + safety TTL)
 * - volatile: Frequently changing data (60s TTL)
 */
@Injectable()
export class ServicesCacheService {
  // CDC-driven invalidation + safety TTL (self-heal if events are missed)
  private readonly SERVICES_TTL: number | null;
  private readonly STATIC_TTL: number | null;
  private readonly ELIGIBILITY_TTL: number | null;
  private readonly VERIFICATION_TTL: number | null;
  private readonly VOLATILE_TTL = 60; // Volatile data still uses TTL

  private readonly metrics: ServicesCacheSnapshot = {
    services: { hits: 0, misses: 0 },
    static: { hits: 0, misses: 0 },
    volatile: { hits: 0, misses: 0 },
    eligibility: { hits: 0, misses: 0 },
    verification: { 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,
    private readonly config: ConfigService
  ) {
    const raw = this.config.get<number>("SERVICES_CACHE_SAFETY_TTL_SECONDS", 60 * 60 * 12);
    const ttl = typeof raw === "number" && Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : null;

    // Apply to CDC-driven buckets (catalog + static + eligibility + verification)
    this.SERVICES_TTL = ttl;
    this.STATIC_TTL = ttl;
    this.ELIGIBILITY_TTL = ttl;
    this.VERIFICATION_TTL = ttl;
  }

  /**
   * Get or fetch catalog data (CDC-driven cache with safety TTL)
   */
  async getCachedServices<T>(
    key: string,
    fetchFn: () => Promise<T>,
    options?: ServicesCacheOptions<T>
  ): Promise<T> {
    return this.getOrSet({
      bucket: "services",
      key,
      ttlSeconds: this.SERVICES_TTL,
      fetchFn,
      options,
    });
  }

  /**
   * Get or fetch static catalog data (CDC-driven cache with safety TTL)
   */
  async getCachedStatic<T>(key: string, fetchFn: () => Promise<T>): Promise<T> {
    return this.getOrSet({ bucket: "static", key, ttlSeconds: 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({ bucket: "volatile", key, ttlSeconds: this.VOLATILE_TTL, fetchFn });
  }

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

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

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

  buildVerificationKey(accountId: string): string {
    return `services:verification:${accountId}`;
  }

  /**
   * Invalidate catalog cache by pattern
   */
  async invalidateServices(serviceType: string): Promise<void> {
    this.metrics.invalidations++;
    await this.cache.delPattern(`services:${serviceType}:*`);
    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 verification cache for an account
   */
  async invalidateVerification(accountId: string): Promise<void> {
    if (!accountId) return;
    this.metrics.invalidations++;
    await this.cache.del(this.buildVerificationKey(accountId));
  }

  /**
   * Get or fetch verification data (event-driven cache with safety TTL)
   */
  async getCachedVerification<T>(accountId: string, fetchFn: () => Promise<T>): Promise<T> {
    const key = this.buildVerificationKey(accountId);
    return this.getOrSet({
      bucket: "verification",
      key,
      ttlSeconds: this.VERIFICATION_TTL,
      fetchFn,
      options: { allowNull: true },
    });
  }

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

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

  /**
   * Get cache metrics for monitoring
   */
  getMetrics(): ServicesCacheSnapshot {
    return {
      services: { ...this.metrics.services },
      static: { ...this.metrics.static },
      volatile: { ...this.metrics.volatile },
      eligibility: { ...this.metrics.eligibility },
      verification: { ...this.metrics.verification },
      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 = {
      status: eligibility ? "eligible" : "not_requested",
      eligibility: typeof eligibility === "string" ? eligibility : null,
      requestedAt: null,
      checkedAt: null,
    };
    if (this.ELIGIBILITY_TTL === null) {
      await this.cache.set(key, payload);
    } else {
      await this.cache.set(key, payload, this.ELIGIBILITY_TTL);
    }
  }

  /**
   * Set eligibility details payload for an account.
   * Used by Salesforce Platform Events to push updates into the cache without re-querying Salesforce.
   */
  async setEligibilityDetails(accountId: string, payload: unknown): Promise<void> {
    const key = this.buildEligibilityKey("", accountId);
    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>(params: GetOrSetParams<T>): Promise<T> {
    const { bucket, key, ttlSeconds, fetchFn, options } = params;
    const allowNull = options?.allowNull ?? false;

    // Check Redis cache first
    const cached = await this.cache.get<T | LegacyCatalogCachePayload<T>>(key);

    if (cached !== null) {
      const normalized = await this.normalizeCachedValue(key, cached, ttlSeconds);
      this.metrics[bucket].hits++;
      return normalized;
    }

    // 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, ttlSeconds);
          await this.linkDependencies(key, dependencies, ttlSeconds);
        }

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

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

  private async normalizeCachedValue<T>(
    key: string,
    cached: T | LegacyCatalogCachePayload<T>,
    ttlSeconds: number | null
  ): Promise<T> {
    if (this.isLegacyCatalogCachePayload<T>(cached)) {
      const ttlArg = ttlSeconds === null ? undefined : ttlSeconds;
      const normalizedValue = cached.value;

      if (ttlArg === undefined) {
        await this.cache.set(key, normalizedValue);
      } else {
        await this.cache.set(key, normalizedValue, ttlArg);
      }

      if (cached.dependencies) {
        await this.storeDependencies(key, cached.dependencies, ttlSeconds);
        await this.linkDependencies(key, cached.dependencies, ttlSeconds);
      }

      return normalizedValue;
    }

    return cached;
  }

  private isLegacyCatalogCachePayload<T>(
    payload: unknown
  ): payload is LegacyCatalogCachePayload<T> {
    if (!payload || typeof payload !== "object") {
      return false;
    }

    const record = payload as Record<string, unknown>;
    return (
      record["__catalogCache"] === true && Object.prototype.hasOwnProperty.call(record, "value")
    );
  }

  /**
   * 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 = [...new Set((productIds ?? []).filter(Boolean))];
    if (uniqueIds.length === 0) {
      return false;
    }

    // Fetch all product dependency indexes in parallel
    const indexResults = await Promise.all(
      uniqueIds.map(async productId => {
        const indexKey = this.buildProductDependencyKey(productId);
        const index = await this.cache.get<{ keys?: string[] }>(indexKey);
        return index?.keys ?? [];
      })
    );

    const keysToInvalidate = new Set<string>();
    for (const keys of indexResults) {
      for (const k of keys) keysToInvalidate.add(k);
    }

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

    // Invalidate all keys in parallel
    await Promise.all(
      [...keysToInvalidate].map(async key => {
        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,
    ttlSeconds: number | null
  ): Promise<void> {
    const normalized = this.normalizeDependencies(dependencies);
    if (normalized) {
      const metaKey = this.buildDependencyMetaKey(key);
      if (ttlSeconds === null) {
        await this.cache.set(metaKey, normalized);
      } else {
        await this.cache.set(metaKey, normalized, ttlSeconds);
      }
    }
  }

  /**
   * 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: [...new Set(productIds)] };
  }

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

    await Promise.all(
      normalized.productIds.map(async productId => {
        const indexKey = this.buildProductDependencyKey(productId);
        const cached = await this.cache.get<{ keys?: string[] }>(indexKey);
        const existing = cached?.keys ?? [];
        if (!existing.includes(key)) {
          existing.push(key);
        }
        if (ttlSeconds === null) {
          await this.cache.set(indexKey, { keys: existing });
        } else {
          await this.cache.set(indexKey, { keys: existing }, ttlSeconds);
        }
      })
    );
  }

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

    await Promise.all(
      normalized.productIds.map(async productId => {
        const indexKey = this.buildProductDependencyKey(productId);
        const existing = await this.cache.get<{ keys?: string[] }>(indexKey);
        if (!existing?.keys?.length) {
          return;
        }
        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 `services:deps:product:${productId}`;
  }

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

// (intentionally no duplicate options type; use ServicesCacheOptions<T> above)
