import {
  Inject,
  Injectable,
  Logger,
  OnModuleDestroy,
  OnModuleInit,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { SchedulerRegistry } from "@nestjs/schedule";
import { ProviderPaymentStatus, ProviderMethod } from "@edr/types";
import { CacBankProvider } from "@edr/payment-providers";
import {
  PAYMENT_PROVIDER_MAP,
  PaymentProviderMap,
} from "../providers/providers.module";
import { PaymentIntent } from "../intents/entities/payment-intent.entity";
import { IntentsRepository } from "../intents/intents.repository";
import { IntentsService } from "../intents/intents.service";

const SWEEP_INTERVAL_NAME = "reconciliation-sweep";

/**
 * Safety net (architecture.md §7.4): webhooks get lost, users abandon hosted pages. The sweep
 * queries the provider for stale non-terminal intents and feeds the answer through the same
 * state machine the webhooks use; intents whose provider session expired are CANCELLED.
 */
@Injectable()
export class ReconciliationService implements OnModuleInit, OnModuleDestroy {
  private readonly logger = new Logger(ReconciliationService.name);
  private readonly intervalMs: number;
  private readonly staleAfterMs: number;
  private readonly batchSize: number;
  private sweeping = false;

  constructor(
    config: ConfigService,
    private readonly intentsRepository: IntentsRepository,
    private readonly intentsService: IntentsService,
    private readonly schedulerRegistry: SchedulerRegistry,
    @Inject(PAYMENT_PROVIDER_MAP)
    private readonly providers: PaymentProviderMap,
    private readonly cacBankProvider: CacBankProvider,
  ) {
    this.intervalMs =
      config.get<number>("app.reconciliation.sweepIntervalMs") ?? 30_000;
    this.staleAfterMs =
      config.get<number>("app.reconciliation.staleAfterMs") ?? 60_000;
    this.batchSize = config.get<number>("app.reconciliation.batchSize") ?? 20;
  }

  onModuleInit(): void {
    const interval = setInterval(() => void this.sweep(), this.intervalMs);
    this.schedulerRegistry.addInterval(SWEEP_INTERVAL_NAME, interval);
  }

  onModuleDestroy(): void {
    if (this.schedulerRegistry.doesExist("interval", SWEEP_INTERVAL_NAME)) {
      this.schedulerRegistry.deleteInterval(SWEEP_INTERVAL_NAME);
    }
  }

  async sweep(): Promise<void> {
    if (this.sweeping) return;
    this.sweeping = true;
    try {
      const cutoff = new Date(Date.now() - this.staleAfterMs);
      const stale = await this.intentsRepository.findStale(
        cutoff,
        this.batchSize,
      );
      for (const intent of stale) {
        await this.reconcileIntent(intent);
      }
    } catch (err) {
      this.logger.error(
        `sweep failed: ${err instanceof Error ? err.message : String(err)}`,
      );
    } finally {
      this.sweeping = false;
    }
  }

  private async reconcileIntent(intent: PaymentIntent): Promise<void> {
    try {
      const provider = this.providers.get(intent.provider);
      if (provider) {
        const status =
          intent.provider === ProviderMethod.CAC_BANK
            ? await this.cacBankProvider.queryStatus(
                intent.merchantOrderId,
                (intent.rawInitiation as { reference?: string })?.reference,
              )
            : await provider.queryStatus(intent.merchantOrderId);
        const result = this.intentsService.fromProviderStatus(status);
        if (result.status !== intent.status || result.providerTxnId) {
          await this.intentsService.applyProviderResult(intent.id, result);
        }
        if (
          result.status === ProviderPaymentStatus.SUCCEEDED ||
          result.status === ProviderPaymentStatus.FAILED ||
          result.status === ProviderPaymentStatus.CANCELLED
        ) {
          this.logger.log(`reconciled intent ${intent.id} → ${result.status}`);
          return;
        }
      }

      // Provider still says pending (or is unknown): expire only once the session is dead.
      if (intent.expiresAt && intent.expiresAt.getTime() < Date.now()) {
        await this.intentsService.expireIntent(intent.id);
        this.logger.log(
          `expired abandoned intent ${intent.id} (${intent.merchantOrderId})`,
        );
      }
    } catch (err) {
      // Per-intent failures must not stall the sweep; the row stays stale and is retried.
      this.logger.warn(
        `reconcile failed for intent ${intent.id}: ${err instanceof Error ? err.message : String(err)}`,
      );
    }
  }
}
