// Dual Ledger Outbox Worker
// Processes SCB synchronization jobs from outbox with retry/backoff and idempotency

import { PrismaClient, Prisma } from '@prisma/client';
import { logger } from '@/infrastructure/monitoring/logger';
import { ScbLedgerClient, createScbLedgerClient } from '@/core/settlement/scb/scb-ledger-client';

type OutboxRow = {
  id: string;
  outbox_id: string;
  sovereign_bank_id: string;
  ledger_id: string;
  reference_id: string;
  payload: any;
  status: string;
  attempts: number;
};

export interface DualLedgerOutboxWorkerOptions {
  batchSize?: number;
  maxAttempts?: number;
  retryDelayMs?: number;
  maxRetryDelayMs?: number;
}

export class DualLedgerOutboxWorker {
  constructor(
    private prisma: PrismaClient,
    private opts: DualLedgerOutboxWorkerOptions = {}
  ) {
    this.opts = {
      batchSize: opts.batchSize ?? 25,
      maxAttempts: opts.maxAttempts ?? 25,
      retryDelayMs: opts.retryDelayMs ?? 1000,
      maxRetryDelayMs: opts.maxRetryDelayMs ?? 300000, // 5 minutes
    };
  }

  /**
   * Process one batch of outbox jobs
   * Returns number of jobs processed
   */
  async runOnce(): Promise<number> {
    // Claim a batch using FOR UPDATE SKIP LOCKED (prevents multi-worker duplication)
    const rows = await this.prisma.$queryRaw<OutboxRow[]>`
      WITH cte AS (
        SELECT id
        FROM dual_ledger_outbox
        WHERE status IN ('QUEUED','FAILED')
          AND attempts < ${this.opts.maxAttempts}
        ORDER BY created_at ASC
        LIMIT ${this.opts.batchSize}
        FOR UPDATE SKIP LOCKED
      )
      UPDATE dual_ledger_outbox o
      SET status = 'SENT',
          attempts = attempts + 1,
          last_attempt_at = now(),
          last_error = NULL
      FROM cte
      WHERE o.id = cte.id
      RETURNING
        o.id, o.outbox_id, o.sovereign_bank_id, o.ledger_id, o.reference_id,
        o.payload, o.status, o.attempts;
    `;

    logger.info('DualLedgerOutboxWorker: Claimed batch', {
      count: rows.length,
      batchSize: this.opts.batchSize,
    });

    // Process each job
    for (const job of rows) {
      await this.processJob(job).catch((error) => {
        // processJob records failure; log but don't throw to keep loop moving
        logger.error('DualLedgerOutboxWorker: Job processing failed', {
          outboxId: job.outbox_id,
          error: error instanceof Error ? error.message : 'Unknown error',
        });
      });
    }

    return rows.length;
  }

  /**
   * Process a single outbox job
   */
  private async processJob(job: OutboxRow): Promise<void> {
    logger.info('DualLedgerOutboxWorker: Processing job', {
      outboxId: job.outbox_id,
      sovereignBankId: job.sovereign_bank_id,
      referenceId: job.reference_id,
      attempts: job.attempts,
    });

    try {
      // Call SCB ledger API (must be idempotent on reference_id)
      const scb = await this.callScbLedgerApi(
        job.sovereign_bank_id,
        job.ledger_id,
        job.payload,
        job.reference_id
      );

      // Record ACK
      await this.prisma.dual_ledger_outbox.update({
        where: { outboxId: job.outbox_id },
        data: {
          status: 'ACKED',
          scbTransactionId: scb.transactionId,
          scbLedgerHash: scb.ledgerHash,
          scbSignature: scb.signature,
          ackedAt: new Date(),
        },
      });

      logger.info('DualLedgerOutboxWorker: SCB sync ACKed', {
        outboxId: job.outbox_id,
        scbTransactionId: scb.transactionId,
        scbLedgerHash: scb.ledgerHash,
      });

      // Update master ledger tracking (by dbisLedgerHash carried in payload)
      const dbisLedgerHash = job.payload?.dbisLedgerHash;
      if (dbisLedgerHash) {
        await this.prisma.gss_master_ledger.updateMany({
          where: { dbisLedgerHash },
          data: {
            sovereignLedgerHash: scb.ledgerHash,
            dualLedgerCommit: true,
            status: 'SETTLED',
            settledAt: new Date(),
          },
        });

        logger.info('DualLedgerOutboxWorker: Master ledger updated', {
          dbisLedgerHash,
          sovereignLedgerHash: scb.ledgerHash,
        });
      }

      // FINALIZE
      await this.prisma.dual_ledger_outbox.update({
        where: { outboxId: job.outbox_id },
        data: {
          status: 'FINALIZED',
          finalizedAt: new Date(),
        },
      });

      logger.info('DualLedgerOutboxWorker: Job finalized', {
        outboxId: job.outbox_id,
      });
    } catch (err: any) {
      const msg =
        err?.message?.slice(0, 4000) ?? 'Unknown error (no message)';

      // Record failure (status=FAILED allows retry)
      await this.prisma.dual_ledger_outbox.update({
        where: { outboxId: job.outbox_id },
        data: {
          status: 'FAILED',
          lastError: msg,
          lastAttemptAt: new Date(),
        },
      });

      logger.error('DualLedgerOutboxWorker: Job failed', {
        outboxId: job.outbox_id,
        attempts: job.attempts,
        error: msg,
      });

      throw err;
    }
  }

  /**
   * Call SCB ledger API (idempotent)
   * Uses ScbLedgerClient for idempotent API calls
   */
  private async callScbLedgerApi(
    sovereignBankId: string,
    ledgerId: string,
    payload: any,
    idempotencyKey: string
  ): Promise<{ transactionId: string; ledgerHash: string; signature: string }> {
    // Create SCB client for this sovereign bank
    const scbClient = createScbLedgerClient(sovereignBankId);

    // Extract account IDs from payload (assuming payload structure from outbox)
    const sourceAccountId = payload.sourceAccountId || payload.sourceBankId;
    const destinationAccountId = payload.destinationAccountId || payload.destinationBankId;

    // Call SCB API via client (handles idempotency, retries, etc.)
    const response = await scbClient.postEntry({
      sovereignBankId,
      ledgerId,
      sourceAccountId,
      destinationAccountId,
      amount: payload.amount,
      currencyCode: payload.currencyCode,
      assetType: payload.assetType,
      referenceId: idempotencyKey,
      metadata: payload.metadata,
    });

    return {
      transactionId: response.transactionId,
      ledgerHash: response.ledgerHash,
      signature: response.signature,
    };
  }

  /**
   * Get exponential backoff delay for retry
   */
  private getRetryDelay(attempts: number): number {
    const delay = Math.min(
      this.opts.retryDelayMs! * Math.pow(2, attempts - 1),
      this.opts.maxRetryDelayMs!
    );
    return delay;
  }
}
