/**
 * Salesforce Catalog CDC (Change Data Capture) Subscriber
 *
 * Handles real-time Product2 and PricebookEntry changes from Salesforce.
 *
 * When catalog data changes:
 * 1. Invalidate relevant Redis caches
 * 2. Send SSE to connected portal clients to refetch
 *
 * @see docs/integrations/salesforce/platform-events.md
 */

import { Injectable, Inject } from "@nestjs/common";
import type { OnModuleInit } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Logger } from "nestjs-pino";
import {
  PubSubClientService,
  isDataCallback,
  extractPayload,
  extractStringField,
  extractRecordIds,
} from "./shared/index.js";
import { ServicesCacheService } from "@bff/modules/services/application/services-cache.service.js";
import { RealtimeService } from "@bff/infra/realtime/realtime.service.js";

@Injectable()
export class CatalogCdcSubscriber implements OnModuleInit {
  constructor(
    private readonly config: ConfigService,
    private readonly pubSubClient: PubSubClientService,
    private readonly catalogCache: ServicesCacheService,
    private readonly realtime: RealtimeService,
    @Inject(Logger) private readonly logger: Logger
  ) {}

  async onModuleInit(): Promise<void> {
    const enabled = this.config.get("SF_EVENTS_ENABLED", "true") === "true";

    if (!enabled) {
      this.logger.debug("Catalog CDC disabled (SF_EVENTS_ENABLED=false)");
      return;
    }

    try {
      await this.subscribeToProductCdc();
      await this.subscribeToPricebookCdc();
    } catch (error) {
      this.logger.warn("Failed to initialize Catalog CDC subscriber", {
        error: error instanceof Error ? error.message : String(error),
      });
    }
  }

  // ─────────────────────────────────────────────────────────────────────────────
  // Subscription Setup
  // ─────────────────────────────────────────────────────────────────────────────

  private async subscribeToProductCdc(): Promise<void> {
    const channel =
      this.config.get<string>("SF_CATALOG_PRODUCT_CDC_CHANNEL")?.trim() ||
      "/data/Product2ChangeEvent";

    await this.pubSubClient.subscribe(
      channel,
      this.handleProductEvent.bind(this, channel),
      "product-cdc"
    );
  }

  private async subscribeToPricebookCdc(): Promise<void> {
    const channel =
      this.config.get<string>("SF_CATALOG_PRICEBOOKENTRY_CDC_CHANNEL")?.trim() ||
      "/data/PricebookEntryChangeEvent";

    await this.pubSubClient.subscribe(
      channel,
      this.handlePricebookEvent.bind(this, channel),
      "pricebook-cdc"
    );
  }

  // ─────────────────────────────────────────────────────────────────────────────
  // CDC Handlers
  // ─────────────────────────────────────────────────────────────────────────────

  private async handleProductEvent(
    channel: string,
    subscription: { topicName?: string },
    callbackType: string,
    data: unknown
  ): Promise<void> {
    if (!isDataCallback(callbackType)) return;

    const payload = extractPayload(data);
    const productIds = extractRecordIds(payload);

    this.logger.log("Product2 CDC event received", {
      channel,
      topicName: subscription.topicName,
      productIds,
    });

    const invalidated = await this.catalogCache.invalidateProducts(productIds);

    if (!invalidated) {
      this.logger.debug(
        "No catalog entries linked to product IDs; falling back to full invalidation",
        {
          channel,
          productIds,
        }
      );
      await this.invalidateAllServices();
      this.realtime.publish("global:services", "services.changed", {
        reason: "product.cdc.fallback_full_invalidation",
        timestamp: new Date().toISOString(),
      });
      return;
    }

    this.realtime.publish("global:services", "services.changed", {
      reason: "product.cdc",
      timestamp: new Date().toISOString(),
    });
  }

  private async handlePricebookEvent(
    channel: string,
    _subscription: { topicName?: string },
    callbackType: string,
    data: unknown
  ): Promise<void> {
    if (!isDataCallback(callbackType)) return;

    const payload = extractPayload(data);
    const pricebookId = extractStringField(payload, ["PricebookId", "Pricebook2Id"]);

    // Ignore events for non-portal pricebooks
    const portalPricebookId = this.config.get<string>("PORTAL_PRICEBOOK_ID");
    if (portalPricebookId && pricebookId && pricebookId !== portalPricebookId) {
      this.logger.debug("Ignoring pricebook event for non-portal pricebook", {
        channel,
        pricebookId,
      });
      return;
    }

    const productId = extractStringField(payload, ["Product2Id", "ProductId"]);

    this.logger.log("PricebookEntry CDC event received", {
      channel,
      pricebookId,
      productId,
    });

    const invalidated = await this.catalogCache.invalidateProducts(productId ? [productId] : []);

    if (!invalidated) {
      this.logger.debug("No catalog entries linked to pricebook product; full invalidation", {
        channel,
        pricebookId,
        productId,
      });
      await this.invalidateAllServices();
      this.realtime.publish("global:services", "services.changed", {
        reason: "pricebook.cdc.fallback_full_invalidation",
        timestamp: new Date().toISOString(),
      });
      return;
    }

    this.realtime.publish("global:services", "services.changed", {
      reason: "pricebook.cdc",
      timestamp: new Date().toISOString(),
    });
  }

  private async invalidateAllServices(): Promise<void> {
    try {
      await this.catalogCache.invalidateAllServices();
    } catch (error) {
      this.logger.warn("Failed to invalidate services caches", {
        error: error instanceof Error ? error.message : String(error),
      });
    }
  }
}
