import {
  BadRequestException,
  forwardRef,
  Inject,
  Injectable,
  InternalServerErrorException,
  Logger,
  NotFoundException,
} from "@nestjs/common";
import { PaymentEntity } from "./entities/payment.entity";
import { PaymentRepository } from "./payment.repository";
import { PaymentClientService } from "./payment-client.service";
import { BillingService } from "../billing/billing.service";

import * as fs from "fs";
import * as path from "path";
import * as Handlebars from "handlebars";

import { ClientAction, ProviderPaymentStatus } from "@edr/payment-providers";
import {
  PaymentService as PaymentServiceEnum,
  PaymentReferenceType,
  PaymentIntentSnapshot,
  ProviderMethod,
} from "@edr/types";
import {
  InitiateResponseDto,
  IntentStatusDto,
  PaymentPlatformDto,
} from "./payments.dto";

/** Everything the gateway needs to open an intent. Amount/currency are supplied by
 *  the caller (billing) — this service never derives them from a domain record. */
export interface InitiateIntentInput {
  /** Opaque domain reference (booking id, …). */
  referenceId: string;
  /** Invoice source that owns the intent ('booking', …) — stored on the projection. */
  source: string;
  /** Gateway reference type the intent is opened with (caller's domain decides it). */
  referenceType: PaymentReferenceType;
  /** Human-readable order ref shown on provider pages. */
  orderRef: string;
  /** Authoritative amount in minor units, computed by the caller. */
  amountMinor: number;
  currency: string;
  /** Stored on the intent projection for receipts/dashboards. */
  reason?: string;
  /** Provider/method selector. */
  method: ProviderMethod | string;
  platform?: PaymentPlatformDto;
  payerAccount?: string;
  returnUrl?: string;
  failureUrl?: string;
}

export interface InitiateIntentResult {
  intentId: string;
  response: InitiateResponseDto;
  /** True when the provider settled the charge synchronously during initiate. */
  immediateSuccess: boolean;
  providerTxnId?: string;
  paidAt?: Date;
}

const STATUS_MAP: Record<string, ProviderPaymentStatus> = {
  "action-required": ProviderPaymentStatus.REQUIRES_ACTION,
  processing: ProviderPaymentStatus.PROCESSING,
  success: ProviderPaymentStatus.SUCCEEDED,
  failed: ProviderPaymentStatus.FAILED,
  canceled: ProviderPaymentStatus.CANCELLED,
  refunded: ProviderPaymentStatus.CANCELLED,
};

const PROVIDER_TO_METHOD: Record<string, PaymentEntity["method"]> = {
  TELEBIRR: "telebirr",
  CBE_BIRR: "cbe-birr",
  EBIRR: "ebirr",
  WAAFI: "waafi",
  CARD: "card",
  DMONEY: "dmoney",
  CAC_BANK: "cac-bank",
};

/**
 * Pure payment-gateway adapter. Owns intents, provider calls and webhooks — and
 * NOTHING domain-specific: it never loads a booking, computes an amount, or
 * advances a domain record. On settlement it notifies billing directly
 * ({@link BillingService.settleByPaymentId}); billing (and through it, the domain)
 * reacts. The billing↔payment pair is a deliberate forwardRef cycle.
 */
@Injectable()
export class PaymentService {
  private readonly logger = new Logger(PaymentService.name);

  constructor(
    private readonly paymentRepo: PaymentRepository,
    private readonly paymentClient: PaymentClientService,
    @Inject(forwardRef(() => BillingService))
    private readonly billing: BillingService,
  ) { }

  async getAll(filters: {
    search?: string;
    status?: string;
    method?: string;
    page?: number;
    pageSize?: number;
  }) {
    const { search, status, method, page = 1, pageSize = 10 } = filters;
    const skip = (page - 1) * pageSize;

    const qb = this.paymentRepo.createQueryBuilder("payment");

    if (search) {
      qb.andWhere(
        "(payment.merchantOrderId ILIKE :search OR payment.refId ILIKE :search OR payment.transactionId ILIKE :search)",
        { search: `%${search}%` },
      );
    }
    if (status) {
      qb.andWhere("payment.status = :status", { status });
    }
    if (method) {
      qb.andWhere("payment.method = :method", { method });
    }

    const [items, total] = await qb
      .orderBy("payment.createdAt", "DESC")
      .skip(skip)
      .take(pageSize)
      .getManyAndCount();

    return {
      items: items.map((p) => ({
        id: p.id,
        bookingId: p.refId,
        amount: p.amount,
        currency: p.currency,
        method: p.method,
        status: p.status,
        merchantOrderId: p.merchantOrderId,
        paidAt: p.paidAt,
        createdAt: p.createdAt,
      })),
      total,
      page,
      pageSize,
    };
  }

  /** Aggregate counts across ALL payments for the dashboard summary cards. */
  async getSummary() {
    const rows = await this.paymentRepo
      .createQueryBuilder("payment")
      .select("payment.status", "status")
      .addSelect("COUNT(*)::int", "count")
      .groupBy("payment.status")
      .getRawMany<{ status: string; count: number }>();

    const byStatus: Record<string, number> = {};
    let total = 0;
    for (const row of rows) {
      byStatus[row.status] = row.count;
      total += row.count;
    }

    const paidAgg = await this.paymentRepo
      .createQueryBuilder("payment")
      .select("COALESCE(SUM(payment.amount), 0)", "sum")
      .where("payment.status = :status", { status: "success" })
      .getRawOne<{ sum: string }>();

    return {
      total,
      success: byStatus["success"] ?? 0,
      processing:
        (byStatus["processing"] ?? 0) + (byStatus["action-required"] ?? 0),
      failed: (byStatus["failed"] ?? 0) + (byStatus["canceled"] ?? 0),
      refunded: byStatus["refunded"] ?? 0,
      paidAmount: Number(paidAgg?.sum ?? 0),
    };
  }

  /**
   * Open a gateway intent for a caller-supplied amount/reference and project it
   * locally. Returns the intent id (so billing can correlate the invoice) plus
   * the client action. When the provider settles synchronously, the intent is
   * marked paid WITHOUT emitting — the caller (billing) settles inline after it
   * has stored the intent id, avoiding a settle-before-correlation race.
   */
  async initiate(input: InitiateIntentInput): Promise<InitiateIntentResult> {
    try {

       

      const snapshot = await this.paymentClient.initiate({
        service: PaymentServiceEnum.FREIGHT,
        referenceType: PaymentReferenceType.SHIPMENT,
        referenceId: input.referenceId,
        orderRef: input.orderRef,
        // amountMinor: input.amountMinor,
        amountMinor:1,
        currency: input.currency,
        provider: input.method as ProviderMethod,
        platform: input.platform,
        payerAccount: input.payerAccount,
        returnUrl:
          input.returnUrl ?? "https://edrfreight.triaplc.com/payment/success",
        failureUrl:
          input.failureUrl ?? "https://edrfreight.triaplc.com/payment/failure",
      });

      const immediateSuccess =
        snapshot.status === ProviderPaymentStatus.SUCCEEDED;
      const paidAt = snapshot.paidAt ? new Date(snapshot.paidAt) : undefined;

      const intent = await this.upsertIntent(input, snapshot);

      if (immediateSuccess) {
        // Settle the projection but DO NOT notify billing — billing settles
        // inline once it has stored intentId on the invoice (see payInvoice),
        // avoiding a settle-before-correlation race.
        await this.markIntentSucceeded(intent.id, {
          providerTxnId: snapshot.providerTxnId,
          paidAt,
          notify: false,
        });
      }

      return {
        intentId: intent.id,
        // `intent` still reflects the projection status ("processing" on immediate
        // success — settlement is applied by the caller, not shown synchronously).
        response: this.formatIntentResponse(intent),
        immediateSuccess,
        providerTxnId: snapshot.providerTxnId,
        paidAt,
      };
    } catch (err) {
      console.log(err);
      throw err;
    }
  }

  /** Create or update the local intent projection from a provider snapshot. */
  private async upsertIntent(
    input: InitiateIntentInput,
    snapshot: PaymentIntentSnapshot,
  ): Promise<PaymentEntity> {
    const existing = await this.paymentRepo.findOneBy({
      refId: input.referenceId,
    });

    const method: PaymentEntity["method"] =
      PROVIDER_TO_METHOD[snapshot.provider ?? ""] ?? "telebirr";
    const status =
      snapshot.status === ProviderPaymentStatus.SUCCEEDED
        ? "processing"
        : this.toLocalStatus(snapshot.status);

    const clientAction = (snapshot.clientAction ?? undefined) as
      | Record<string, unknown>
      | undefined;
    const data = {
      status,
      method,
      merchantOrderId:
        snapshot.merchantOrderId ?? existing?.merchantOrderId ?? "",
      transactionId: snapshot.providerTxnId ?? existing?.transactionId,
      expiresAt: snapshot.expiresAt
        ? new Date(snapshot.expiresAt)
        : existing?.expiresAt,
      failerCode: snapshot.failureCode ?? undefined,
      failureMessage: snapshot.failureMessage ?? undefined,
    };

    if (existing) {
      await this.paymentRepo.update({ id: existing.id }, {
        ...data,
        clientAction,
      } as any);
      return { ...existing, ...data, clientAction } as PaymentEntity;
    }

    return this.paymentRepo.create({
      refId: input.referenceId,
      type: input.source,
      referenceType: input.referenceType,
      amount: input.amountMinor,
      currency: input.currency as PaymentEntity["currency"],
      reason: input.reason ?? `Payment for ${input.orderRef}`,
      rawInitiation: snapshot as unknown as Record<string, unknown>,
      clientAction: clientAction ?? {},
      ...data,
    } as any);
  }

  /**
   * Reconcile an intent's status with the gateway by reference. Read-only on the
   * domain side: it syncs the local projection and, when the provider reports a
   * newly-observed success, notifies billing to settle. `referenceId` is opaque
   * (the booking id, but this service does not load it).
   */
  async getIntentByBookingId(referenceId: string): Promise<IntentStatusDto> {
    const local = await this.paymentRepo.findOneBy({ refId: referenceId });

    let snapshot: PaymentIntentSnapshot | null = null;
    try {
      snapshot = await this.paymentClient.getIntentByReference(
        (local?.referenceType as PaymentReferenceType) ??
        PaymentReferenceType.SHIPMENT,
        referenceId,
      );
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      this.logger.warn(
        `payment service lookup failed for reference ${referenceId}: ${message}; using local intent`,
      );
    }

    if (!snapshot) {
      if (!local) throw new NotFoundException("PaymentIntent not found");
      return this.formatIntentStatus(local);
    }
    if (!local) throw new NotFoundException("PaymentIntent not found");

    // Sync local projection with provider-reported status.
    const becameSuccess =
      snapshot.status === ProviderPaymentStatus.SUCCEEDED &&
      local.status !== "success";

    if (becameSuccess) {
      await this.markIntentSucceeded(local.id, {
        providerTxnId: snapshot.providerTxnId,
        paidAt: snapshot.paidAt ? new Date(snapshot.paidAt) : undefined,
        notify: true,
      });
    } else if (snapshot.status !== ProviderPaymentStatus.SUCCEEDED) {
      await this.paymentRepo.update(
        { id: local.id },
        {
          status: this.toLocalStatus(snapshot.status),
          failerCode: snapshot.failureCode ?? undefined,
          failureMessage: snapshot.failureMessage ?? undefined,
        },
      );
    }

    const refreshed = await this.paymentRepo.findOneBy({ id: local.id });
    return this.formatIntentStatus(refreshed ?? local);
  }

  /**
   * Mark a gateway intent paid and (by default) notify billing to settle the
   * linked invoice. Idempotent — no-op when already success. Pass `notify: false`
   * when the caller settles inline and will trigger settlement itself.
   */
  async markIntentSucceeded(
    intentId: string,
    opts: { providerTxnId?: string; paidAt?: Date; notify?: boolean } = {},
  ): Promise<{ alreadyFinalized: boolean }> {
    const intent = await this.paymentRepo.findOneBy({ id: intentId });
    if (!intent) throw new NotFoundException("PaymentIntent not found");
    if (intent.status === "success") return { alreadyFinalized: true };

    const paidAt = opts.paidAt ?? new Date();
    await this.paymentRepo.update(
      { id: intent.id },
      {
        status: "success",
        paidAt,
        transactionId: opts.providerTxnId ?? intent.transactionId,
      },
    );

    if (opts.notify !== false) {
      await this.billing.settleByPaymentId(
        intent.id,
        opts.providerTxnId,
        paidAt,
      );
    }

    return { alreadyFinalized: false };
  }

  async markPaymentFailed(input: {
    intentId: string;
    failureCode?: string;
    failureMessage?: string;
  }): Promise<void> {
    const intent = await this.paymentRepo.findOneBy({ id: input.intentId });
    if (!intent) throw new NotFoundException("PaymentIntent not found");
    if (intent.status === "success" || intent.status === "canceled") return;

    await this.paymentRepo.update(
      { id: intent.id },
      {
        status: "failed",
        failerCode: input.failureCode,
        failureMessage: input.failureMessage,
      },
    );

    // Invoice stays open for retry — nothing to settle. Logged only.
    this.logger.warn(
      `Payment ${intent.id} failed for ${intent.refId}` +
      (input.failureMessage ? `: ${input.failureMessage}` : ""),
    );
  }

  async getActivePaymentByOrderIdAndMethod(
    orderId: string,
    method: PaymentEntity["method"],
  ): Promise<PaymentEntity | null> {
    return this.paymentRepo.getActivePaymentByOrderIdAndMethod(orderId, method);
  }

  async genReceiptHtml(orderId: string) {
    const payment = await this.paymentRepo.findOneBy({
      merchantOrderId: orderId,
      status: "success",
    });
    if (!payment)
      throw new BadRequestException(
        "No successful payment found for this order",
      );

    const filePath = path.join(__dirname, "templates", "receipt.hbs");
    if (!fs.existsSync(filePath)) throw new InternalServerErrorException();

    const source = fs.readFileSync(filePath, "utf8");
    const template = Handlebars.compile(source);
    return template({
      vendorName: "Ethio Djibouti Railway Freight Booking",
      vendorAddress: "Addis Ababa",
      receiptDate: payment.paidAt,
      paymentMethod: payment.method,
      subtotal: payment.amount.toString(),
      total: payment.amount.toString(),
      currency: payment.currency,
      reason: payment.reason,
    });
  }

  findBookingById(id: string) {
    return this.paymentRepo.findOneBy({ refId: id });
  }

  formatIntentResponse(intent: PaymentEntity): InitiateResponseDto {
    const clientAction =
      intent.clientAction && typeof intent.clientAction === "object"
        ? (intent.clientAction as unknown as ClientAction)
        : undefined;
    return {
      intentId: intent.id,
      status: STATUS_MAP[intent.status] ?? ProviderPaymentStatus.PROCESSING,
      clientAction,
      merchantOrderId: intent.merchantOrderId ?? undefined,
    };
  }

  private formatIntentStatus(intent: PaymentEntity): IntentStatusDto {
    return {
      ...this.formatIntentResponse(intent),
      paidAt: intent.paidAt?.toISOString(),
      failureCode: intent.failerCode ?? undefined,
      failureMessage: intent.failureMessage ?? undefined,
    };
  }

  async handlePaymentEvent(event: {
    eventType: string;
    eventId: string;
    referenceId: string;
    intentId: string;
    providerTxnId?: string;
    paidAt?: string;
    failureCode?: string;
    failureMessage?: string;
  }): Promise<{
    processed: boolean;
    alreadyFinalized?: boolean;
    reason?: string;
  }> {
    this.logger.log(`Received payment event: ${JSON.stringify(event)}`);
    if (event.eventType === "payment.succeeded") {
      const intent = await this.paymentRepo.findOneBy({
        refId: event.referenceId,
      });
      if (!intent) {
        return {
          processed: false,
          reason: `No local intent for reference ${event.referenceId}`,
        };
      }
      const { alreadyFinalized } = await this.markIntentSucceeded(intent.id, {
        providerTxnId: event.providerTxnId,
        paidAt: event.paidAt ? new Date(event.paidAt) : undefined,
        notify: true,
      });
      this.logger.log(
        `Payment finalized for intent ${intent.id}, alreadyFinalized: ${alreadyFinalized}`,
      );

      // The payment service stays domain-agnostic: it settles the intent and
      // lets billing settle the invoice (markIntentSucceeded → settleByPaymentId),
      // which emits `${source}.invoice.paid`. Per-source advances (booking → PAID,
      // warehouse → release, …) live in the domain services that listen for it.
      return { processed: true, alreadyFinalized };
    }

    if (event.eventType === "payment.failed") {
      const intent = await this.paymentRepo.findOneBy({
        refId: event.referenceId,
      });
      if (!intent) {
        return {
          processed: false,
          reason: `No local intent for reference ${event.referenceId}`,
        };
      }
      await this.markPaymentFailed({
        intentId: intent.id,
        failureCode: event.failureCode,
        failureMessage: event.failureMessage,
      });
      return { processed: true };
    }

    return {
      processed: false,
      reason: `Unknown event type: ${event.eventType}`,
    };
  }

  private toLocalStatus(
    status: ProviderPaymentStatus,
  ): PaymentEntity["status"] {
    switch (status) {
      case ProviderPaymentStatus.SUCCEEDED:
        return "success";
      case ProviderPaymentStatus.FAILED:
        return "failed";
      case ProviderPaymentStatus.CANCELLED:
        return "canceled";
      case ProviderPaymentStatus.PROCESSING:
        return "processing";
      default:
        return "action-required";
    }
  }

  async findByCompanyId(companyId: string) {
    return this.paymentRepo.findByCompanyId(companyId);
  }
}
