import { Injectable, Inject, OnModuleInit, OnModuleDestroy } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Logger } from "nestjs-pino";
import PubSubApiClientPkg from "salesforce-pubsub-api-client";
import { SalesforceConnection } from "../services/salesforce-connection.service";
import { OrdersCacheService } from "@bff/modules/orders/services/orders-cache.service";
import { ProvisioningQueueService } from "@bff/modules/orders/queue/provisioning.queue";

type PubSubCallback = (
  subscription: { topicName?: string },
  callbackType: string,
  data: unknown
) => void | Promise<void>;

interface PubSubClient {
  connect(): Promise<void>;
  subscribe(topic: string, cb: PubSubCallback, numRequested?: number): Promise<void>;
  close(): Promise<void>;
}

type PubSubCtor = new (opts: {
  authType: string;
  accessToken: string;
  instanceUrl: string;
  pubSubEndpoint: string;
}) => PubSubClient;

/**
 * CDC Subscriber for Order changes
 *
 * 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 (Draft, Pending Review, Completed, Cancelled)
 * - TotalAmount
 * - BillingAddress, BillingCity, etc.
 * - Customer-visible custom fields
 *
 * INTERNAL SYSTEM FIELDS (ignore - updated by fulfillment):
 * - Activation_Status__c (Activating, Activated, Failed)
 * - WHMCS_Order_ID__c
 * - Activation_Error_Code__c
 * - Activation_Error_Message__c
 * - Activation_Last_Attempt_At__c
 * - WHMCS_Service_ID__c (on OrderItem)
 *
 * WHY: The fulfillment flow already invalidates cache when it completes.
 * CDC should only catch external changes made by admins in Salesforce UI.
 */
@Injectable()
export class OrderCdcSubscriber implements OnModuleInit, OnModuleDestroy {
  private client: PubSubClient | null = null;
  private pubSubCtor: PubSubCtor | null = null;
  private orderChannel: string | null = null;
  private orderItemChannel: string | null = null;
  private readonly numRequested: number;

  // Internal fields that are updated by fulfillment process - ignore these
  private readonly INTERNAL_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 these
  private readonly INTERNAL_ORDER_ITEM_FIELDS = new Set(["WHMCS_Service_ID__c"]);

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

  constructor(
    private readonly config: ConfigService,
    private readonly sfConnection: SalesforceConnection,
    private readonly ordersCache: OrdersCacheService,
    private readonly provisioningQueue: ProvisioningQueueService,
    @Inject(Logger) private readonly logger: Logger
  ) {
    this.numRequested = this.resolveNumRequested();
  }

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

    const orderChannel =
      this.config.get<string>("SF_ORDER_CDC_CHANNEL")?.trim() || "/data/OrderChangeEvent";
    const orderItemChannel =
      this.config.get<string>("SF_ORDER_ITEM_CDC_CHANNEL")?.trim() || "/data/OrderItemChangeEvent";

    this.logger.log("Initializing Salesforce Order CDC subscriber", {
      orderChannel,
      orderItemChannel,
    });

    try {
      const client = await this.ensureClient();

      this.orderChannel = orderChannel;
      await this.subscribeWithDiagnostics(
        client,
        orderChannel,
        this.handleOrderEvent.bind(this, orderChannel),
        "order"
      );

      this.orderItemChannel = orderItemChannel;
      await this.subscribeWithDiagnostics(
        client,
        orderItemChannel,
        this.handleOrderItemEvent.bind(this, orderItemChannel),
        "order_item"
      );
    } catch (error) {
      this.logger.warn("Failed to initialize order CDC subscriber", {
        error: error instanceof Error ? error.message : String(error),
      });
    }
  }

  async onModuleDestroy(): Promise<void> {
    if (!this.client) return;
    try {
      await this.client.close();
    } catch (error) {
      this.logger.warn("Failed to close Order CDC subscriber cleanly", {
        error: error instanceof Error ? error.message : String(error),
      });
    }
  }

  private async ensureClient(): Promise<PubSubClient> {
    if (this.client) {
      return this.client;
    }

    const ctor = this.loadPubSubCtor();

    await this.sfConnection.connect();
    const accessToken = this.sfConnection.getAccessToken();
    const instanceUrl = this.sfConnection.getInstanceUrl();

    if (!accessToken || !instanceUrl) {
      throw new Error("Salesforce access token or instance URL missing for CDC subscriber");
    }

    const pubSubEndpoint =
      this.config.get<string>("SF_PUBSUB_ENDPOINT") || "api.pubsub.salesforce.com:7443";

    const client = new ctor({
      authType: "user-supplied",
      accessToken,
      instanceUrl,
      pubSubEndpoint,
    });

    await client.connect();
    this.client = client;
    return client;
  }

  private async subscribeWithDiagnostics(
    client: PubSubClient,
    channel: string,
    handler: PubSubCallback,
    label: string
  ): Promise<void> {
    this.logger.log("Attempting Salesforce CDC subscription", {
      channel,
      label,
      numRequested: this.numRequested,
    });

    try {
      await client.subscribe(channel, handler, this.numRequested);
      this.logger.log("Successfully subscribed to Salesforce CDC channel", {
        channel,
        label,
        numRequested: this.numRequested,
      });
    } catch (error) {
      this.logger.error("Salesforce CDC subscription failed", {
        channel,
        label,
        error: error instanceof Error ? error.message : String(error),
      });
      throw error;
    }
  }

  private loadPubSubCtor(): PubSubCtor {
    if (this.pubSubCtor) {
      return this.pubSubCtor;
    }

    const maybeCtor = (PubSubApiClientPkg as unknown as PubSubCtor) ?? null;
    const maybeDefault = (PubSubApiClientPkg as { default?: PubSubCtor }).default ?? null;
    const ctor = typeof maybeCtor === "function" ? maybeCtor : maybeDefault;

    if (!ctor) {
      throw new Error("Failed to load Salesforce Pub/Sub client constructor");
    }

    this.pubSubCtor = ctor;
    return this.pubSubCtor;
  }

  private resolveNumRequested(): number {
    const raw = this.config.get<string>("SF_PUBSUB_NUM_REQUESTED") ?? "25";
    const parsed = Number.parseInt(raw, 10);
    if (!Number.isFinite(parsed) || parsed <= 0) {
      this.logger.warn("Invalid SF_PUBSUB_NUM_REQUESTED value; defaulting to 25", {
        rawValue: raw,
      });
      return 25;
    }
    return parsed;
  }

  /**
   * Handle Order CDC events
   * Only invalidate cache if customer-facing fields changed
   */
  private async handleOrderEvent(
    channel: string,
    subscription: { topicName?: string },
    callbackType: string,
    data: unknown
  ): Promise<void> {
    if (!this.isDataCallback(callbackType)) return;

    const payload = this.extractPayload(data);
    const header = payload ? this.extractChangeEventHeader(payload) : undefined;
    const entityName =
      this.extractStringField(payload, ["entityName"]) ||
      (typeof header?.entityName === "string" ? header.entityName : undefined);
    const changeType =
      this.extractStringField(payload, ["changeType"]) ||
      (typeof header?.changeType === "string" ? header.changeType : undefined);
    const changedFields = this.extractChangedFields(payload);

    // Extract Order ID
    let orderId = this.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();
      }
    }
    const accountId = this.extractStringField(payload, ["AccountId"]);

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

    // 1. CHECK FOR PROVISIONING TRIGGER (Activation status change)
    if (payload && changedFields.has("Activation_Status__c")) {
      await this.handleActivationStatusChange(payload, orderId);
    }

    // 2. CACHE INVALIDATION (existing logic)
    // Filter: Only invalidate if customer-facing fields changed
    const hasCustomerFacingChange = this.hasCustomerFacingChanges(changedFields);

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

    this.logger.log("Order CDC event received with customer-facing changes, invalidating cache", {
      channel,
      orderId,
      accountId,
      changedFields: Array.from(changedFields),
    });

    try {
      // Invalidate specific order cache
      await this.ordersCache.invalidateOrder(orderId);

      // Invalidate account's order list cache
      if (accountId) {
        await this.ordersCache.invalidateAccountOrders(accountId);
      }
    } catch (error) {
      this.logger.warn("Failed to invalidate order cache from CDC event", {
        orderId,
        accountId,
        error: error instanceof Error ? error.message : String(error),
      });
    }
  }

  /**
   * Handle Activation_Status__c changes and trigger provisioning when Salesforce moves an order to "Activating"
   */
  private async handleActivationStatusChange(
    payload: Record<string, unknown>,
    orderId: string
  ): Promise<void> {
    const activationStatus = this.extractStringField(payload, ["Activation_Status__c"]);
    const status = this.extractStringField(payload, ["Status"]);
    const whmcsOrderId = this.extractStringField(payload, ["WHMCS_Order_ID__c"]);

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

    if (status && !this.PROVISION_TRIGGER_STATUSES.has(status)) {
      this.logger.debug(
        "Activation status set to Activating but order status is 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("Order activation moved to Activating via CDC, enqueuing fulfillment", {
      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,
        activationStatus,
        status,
      });
    } catch (error) {
      this.logger.error("Failed to enqueue provisioning job from activation change", {
        orderId,
        activationStatus,
        status,
        error: error instanceof Error ? error.message : String(error),
      });
    }
  }

  /**
   * Handle OrderItem CDC events
   * Only invalidate if customer-facing fields changed
   */
  private async handleOrderItemEvent(
    channel: string,
    subscription: { topicName?: string },
    callbackType: string,
    data: unknown
  ): Promise<void> {
    if (!this.isDataCallback(callbackType)) return;

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

    const orderId = this.extractStringField(payload, ["OrderId"]);
    if (!orderId) {
      this.logger.warn("OrderItem CDC event missing OrderId; skipping", { channel });
      return;
    }

    // Filter: Only invalidate if customer-facing fields changed
    const hasCustomerFacingChange = this.hasCustomerFacingOrderItemChanges(changedFields);

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

    this.logger.log("OrderItem CDC event received, invalidating order cache", {
      channel,
      orderId,
      changedFields: Array.from(changedFields),
    });

    try {
      await this.ordersCache.invalidateOrder(orderId);
    } catch (error) {
      this.logger.warn("Failed to invalidate order cache from OrderItem CDC event", {
        orderId,
        error: error instanceof Error ? error.message : String(error),
      });
    }
  }

  /**
   * 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;
    }

    // Remove internal fields from changed fields
    const customerFacingChanges = Array.from(changedFields).filter(
      field => !this.INTERNAL_FIELDS.has(field)
    );

    return customerFacingChanges.length > 0;
  }

  /**
   * Check if changed OrderItem fields include customer-facing fields
   */
  private hasCustomerFacingOrderItemChanges(changedFields: Set<string>): boolean {
    if (changedFields.size === 0) {
      return true; // Safe default
    }

    const customerFacingChanges = Array.from(changedFields).filter(
      field => !this.INTERNAL_ORDER_ITEM_FIELDS.has(field)
    );

    return customerFacingChanges.length > 0;
  }

  /**
   * Extract changed field names from CDC payload
   * CDC payload includes changeOrigin with changedFields array
   */
  private extractChangedFields(payload: Record<string, unknown> | undefined): Set<string> {
    if (!payload) return new Set();

    const header = this.extractChangeEventHeader(payload);
    const headerChangedFields = Array.isArray(header?.changedFields)
      ? (header?.changedFields as unknown[]).filter(
          (field): field is string => typeof field === "string" && field.length > 0
        )
      : [];

    // CDC provides changed fields in different formats depending on API version
    // Try to extract from common locations
    const changedFieldsArray =
      (payload.changedFields as string[] | undefined) ||
      (payload.changeOrigin as { changedFields?: string[] })?.changedFields ||
      [];

    return new Set([
      ...headerChangedFields,
      ...changedFieldsArray.filter(field => typeof field === "string" && field.length > 0),
    ]);
  }

  private isDataCallback(callbackType: string): boolean {
    const normalized = String(callbackType || "").toLowerCase();
    return normalized === "data" || normalized === "event";
  }

  private extractPayload(data: unknown): Record<string, unknown> | undefined {
    if (!data || typeof data !== "object") {
      return undefined;
    }

    const candidate = data as { payload?: unknown };
    if (candidate.payload && typeof candidate.payload === "object") {
      return candidate.payload as Record<string, unknown>;
    }

    return data as Record<string, unknown>;
  }

  private extractStringField(
    payload: Record<string, unknown> | undefined,
    fieldNames: string[]
  ): string | undefined {
    if (!payload) return undefined;
    for (const field of fieldNames) {
      const value = payload[field];
      if (typeof value === "string") {
        const trimmed = value.trim();
        if (trimmed.length > 0) {
          return trimmed;
        }
      }
    }
    return undefined;
  }

  private extractChangeEventHeader(payload: Record<string, unknown>):
    | {
        changedFields?: unknown;
        recordIds?: unknown;
        entityName?: unknown;
        changeType?: unknown;
      }
    | undefined {
    const header = payload["ChangeEventHeader"];
    if (header && typeof header === "object") {
      return header as {
        changedFields?: unknown;
        recordIds?: unknown;
        entityName?: unknown;
        changeType?: unknown;
      };
    }
    return undefined;
  }
}
