import {
  BadRequestException,
  Injectable,
  Logger,
  NotFoundException,
  ServiceUnavailableException,
} from '@nestjs/common';
import { PaymentMethod, Prisma } from '@prisma/client';
import { PrismaService } from '../../../prisma/prisma.service';
import { RecordPaymentDto } from '../dto/record-payment.dto';
import { StubPaymentProvider, YouCanPayProvider } from '../providers/payment-provider.interface';
import { PdfService } from '../../pdf/pdf.service';
import { AuthenticatedUser } from '../../../common/decorators/current-user.decorator';

@Injectable()
export class PaymentsService {
  private readonly logger = new Logger(PaymentsService.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly stub: StubPaymentProvider,
    private readonly youcan: YouCanPayProvider,
    private readonly pdf: PdfService,
  ) {}

  async record(fundCallItemId: string, dto: RecordPaymentDto, user: AuthenticatedUser) {
    await this.getAccessibleItem(fundCallItemId, user, true);
    this.assertOfflineMethodEnabled(dto.method);
    this.assertPositiveAmount(dto.amount);

    return this.prisma.$transaction(async (tx) => {
      await this.assertPayableAmount(tx, fundCallItemId, dto.amount);
      const payment = await tx.payment.create({
        data: {
          fundCallItemId,
          amount: new Prisma.Decimal(dto.amount),
          currency: dto.currency ?? 'MAD',
          method: dto.method,
          status: dto.method === 'CARD' ? 'PENDING' : 'SUCCEEDED',
          reference: dto.reference,
          providerPayload: dto.providerPayload as Prisma.InputJsonValue,
          paidAt: dto.method === 'CARD' ? null : new Date(),
        },
      });

      if (payment.status === 'SUCCEEDED') {
        await this.applyPayment(tx, fundCallItemId, dto.amount);
      }

      this.logger.log(`Payment ${payment.id} recorded (${payment.status}) for item ${fundCallItemId}`);
      return payment;
    });
  }

  /**
   * Create an online card payment intent (provider selected by currency).
   * Creates a PENDING Payment row and returns the hosted checkout URL.
   */
  async initiateOnlinePayment(
    fundCallItemId: string,
    params: { amount: number; currency?: string; providerCode?: string; successUrl?: string; failureUrl?: string },
    user: AuthenticatedUser,
  ) {
    if (process.env.ONLINE_PAYMENTS_ENABLED !== 'true') {
      throw new ServiceUnavailableException('Online payments are disabled for launch');
    }
    const item = await this.getAccessibleItem(fundCallItemId, user);
    this.assertPositiveAmount(params.amount);
    await this.assertPayableAmount(this.prisma, fundCallItemId, params.amount);

    const currency = params.currency ?? 'MAD';
    const provider = params.providerCode === 'STUB' && process.env.ENABLE_STUB_PROVIDERS === 'true'
      ? this.stub
      : this.youcan;

    const payment = await this.prisma.payment.create({
      data: {
        fundCallItemId,
        amount: new Prisma.Decimal(params.amount),
        currency,
        method: 'CARD',
        status: 'PENDING',
      },
    });

    const intent = await provider.createIntent({
      paymentId: payment.id,
      amount: params.amount,
      currency,
      description: `Appel de fonds ${item.fundCall.label} — Lot ${item.lot.lotNumber}`,
      successUrl: params.successUrl ?? `${process.env.APP_BASE_URL}/payments/${payment.id}/success`,
      failureUrl: params.failureUrl ?? `${process.env.APP_BASE_URL}/payments/${payment.id}/failure`,
      metadata: { paymentId: payment.id },
    });

    await this.prisma.payment.update({
      where: { id: payment.id },
      data: { reference: intent.providerRef, providerPayload: { provider: provider.code } as Prisma.InputJsonValue },
    });

    return { paymentId: payment.id, provider: provider.code, checkoutUrl: intent.checkoutUrl };
  }

  /**
   * Webhook handler — called by payment provider once customer completes
   * or fails the hosted checkout. Idempotent.
   */
  async handleWebhook(providerCode: string, rawBody: string, headers: Record<string, string>) {
    if (process.env.ONLINE_PAYMENTS_ENABLED !== 'true') {
      throw new ServiceUnavailableException('Online payments are disabled for launch');
    }
    const provider = providerCode === 'STUB' && process.env.ENABLE_STUB_PROVIDERS === 'true'
      ? this.stub
      : this.youcan;
    const parsed = await provider.parseWebhook(rawBody, headers);

    const payment = await this.prisma.payment.findUnique({
      where: { id: parsed.paymentId },
    });
    if (!payment) {
      this.logger.warn(`Webhook for unknown paymentId ${parsed.paymentId}`);
      return { ok: false };
    }
    if (payment.status !== 'PENDING') {
      return { ok: true, alreadyProcessed: true };
    }

    if (parsed.status === 'SUCCEEDED') {
      return this.confirmCardPayment(payment.id, parsed.providerRef);
    }

    await this.prisma.payment.update({
      where: { id: payment.id },
      data: { status: 'FAILED', reference: parsed.providerRef },
    });
    return { ok: true, status: 'FAILED' };
  }

  async confirmCardPayment(paymentId: string, reference: string, user?: AuthenticatedUser) {
    if (process.env.ONLINE_PAYMENTS_ENABLED !== 'true') {
      throw new ServiceUnavailableException('Online payments are disabled for launch');
    }
    if (user) await this.assertPaymentAccess(paymentId, user, true);
    return this.prisma.$transaction(async (tx) => {
      const changed = await tx.payment.updateMany({
        where: { id: paymentId, status: 'PENDING' },
        data: { status: 'SUCCEEDED', paidAt: new Date(), reference },
      });
      const payment = await tx.payment.findUniqueOrThrow({ where: { id: paymentId } });
      if (changed.count === 0) return payment;
      await this.applyPayment(tx, payment.fundCallItemId, Number(payment.amount));
      return payment;
    });
  }

  private async applyPayment(
    tx: Prisma.TransactionClient,
    itemId: string,
    amount: number,
  ) {
    const item = await tx.fundCallItem.findUnique({ where: { id: itemId } });
    if (!item) return;
    const newPaid = Number(item.paidAmount) + amount;
    const total = Number(item.amount);
    if (newPaid > total + 0.01) {
      throw new BadRequestException('Payment amount exceeds remaining balance');
    }
    const status =
      newPaid >= total ? 'PAID' : newPaid > 0 ? 'PARTIAL' : item.status;
    await tx.fundCallItem.update({
      where: { id: itemId },
      data: { paidAmount: new Prisma.Decimal(newPaid), status },
    });
  }

  async listForItem(itemId: string, user: AuthenticatedUser) {
    await this.getAccessibleItem(itemId, user);
    return this.prisma.payment.findMany({
      where: { fundCallItemId: itemId },
      orderBy: { createdAt: 'desc' },
    });
  }

  /** Dev-only helper used by the stub checkout endpoint to auto-confirm. */
  async confirmStubCheckout(token: string) {
    if (process.env.ENABLE_STUB_PROVIDERS !== 'true') {
      throw new BadRequestException('Stub checkout is disabled');
    }
    const payment = await this.prisma.payment.findFirst({ where: { reference: token } });
    if (!payment) return null;
    if (payment.status === 'PENDING') {
      await this.confirmCardPayment(payment.id, token);
    }
    return { paymentId: payment.id };
  }

  /** Generate a PDF receipt buffer for a SUCCEEDED payment. */
  async generateReceiptPdf(paymentId: string, user: AuthenticatedUser): Promise<Buffer | null> {
    await this.assertPaymentAccess(paymentId, user);
    const payment = await this.prisma.payment.findUnique({
      where: { id: paymentId },
      include: {
        fundCallItem: {
          include: {
            lot: true,
            fundCall: { include: { complex: true } },
          },
        },
      },
    });
    if (!payment || payment.status !== 'SUCCEEDED') return null;

    const residents = await this.prisma.resident.findMany({
      where: { lotId: payment.fundCallItem.lotId, endDate: null, role: 'PROPRIETAIRE' },
      include: { user: true },
    });
    const primary = residents[0]?.user;
    const residentName = primary
      ? `${primary.firstName ?? ''} ${primary.lastName ?? ''}`.trim() || primary.email
      : 'Copropriétaire';

    return this.pdf.generateReceipt({
      receiptNo: payment.id.slice(0, 8).toUpperCase(),
      paidAt: payment.paidAt ?? new Date(),
      complexName: payment.fundCallItem.fundCall.complex.name,
      lotNumber: payment.fundCallItem.lot.lotNumber,
      residentName,
      amount: Number(payment.amount),
      currency: payment.currency,
      method: payment.method,
      reference: payment.reference ?? undefined,
      period: payment.fundCallItem.fundCall.period,
    });
  }

  private assertPositiveAmount(amount: number) {
    if (!Number.isFinite(amount) || amount <= 0) {
      throw new BadRequestException('Amount must be > 0');
    }
  }

  private assertOfflineMethodEnabled(method: PaymentMethod) {
    if (method === PaymentMethod.CARD && process.env.ONLINE_PAYMENTS_ENABLED !== 'true') {
      throw new ServiceUnavailableException('Online card payments are disabled for launch');
    }

    const allowed = (process.env.OFFLINE_PAYMENT_METHODS ?? 'BANK_TRANSFER')
      .split(',')
      .map((value) => value.trim())
      .filter(Boolean);
    if (!allowed.includes(method)) {
      throw new BadRequestException(`Payment method ${method} is not enabled`);
    }
  }

  private async assertPayableAmount(
    tx: Pick<Prisma.TransactionClient, 'fundCallItem'>,
    itemId: string,
    amount: number,
  ) {
    const item = await tx.fundCallItem.findUnique({ where: { id: itemId } });
    if (!item) throw new NotFoundException('Fund call item not found');
    const remaining = Number(item.amount) - Number(item.paidAmount);
    if (amount > remaining + 0.01) {
      throw new BadRequestException('Payment amount exceeds remaining balance');
    }
  }

  private async assertPaymentAccess(paymentId: string, user: AuthenticatedUser, managerOnly = false) {
    const payment = await this.prisma.payment.findUnique({
      where: { id: paymentId },
      select: { fundCallItemId: true },
    });
    if (!payment) throw new NotFoundException('Payment not found');
    await this.getAccessibleItem(payment.fundCallItemId, user, managerOnly);
  }

  private async getAccessibleItem(
    itemId: string,
    user: AuthenticatedUser,
    managerOnly = false,
  ) {
    const item = await this.prisma.fundCallItem.findUnique({
      where: { id: itemId },
      include: {
        fundCall: { include: { complex: true } },
        lot: {
          include: {
            residents: { where: { userId: user.userId, endDate: null } },
          },
        },
      },
    });
    if (!item) throw new NotFoundException('Fund call item not found');

    const isManager = user.isSuperAdmin || user.roles.some((r) => ['SUPERADMIN', 'SYNDIC'].includes(r.code));
    const sameTenant = user.isSuperAdmin || (!!user.tenantId && item.fundCall.complex.tenantId === user.tenantId);
    if (isManager && sameTenant) return item;
    if (managerOnly) throw new NotFoundException('Fund call item not found');

    const isResidentOfLot = item.lot.residents.length > 0;
    if (sameTenant && isResidentOfLot) return item;
    throw new NotFoundException('Fund call item not found');
  }
}
