import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import {
  DunningLevel,
  NotificationChannel,
  Prisma,
} from '@prisma/client';
import { PrismaService } from '../../prisma/prisma.service';
import { NotificationsService } from '../notifications/notifications.service';
import { PdfService } from '../pdf/pdf.service';
import { AuthenticatedUser } from '../../common/decorators/current-user.decorator';

/**
 * Dunning service: manages reminder rules and executes a daily cron to apply
 * the appropriate dunning action (reminder, formal notice, legal) based on
 * days-past-due vs. configured rules.
 */
@Injectable()
export class DunningService {
  private readonly logger = new Logger(DunningService.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly notifications: NotificationsService,
    private readonly pdf: PdfService,
  ) {}

  /** Seed default rules for a complex (J+5 reminder, J+15 formal, J+30 legal). */
  async ensureDefaultRules(user: AuthenticatedUser, complexId: string) {
    await this.assertComplexAccess(user, complexId);
    const defaults: { level: DunningLevel; daysAfterDue: number; template: string; penaltyRate: number }[] = [
      { level: DunningLevel.REMINDER, daysAfterDue: 5, template: 'dunning_reminder', penaltyRate: 0 },
      { level: DunningLevel.FORMAL_NOTICE, daysAfterDue: 15, template: 'dunning_formal', penaltyRate: 1 },
      { level: DunningLevel.LEGAL, daysAfterDue: 30, template: 'dunning_legal', penaltyRate: 2 },
    ];
    for (const d of defaults) {
      await this.prisma.dunningRule.upsert({
        where: { complexId_level: { complexId, level: d.level } },
        create: { complexId, ...d, penaltyRate: new Prisma.Decimal(d.penaltyRate) },
        update: {},
      });
    }
    return this.prisma.dunningRule.findMany({ where: { complexId }, orderBy: { daysAfterDue: 'asc' } });
  }

  async listRules(user: AuthenticatedUser, complexId: string) {
    await this.assertComplexAccess(user, complexId);
    return this.prisma.dunningRule.findMany({ where: { complexId }, orderBy: { daysAfterDue: 'asc' } });
  }

  async upsertRule(
    user: AuthenticatedUser,
    complexId: string,
    level: DunningLevel,
    data: { daysAfterDue?: number; template?: string; penaltyRate?: number; enabled?: boolean },
  ) {
    await this.assertComplexAccess(user, complexId);
    return this.prisma.dunningRule.upsert({
      where: { complexId_level: { complexId, level } },
      create: {
        complexId,
        level,
        daysAfterDue: data.daysAfterDue ?? 5,
        template: data.template ?? 'dunning_reminder',
        penaltyRate: data.penaltyRate != null ? new Prisma.Decimal(data.penaltyRate) : undefined,
        enabled: data.enabled ?? true,
      },
      update: {
        ...(data.daysAfterDue != null && { daysAfterDue: data.daysAfterDue }),
        ...(data.template != null && { template: data.template }),
        ...(data.penaltyRate != null && { penaltyRate: new Prisma.Decimal(data.penaltyRate) }),
        ...(data.enabled != null && { enabled: data.enabled }),
      },
    });
  }

  /** Run every day at 07:00 (server time) — can be triggered manually for tests. */
  @Cron(CronExpression.EVERY_DAY_AT_7AM, { name: 'dunning-daily' })
  async runDailyDunning() {
    this.logger.log('🔔 Running daily dunning...');
    const result = await this.executeDunningCycle(new Date());
    this.logger.log(
      `Dunning cycle complete: ${result.processed} items processed, ${result.actionsCreated} actions sent`,
    );
  }

  /**
   * Core engine — public so it can be invoked manually via admin endpoint
   * or by tests. Idempotent: DunningAction rows prevent duplicate actions
   * for the same (item, level).
   */
  async executeDunningCycle(referenceDate: Date) {
    const overdueItems = await this.prisma.fundCallItem.findMany({
      where: {
        status: { in: ['DUE', 'PARTIAL', 'OVERDUE'] },
        fundCall: { dueDate: { lt: referenceDate } },
      },
      include: {
        fundCall: { include: { complex: true } },
        lot: {
          include: {
            residents: {
              where: { endDate: null },
              include: { user: true },
            },
          },
        },
      },
    });

    let actionsCreated = 0;
    for (const item of overdueItems) {
      const daysOverdue = Math.floor(
        (referenceDate.getTime() - item.fundCall.dueDate.getTime()) / 86400000,
      );
      const rules = await this.prisma.dunningRule.findMany({
        where: {
          complexId: item.fundCall.complexId,
          enabled: true,
          daysAfterDue: { lte: daysOverdue },
        },
        orderBy: { daysAfterDue: 'desc' },
      });
      if (!rules.length) continue;
      const applicable = rules[0]; // highest level reached

      // Idempotency check
      const existing = await this.prisma.dunningAction.findFirst({
        where: { fundCallItemId: item.id, level: applicable.level },
      });
      if (existing) continue;

      // Apply penalty
      const penalty =
        Number(applicable.penaltyRate) > 0
          ? Number(item.amount) * (Number(applicable.penaltyRate) / 100)
          : 0;

      await this.prisma.fundCallItem.update({
        where: { id: item.id },
        data: {
          status: 'OVERDUE',
          penaltyAmount: new Prisma.Decimal(Number(item.penaltyAmount) + penalty),
        },
      });

      // Notify residents (all primary residents of the lot)
      const recipients = item.lot.residents.filter((r) => r.isPrimary);
      let notificationId: string | undefined;
      for (const r of recipients) {
        const notif = await this.notifications.send({
          tenantId: item.fundCall.complex.tenantId,
          userId: r.user.id,
          channel: NotificationChannel.EMAIL,
          template: applicable.template,
          recipient: r.user.email,
          subject: `Relance ${applicable.level} — ${item.fundCall.label}`,
          body: this.renderTemplate(applicable.level, {
            lotNumber: item.lot.lotNumber,
            amount: item.amount,
            daysOverdue,
            complex: item.fundCall.complex.name,
          }),
          metadata: { fundCallItemId: item.id, level: applicable.level },
        });
        notificationId = notif.id;
      }

      await this.prisma.dunningAction.create({
        data: {
          fundCallItemId: item.id,
          level: applicable.level,
          notificationId,
        },
      });
      actionsCreated++;
    }

    return { processed: overdueItems.length, actionsCreated };
  }

  private renderTemplate(
    level: DunningLevel,
    ctx: { lotNumber: string; amount: Prisma.Decimal; daysOverdue: number; complex: string },
  ) {
    const tpl = {
      REMINDER: `Rappel amical: l'appel de fonds "${ctx.complex}" pour le lot ${ctx.lotNumber} (${ctx.amount} MAD) est en retard de ${ctx.daysOverdue} jours. Merci de régulariser.`,
      FORMAL_NOTICE: `Mise en demeure: le paiement de ${ctx.amount} MAD pour le lot ${ctx.lotNumber} est en retard de ${ctx.daysOverdue} jours. Une pénalité est appliquée.`,
      LEGAL: `Procédure contentieuse: faute de règlement, le dossier (lot ${ctx.lotNumber}, ${ctx.amount} MAD) sera transmis pour recouvrement judiciaire.`,
    } as const;
    return tpl[level];
  }

  /** Admin trigger for immediate cycle (testing / catch-up). */
  async triggerNow(_user: AuthenticatedUser) {
    return this.executeDunningCycle(new Date());
  }

  /**
   * Generate a formal-notice (mise en demeure) PDF for a fund-call item
   * that has reached FORMAL_NOTICE or LEGAL dunning level.
   */
  async generateFormalNoticePdf(user: AuthenticatedUser, fundCallItemId: string): Promise<Buffer | null> {
    await this.assertFundCallItemAccess(user, fundCallItemId);
    const action = await this.prisma.dunningAction.findFirst({
      where: { fundCallItemId, level: { in: ['FORMAL_NOTICE', 'LEGAL'] } },
      orderBy: { executedAt: 'desc' },
    });
    if (!action) return null;

    const item = await this.prisma.fundCallItem.findUnique({
      where: { id: fundCallItemId },
      include: {
        fundCall: { include: { complex: { include: { tenant: true } } } },
        lot: {
          include: {
            residents: {
              where: { endDate: null, role: 'PROPRIETAIRE' },
              include: { user: true },
            },
          },
        },
      },
    });
    if (!item) return null;

    const primary = item.lot.residents[0]?.user;
    const residentName = primary
      ? `${primary.firstName ?? ''} ${primary.lastName ?? ''}`.trim() || primary.email
      : 'Copropriétaire';

    const deadline = new Date();
    deadline.setDate(deadline.getDate() + 15);

    return this.pdf.generateFormalNotice({
      noticeNo: `FN-${fundCallItemId.slice(0, 8).toUpperCase()}`,
      issuedAt: action.executedAt,
      complexName: item.fundCall.complex.name,
      lotNumber: item.lot.lotNumber,
      residentName,
      totalDue: Number(item.amount) - Number(item.paidAmount),
      penalty: Number(item.penaltyAmount),
      currency: 'MAD',
      dueDate: deadline,
      syndicName: item.fundCall.complex.tenant.companyName,
    });
  }

  private async assertComplexAccess(user: AuthenticatedUser, complexId: string) {
    if (user.isSuperAdmin) return;
    const complex = await this.prisma.complex.findFirst({
      where: { id: complexId, tenantId: user.tenantId ?? undefined },
      select: { id: true },
    });
    if (!complex) throw new NotFoundException('Complex not found');
  }

  private async assertFundCallItemAccess(user: AuthenticatedUser, itemId: string) {
    if (user.isSuperAdmin) return;
    const item = await this.prisma.fundCallItem.findFirst({
      where: {
        id: itemId,
        fundCall: { complex: { tenantId: user.tenantId ?? undefined } },
      },
      select: { id: true },
    });
    if (!item) throw new NotFoundException('Fund call item not found');
  }
}
