/**
 * Salesforce Order CDC (Change Data Capture) Subscriber
 *
 * Handles real-time Order and OrderItem changes from Salesforce.
 *
 * Strategy:
 * 1. Trigger provisioning when Salesforce sets Activation_Status__c to "Activating"
 * 2. Only invalidate cache for customer-facing field changes, NOT internal system fields
 *
 * Customer-facing fields (invalidate cache):
 * - Status, TotalAmount, BillingAddress, etc.
 *
 * Internal system fields (ignore):
 * - Activation_Status__c, WHMCS_Order_ID__c, Activation_Error_*
 *
 * Why: Fulfillment flow already invalidates cache on completion.
 * CDC should only catch external changes made by admins in Salesforce UI.
 *
 * @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,
  extractChangeEventHeader,
  extractChangedFields,
} from "./shared/index.js";
import { OrdersCacheService } from "@bff/modules/orders/services/orders-cache.service.js";
import { ProvisioningQueueService } from "@bff/modules/orders/queue/provisioning.queue.js";
import { RealtimeService } from "@bff/infra/realtime/realtime.service.js";

/** Fields updated by internal fulfillment process - ignore for cache invalidation */
const INTERNAL_ORDER_FIELDS = new Set([
  "Activation_Status__c",
  "WHMCS_Order_ID__c",
  "Activation_Error_Code__c",
  "Activation_Error_Message__c",
  "Activation_Last_Attempt_At__c",
  "ActivatedDate",
]);

/** Internal OrderItem fields - ignore for cache invalidation */
const INTERNAL_ORDER_ITEM_FIELDS = new Set(["WHMCS_Service_ID__c"]);

/** Statuses that trigger provisioning */
const PROVISION_TRIGGER_STATUSES = new Set(["Approved", "Reactivate"]);

@Injectable()
export class OrderCdcSubscriber implements OnModuleInit {
  constructor(
    private readonly config: ConfigService,
    private readonly pubSubClient: PubSubClientService,
    private readonly ordersCache: OrdersCacheService,
    private readonly provisioningQueue: ProvisioningQueueService,
    private readonly realtime: RealtimeService,
    @Inject(Logger) private readonly logger: Logger
  ) {}

  async onModuleInit(): Promise<void> {
    const enabled = this.config.get("SF_EVENTS_ENABLED", "false") === "true";
    if (!enabled) {
      this.logger.debug("Order CDC disabled (SF_EVENTS_ENABLED=false)");
      return;
    }

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

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

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

    await this.pubSubClient.subscribe(
      channel,
      this.handleOrderEvent.bind(this, channel),
      "order-cdc"
    );
  }

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

    await this.pubSubClient.subscribe(
      channel,
      this.handleOrderItemEvent.bind(this, channel),
      "order-item-cdc"
    );
  }

  // ─────────────────────────────────────────────────────────────────────────────
  // Order CDC Handler
  // ─────────────────────────────────────────────────────────────────────────────

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

    const payload = extractPayload(data);
    const header = payload ? extractChangeEventHeader(payload) : undefined;
    const changedFields = extractChangedFields(payload);

    const orderId = this.extractOrderId(payload, header);
    const accountId = extractStringField(payload, ["AccountId"]);

    if (!orderId) {
      this.logger.warn("Order CDC event missing Order ID; skipping", { channel });
      return;
    }

    // Check for provisioning trigger (Activation_Status__c change)
    if (payload && changedFields.has("Activation_Status__c")) {
      await this.handleActivationStatusChange(payload, orderId);
    }

    // Cache invalidation - only for customer-facing field changes
    const hasCustomerFacingChange = this.hasCustomerFacingChanges(changedFields);

    if (!hasCustomerFacingChange) {
      this.logger.debug("Order CDC: only internal field changes; skipping cache invalidation", {
        channel,
        orderId,
        changedFields: [...changedFields],
      });
      return;
    }

    this.logger.log("Order CDC: invalidating cache for customer-facing changes", {
      channel,
      orderId,
      accountId,
      changedFields: [...changedFields],
    });

    await this.invalidateOrderCaches(orderId, accountId);
  }

  /**
   * Handle Activation_Status__c changes - trigger provisioning when status is "Activating"
   */
  private async handleActivationStatusChange(
    payload: Record<string, unknown>,
    orderId: string
  ): Promise<void> {
    const activationStatus = extractStringField(payload, ["Activation_Status__c"]);
    const status = extractStringField(payload, ["Status"]);
    const whmcsOrderId = extractStringField(payload, ["WHMCS_Order_ID__c"]);

    if (activationStatus !== "Activating") {
      this.logger.debug("Activation status not 'Activating'; skipping provisioning", {
        orderId,
        activationStatus,
      });
      return;
    }

    if (status && !PROVISION_TRIGGER_STATUSES.has(status)) {
      this.logger.debug("Order status not a provisioning trigger", {
        orderId,
        activationStatus,
        status,
      });
      return;
    }

    if (whmcsOrderId) {
      this.logger.log("Order already has WHMCS Order ID; skipping provisioning", {
        orderId,
        whmcsOrderId,
      });
      return;
    }

    this.logger.log("Enqueuing provisioning for order activation via CDC", {
      orderId,
      activationStatus,
      status,
    });

    try {
      await this.provisioningQueue.enqueue({
        sfOrderId: orderId,
        idempotencyKey: `cdc-activation-${Date.now()}-${orderId}`,
        correlationId: `cdc-order-${orderId}`,
      });

      this.logger.log("Successfully enqueued provisioning job from activation change", {
        orderId,
      });
    } catch (error) {
      this.logger.error("Failed to enqueue provisioning job from CDC", {
        orderId,
        error: error instanceof Error ? error.message : String(error),
      });
    }
  }

  // ─────────────────────────────────────────────────────────────────────────────
  // OrderItem CDC Handler
  // ─────────────────────────────────────────────────────────────────────────────

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

    const payload = extractPayload(data);
    const changedFields = extractChangedFields(payload);

    const orderId = extractStringField(payload, ["OrderId"]);
    const accountId = extractStringField(payload, ["AccountId"]);

    if (!orderId) {
      this.logger.warn("OrderItem CDC event missing OrderId; skipping", { channel });
      return;
    }

    const hasCustomerFacingChange = this.hasCustomerFacingOrderItemChanges(changedFields);

    if (!hasCustomerFacingChange) {
      this.logger.debug("OrderItem CDC: only internal field changes; skipping", {
        channel,
        orderId,
        changedFields: [...changedFields],
      });
      return;
    }

    this.logger.log("OrderItem CDC: invalidating order cache", {
      channel,
      orderId,
      changedFields: [...changedFields],
    });

    await this.invalidateOrderCaches(orderId, accountId);
  }

  // ─────────────────────────────────────────────────────────────────────────────
  // Helpers
  // ─────────────────────────────────────────────────────────────────────────────

  private extractOrderId(
    payload: Record<string, unknown> | undefined,
    header: ReturnType<typeof extractChangeEventHeader>
  ): string | undefined {
    let orderId = extractStringField(payload, ["Id", "OrderId"]);

    if (!orderId && header && Array.isArray(header.recordIds) && header.recordIds.length > 0) {
      const firstId = header.recordIds.find(
        (value): value is string => typeof value === "string" && value.trim().length > 0
      );
      if (firstId) {
        orderId = firstId.trim();
      }
    }

    return orderId;
  }

  /**
   * Check if changed fields include customer-facing fields
   * Returns false if ONLY internal/system fields changed
   */
  private hasCustomerFacingChanges(changedFields: Set<string>): boolean {
    if (changedFields.size === 0) {
      // No changed fields info = assume customer-facing change (safe default)
      return true;
    }

    const customerFacingChanges = [...changedFields].filter(
      field => !INTERNAL_ORDER_FIELDS.has(field)
    );

    return customerFacingChanges.length > 0;
  }

  private hasCustomerFacingOrderItemChanges(changedFields: Set<string>): boolean {
    if (changedFields.size === 0) {
      return true; // Safe default
    }

    const customerFacingChanges = [...changedFields].filter(
      field => !INTERNAL_ORDER_ITEM_FIELDS.has(field)
    );

    return customerFacingChanges.length > 0;
  }

  private async invalidateOrderCaches(orderId: string, accountId?: string): Promise<void> {
    try {
      await this.ordersCache.invalidateOrder(orderId);

      if (accountId) {
        await this.ordersCache.invalidateAccountOrders(accountId);
      }

      const timestamp = new Date().toISOString();

      if (accountId) {
        this.realtime.publish(`account:sf:${accountId}`, "orders.changed", { timestamp });
      }

      this.realtime.publish(`orders:sf:${orderId}`, "order.cdc.changed", { timestamp });
    } catch (error) {
      this.logger.warn("Failed to invalidate order cache from CDC event", {
        orderId,
        accountId,
        error: error instanceof Error ? error.message : String(error),
      });
    }
  }
}
