import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { CheckoutSession, CheckoutSessionStatus, PaymentIntent, Prisma } from "@prisma/client";
import { newId, newPublicToken } from "../common/ids/id.util";
import { PaymentIntentService } from "../payment-intents/payment-intent.service";
import { PrismaService } from "../prisma/prisma.service";
import type { ConfirmPublicCheckoutSessionDto } from "./dto/confirm-public-checkout-session.dto";
import type { CreateCheckoutSessionDto } from "./dto/create-checkout-session.dto";

export const PUBLIC_MAX_FAILED_ATTEMPTS = 5;

@Injectable()
export class CheckoutSessionService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly config: ConfigService,
    private readonly paymentIntents: PaymentIntentService,
  ) {}

  async create(merchantId: string, dto: CreateCheckoutSessionDto) {
    const intent = await this.prisma.paymentIntent.findFirst({
      where: { id: dto.intentId, merchantId },
    });
    if (!intent) throw new BadRequestException("intentId does not belong to this merchant");

    const existing = await this.prisma.checkoutSession.findUnique({
      where: { intentId: intent.id },
    });
    if (existing) {
      return this.toCreatedDto(existing);
    }

    const defaultHours = Number(this.config.get("CHECKOUT_SESSION_DEFAULT_EXPIRATION_HOURS") ?? 24);
    const hours = dto.expirationHours ?? defaultHours;
    const expiresAt = new Date(Date.now() + hours * 3_600_000);

    const publicToken = newPublicToken();
    const clientSecretRaw = newPublicToken();

    const session = await this.prisma.checkoutSession.create({
      data: {
        id: newId("checkoutSession"),
        intentId: intent.id,
        publicToken,
        clientSecret: `cs_secret_${clientSecretRaw}`,
        status: CheckoutSessionStatus.open,
        uiConfigJson: (dto.uiConfig as Prisma.InputJsonValue) ?? Prisma.JsonNull,
        returnUrl: dto.returnUrl ?? null,
        cancelUrl: dto.cancelUrl ?? null,
        expiresAt,
      },
    });

    return this.toCreatedDto(session);
  }

  async findOne(merchantId: string, id: string) {
    const session = await this.prisma.checkoutSession.findUnique({
      where: { id },
      include: { intent: { select: { merchantId: true } } },
    });
    if (!session || session.intent.merchantId !== merchantId) {
      throw new NotFoundException("Checkout session not found");
    }
    return this.toDto(session);
  }

  /** Acceso público por publicToken (hosted checkout). Devuelve info segura para mostrar al comprador. */
  async retrieveByPublicToken(publicToken: string) {
    const session = await this.loadPublicSessionOrThrow(publicToken);
    const c = session.intent.customer;
    return {
      id: session.id,
      status: session.status,
      expiresAt: session.expiresAt,
      amount: session.intent.amount.toString(),
      currency: session.intent.currency,
      country: session.intent.country,
      description: session.intent.description,
      merchantName: session.intent.merchant.name,
      uiConfig: session.uiConfigJson as Record<string, unknown> | null,
      returnUrl: session.returnUrl,
      cancelUrl: session.cancelUrl,
      // Se devuelven sólo campos seguros para mostrar al comprador. El hosted
      // checkout los usa para prellenar el form cuando el merchant ya creó
      // un Customer al iniciar el cobro.
      customer: c
        ? {
            email: c.email,
            name: c.name,
            docNumber: c.docNumber,
            phone: c.phone,
            country: c.country,
          }
        : null,
    };
  }

  async confirmByPublicToken(publicToken: string, dto: ConfirmPublicCheckoutSessionDto) {
    const session = await this.loadPublicSessionOrThrow(publicToken);

    if (session.status !== CheckoutSessionStatus.open) {
      throw new NotFoundException("Checkout session not found");
    }

    if (dto.customer) {
      const customerData = dto.customer;
      if (session.intent.customerId) {
        await this.prisma.customer.update({
          where: { id: session.intent.customerId },
          data: this.customerPatch(customerData),
        });
      } else {
        const created = await this.prisma.customer.create({
          data: {
            id: newId("customer"),
            merchantId: session.intent.merchantId,
            ...this.customerPatch(customerData),
          },
        });
        await this.prisma.paymentIntent.update({
          where: { id: session.intent.id },
          data: { customerId: created.id },
        });
      }
    }

    try {
      const result = await this.paymentIntents.confirm(
        session.intent.merchantId,
        session.intent.id,
        {
          returnUrl: session.returnUrl ?? undefined,
          cancelUrl: session.cancelUrl ?? undefined,
          platformId: dto.platformId,
        },
      );

      if (result.intent.status === "approved" || result.intent.status === "pending") {
        // No tocamos failedAttempts; en approved opcionalmente marcamos completed.
        if (result.intent.status === "approved") {
          await this.prisma.checkoutSession.update({
            where: { id: session.id },
            data: { status: CheckoutSessionStatus.completed },
          });
        }
      } else {
        await this.bumpFailed(session.id);
      }

      return {
        status: result.intent.status,
        redirectUrl: result.attempt.redirectUrl ?? null,
        qr: result.attempt.qr ?? null,
        returnUrl: session.returnUrl,
        cancelUrl: session.cancelUrl,
      };
    } catch (err) {
      await this.bumpFailed(session.id);
      throw err;
    }
  }

  /**
   * Lista los medios de pago que el comprador puede elegir para esta sesión, según
   * el provider que rutearía el intent. Vacío ⇒ el provider no expone selección
   * inline (ej. dLocal, que elige en su propio checkout). Consumido por el hosted
   * checkout para renderizar el selector de método.
   */
  async listPaymentMethodsByPublicToken(publicToken: string) {
    const session = await this.loadPublicSessionOrThrow(publicToken);
    const intent = session.intent;
    // Mismo criterio de selección que ProviderRouterService.route (sin construir
    // el adapter ni desencriptar credenciales).
    const config = await this.prisma.merchantProviderConfig.findFirst({
      where: {
        merchantId: intent.merchantId,
        country: intent.country,
        active: true,
        ...(intent.requestedProvider ? { provider: intent.requestedProvider } : {}),
      },
      orderBy: [{ priority: "desc" }, { createdAt: "desc" }],
      select: { provider: true },
    });
    if (!config) return [];

    const methods = await this.prisma.providerPaymentMethod.findMany({
      where: { provider: config.provider, country: intent.country, active: true },
      orderBy: { name: "asc" },
      select: { externalId: true, name: true, method: true },
    });
    return methods.map((m) => ({ platformId: m.externalId, name: m.name, method: m.method }));
  }

  private async loadPublicSessionOrThrow(publicToken: string) {
    const session = await this.prisma.checkoutSession.findUnique({
      where: { publicToken },
      include: {
        intent: {
          include: {
            merchant: { select: { name: true, status: true } },
            // Necesario para que retrieveByPublicToken pueda exponer los datos
            // del comprador y el hosted checkout prellene el form.
            customer: true,
          },
        },
      },
    });
    if (!session) throw new NotFoundException("Checkout session not found");
    if (session.intent.merchant.status !== "active") {
      throw new NotFoundException("Checkout session not found");
    }
    if (session.failedAttempts >= PUBLIC_MAX_FAILED_ATTEMPTS) {
      throw new NotFoundException("Checkout session not found");
    }

    const refreshed = await this.markExpiredIfDue(session);
    return { ...session, status: refreshed.status, expiresAt: refreshed.expiresAt };
  }

  private async bumpFailed(sessionId: string) {
    await this.prisma.checkoutSession.update({
      where: { id: sessionId },
      data: { failedAttempts: { increment: 1 } },
    });
  }

  private customerPatch(c: NonNullable<ConfirmPublicCheckoutSessionDto["customer"]>) {
    return {
      ...(c.email !== undefined ? { email: c.email } : {}),
      ...(c.name !== undefined ? { name: c.name } : {}),
      ...(c.docNumber !== undefined ? { docNumber: c.docNumber } : {}),
      ...(c.phone !== undefined ? { phone: c.phone } : {}),
      ...(c.country !== undefined ? { country: c.country } : {}),
    };
  }

  private async markExpiredIfDue(session: CheckoutSession): Promise<CheckoutSession> {
    if (session.status === CheckoutSessionStatus.open && session.expiresAt.getTime() < Date.now()) {
      return this.prisma.checkoutSession.update({
        where: { id: session.id },
        data: { status: CheckoutSessionStatus.expired },
      });
    }
    return session;
  }

  private buildUrl(publicToken: string): string {
    const base = this.config.get<string>("CHECKOUT_BASE_URL") ?? "http://localhost:5176";
    return `${base.replace(/\/$/, "")}/c/${publicToken}`;
  }

  private toDto(session: CheckoutSession & { intent?: Pick<PaymentIntent, "merchantId"> }) {
    return {
      id: session.id,
      intentId: session.intentId,
      publicToken: session.publicToken,
      url: this.buildUrl(session.publicToken),
      status: session.status,
      returnUrl: session.returnUrl,
      cancelUrl: session.cancelUrl,
      uiConfig: session.uiConfigJson as Record<string, unknown> | null,
      failedAttempts: session.failedAttempts,
      expiresAt: session.expiresAt,
      createdAt: session.createdAt,
    };
  }

  private toCreatedDto(session: CheckoutSession) {
    return { ...this.toDto(session), clientSecret: session.clientSecret };
  }
}
