import { BadRequestException, Injectable, Logger, NotFoundException } from '@nestjs/common';
import { presupuesto_estado } from '@prisma/client';
import { randomUUID } from 'crypto';
import { envs } from 'src/config/envs';
import { MailService } from 'src/mail/mail.service';
import { PrismaService } from 'src/prisma/prisma.service';
import { SendPresupuestoEmailDto } from './dto/send-email.dto';
import { PresupuestosPdfService } from './presupuestos-pdf.service';

/** Estados desde los que el cliente puede responder en el portal. */
const ESTADOS_RESPONDIBLES: presupuesto_estado[] = [
  presupuesto_estado.SENT,
  presupuesto_estado.VIEWED,
];

/** Estados internos previos al envío: al mandar el email pasan a SENT. */
const ESTADOS_PRE_ENVIO: presupuesto_estado[] = [
  presupuesto_estado.DRAFT,
  presupuesto_estado.IN_REVIEW,
  presupuesto_estado.APPROVED,
];
const PRESUPUESTO_ESTADO_FACTURADO = 'FACTURADO' as unknown as presupuesto_estado;

@Injectable()
export class PresupuestosPublicoService {
  private readonly logger = new Logger(PresupuestosPublicoService.name);
  private readonly baseUrl = envs.pagoLinkBaseUrl ?? 'https://app.novasispy.com';

  constructor(
    private readonly prisma: PrismaService,
    private readonly mail: MailService,
    private readonly pdfService: PresupuestosPdfService,
  ) {}

  // ── URLs públicas ───────────────────────────────────────────────────────────

  private portalUrl(token: string): string {
    return `${this.baseUrl}/pub/presupuesto/${token}`;
  }

  private pixelUrl(token: string): string {
    return `${this.baseUrl}/api/v1/pub/presupuesto/track/open/${token}`;
  }

  // ── Formato ─────────────────────────────────────────────────────────────────

  private fmtMonto(valor: number, decimales: number, simbolo?: string | null): string {
    const num = new Intl.NumberFormat('es-PY', {
      minimumFractionDigits: decimales,
      maximumFractionDigits: decimales,
    }).format(valor);
    return simbolo ? `${simbolo} ${num}` : num;
  }

  /**
   * Decimales de visualización de un monto según la moneda. PYG no usa
   * decimales; el resto usa su `decimales_estandar` (default 2). El campo legacy
   * `moneda.decimales` no es confiable (ej. PYG lo tiene en 8), por eso no se usa.
   */
  private decimalesMoneda(
    moneda?: { codigo?: string | null; decimales_estandar?: number | null } | null,
  ): number {
    if (!moneda || (moneda.codigo ?? 'PYG') === 'PYG') return 0;
    return moneda.decimales_estandar ?? 2;
  }

  private fmtFecha(fecha: Date | null | undefined): string {
    if (!fecha) return '—';
    return new Intl.DateTimeFormat('es-PY', { day: '2-digit', month: '2-digit', year: 'numeric' }).format(fecha);
  }

  // ── Envío de email (autenticado) ──────────────────────────────────────────────

  /** Envía el presupuesto por email al cliente con el PDF adjunto y lo deja en estado SENT. */
  async sendEmail(id: string, dto: SendPresupuestoEmailDto, empresa_id: string, user_id?: string) {
    const presupuesto = await this.prisma.presupuesto_cab.findFirst({
      where: { id, empresa_id, deleted: false },
      include: {
        clientes: { include: { personas: { select: { razon_social: true, email: true } } } },
        moneda: { select: { codigo: true, simbolo: true, decimales_estandar: true } },
      },
    });
    if (!presupuesto) throw new NotFoundException('Presupuesto no encontrado');
    if (
      presupuesto.estado === presupuesto_estado.CONVERTED ||
      presupuesto.estado === PRESUPUESTO_ESTADO_FACTURADO ||
      presupuesto.estado === presupuesto_estado.CANCELLED
    ) {
      throw new BadRequestException(`No se puede enviar un presupuesto en estado ${presupuesto.estado}`);
    }

    // Destinatarios: los del DTO, o el email del cliente como fallback
    const destinatarios = dto.destinatarios?.length
      ? dto.destinatarios
      : presupuesto.clientes?.personas?.email
        ? [presupuesto.clientes.personas.email]
        : [];
    if (!destinatarios.length) {
      throw new BadRequestException(
        'El cliente no tiene email principal configurado. Cargá un destinatario principal para poder enviar.',
      );
    }

    const [empresa, config] = await Promise.all([
      this.prisma.empresas.findUnique({
        where: { id: empresa_id },
        select: { razon_social: true, nombre_fantasia: true },
      }),
      this.prisma.presupuesto_config.findUnique({ where: { empresa_id } }),
    ]);

    // M41: si supera el umbral, debe estar aprobado antes de enviarse al cliente.
    const umbralAprob = config ? Number(config.monto_umbral_aprobacion) : null;
    if (
      umbralAprob != null &&
      Number(presupuesto.total) >= umbralAprob &&
      (presupuesto.estado === presupuesto_estado.DRAFT || presupuesto.estado === presupuesto_estado.IN_REVIEW)
    ) {
      throw new BadRequestException(
        presupuesto.estado === presupuesto_estado.IN_REVIEW
          ? 'Este presupuesto está pendiente de aprobación interna'
          : 'Este presupuesto requiere aprobación interna antes de enviarse al cliente',
      );
    }

    const empresaNombre = empresa?.razon_social ?? empresa?.nombre_fantasia ?? 'Novasis ERP';
    const token = presupuesto.token_acceso_publico ?? randomUUID();
    const asunto =
      dto.asunto?.trim() ||
      (config?.asunto_email_template ?? 'Presupuesto {numero} — {empresa}')
        .replace('{numero}', presupuesto.numero)
        .replace('{empresa}', empresaNombre);

    // PDF en base64 reutilizando el microservicio generador-pdf
    const pdfBuffer = await this.pdfService.generatePdf(id, empresa_id, 'base64');

    const decimales = this.decimalesMoneda(presupuesto.moneda);
    const simbolo = presupuesto.moneda?.simbolo;

    // Registrar el intento de envío antes de mandar el correo
    const log = await this.prisma.presupuesto_email.create({
      data: {
        presupuesto_id: id,
        destinatarios: destinatarios.join(', '),
        asunto,
        enviado_por: user_id ?? null,
        estado_envio: 'ENVIANDO',
      },
    });

    try {
      await this.mail.sendPresupuestoEmail({
        to: destinatarios,
        cc: dto.cc,
        replyTo: config?.email_remitente || undefined,
        asunto,
        empresaNombre,
        clienteNombre: presupuesto.clientes?.personas?.razon_social ?? presupuesto.contacto_cliente ?? 'Cliente',
        numero: presupuesto.numero,
        version: presupuesto.version,
        titulo: presupuesto.titulo,
        fechaVigencia: this.fmtFecha(presupuesto.fecha_vigencia),
        total: this.fmtMonto(Number(presupuesto.total), decimales, simbolo),
        mensaje: dto.mensaje,
        portalUrl: this.portalUrl(token),
        pixelUrl: this.pixelUrl(token),
        pdfBuffer,
        pdfFilename: `presupuesto-${presupuesto.numero}.pdf`,
      });
    } catch (error) {
      const detalle = error instanceof Error ? error.message : 'Error desconocido';
      await this.prisma.presupuesto_email.update({
        where: { id: log.id },
        data: { estado_envio: 'ERROR', error_detalle: detalle },
      });
      this.logger.error(`Error enviando presupuesto ${presupuesto.numero}: ${detalle}`);
      throw new BadRequestException(`No se pudo enviar el email: ${detalle}`);
    }

    // Marcar enviado + transición a SENT (si aún no fue respondido)
    const estadoAnterior = presupuesto.estado;
    const pasaASent = ESTADOS_PRE_ENVIO.includes(estadoAnterior);

    await this.prisma.$transaction(async (tx) => {
      await tx.presupuesto_email.update({
        where: { id: log.id },
        data: { estado_envio: 'ENVIADO', fecha_envio: new Date() },
      });
      await tx.presupuesto_cab.update({
        where: { id },
        data: {
          token_acceso_publico: token,
          fecha_envio_email: new Date(),
          ...(pasaASent ? { estado: presupuesto_estado.SENT } : {}),
          updated_at: new Date(),
        },
      });
      if (pasaASent) {
        await tx.presupuesto_historial.create({
          data: {
            presupuesto_id: id,
            estado_anterior: estadoAnterior,
            estado_nuevo: presupuesto_estado.SENT,
            usuario_id: user_id ?? null,
            comentario: `Enviado por email a ${destinatarios.join(', ')}`,
          },
        });
      }
    });

    return { message: 'Presupuesto enviado correctamente', destinatarios, portalUrl: this.portalUrl(token) };
  }

  /** Genera (o reutiliza) el enlace público para compartirlo manualmente (ej. WhatsApp). */
  async generateLink(id: string, empresa_id: string, user_id?: string) {
    const presupuesto = await this.prisma.presupuesto_cab.findFirst({
      where: { id, empresa_id, deleted: false },
      select: {
        id: true,
        numero: true,
        estado: true,
        total: true,
        token_acceso_publico: true,
      },
    });
    if (!presupuesto) throw new NotFoundException('Presupuesto no encontrado');
    if (
      presupuesto.estado === presupuesto_estado.CONVERTED ||
      presupuesto.estado === PRESUPUESTO_ESTADO_FACTURADO ||
      presupuesto.estado === presupuesto_estado.CANCELLED
    ) {
      throw new BadRequestException(`No se puede compartir un presupuesto en estado ${presupuesto.estado}`);
    }

    const config = await this.prisma.presupuesto_config.findUnique({ where: { empresa_id } });
    const umbralAprob = config ? Number(config.monto_umbral_aprobacion) : null;
    if (
      umbralAprob != null &&
      Number(presupuesto.total) >= umbralAprob &&
      (presupuesto.estado === presupuesto_estado.DRAFT || presupuesto.estado === presupuesto_estado.IN_REVIEW)
    ) {
      throw new BadRequestException(
        presupuesto.estado === presupuesto_estado.IN_REVIEW
          ? 'Este presupuesto está pendiente de aprobación interna'
          : 'Este presupuesto requiere aprobación interna antes de compartirlo con el cliente',
      );
    }

    const token = presupuesto.token_acceso_publico ?? randomUUID();
    const estadoAnterior = presupuesto.estado;
    const pasaASent = ESTADOS_PRE_ENVIO.includes(estadoAnterior);

    await this.prisma.$transaction(async (tx) => {
      await tx.presupuesto_cab.update({
        where: { id },
        data: {
          token_acceso_publico: token,
          ...(pasaASent ? { estado: presupuesto_estado.SENT } : {}),
          updated_at: new Date(),
        },
      });

      await tx.presupuesto_email.create({
        data: {
          presupuesto_id: id,
          destinatarios: 'ENLACE_COMPARTIDO',
          asunto: `Enlace de presupuesto ${presupuesto.numero}`,
          enviado_por: user_id ?? null,
          estado_envio: 'LINK',
          fecha_envio: new Date(),
        },
      });

      if (pasaASent) {
        await tx.presupuesto_historial.create({
          data: {
            presupuesto_id: id,
            estado_anterior: estadoAnterior,
            estado_nuevo: presupuesto_estado.SENT,
            usuario_id: user_id ?? null,
            comentario: 'Enlace público generado para compartir con el cliente',
          },
        });
      }
    });

    return {
      message: 'Enlace público generado correctamente',
      portalUrl: this.portalUrl(token),
      estado: pasaASent ? presupuesto_estado.SENT : presupuesto.estado,
    };
  }

  // ── Portal público (sin autenticación) ────────────────────────────────────────

  private async findByToken(token: string) {
    const presupuesto = await this.prisma.presupuesto_cab.findFirst({
      where: { token_acceso_publico: token, deleted: false },
      include: {
        items: { orderBy: { orden: 'asc' } },
        clientes: { include: { personas: { select: { razon_social: true, ruc: true } } } },
        empresas: { select: { razon_social: true, nombre_fantasia: true, ruc: true, dv: true, logo: true, celular: true, email: true } },
        moneda: { select: { codigo: true, simbolo: true, decimales_estandar: true } },
      },
    });
    if (!presupuesto) throw new NotFoundException('Presupuesto no encontrado o enlace inválido');
    return presupuesto;
  }

  /** Datos del presupuesto para el portal del cliente. Marca como visto (SENT → VIEWED). */
  async getPublic(token: string) {
    const p = await this.findByToken(token);
    await this.marcarVisto(p.id, p.estado, p.fecha_vista_cliente);

    const expirado = p.fecha_vigencia ? p.fecha_vigencia < new Date() : false;
    const num = (v: unknown) => (v == null ? 0 : Number(v));

    return {
      token,
      numero: p.numero,
      version: p.version,
      estado: p.estado,
      titulo: p.titulo,
      fecha_emision: p.fecha_emision,
      fecha_vigencia: p.fecha_vigencia,
      expirado,
      condicion_pago: p.condicion_pago,
      notas_cliente: p.notas_cliente,
      moneda: { codigo: p.moneda?.codigo ?? 'PYG', simbolo: p.moneda?.simbolo ?? '₲', decimales: this.decimalesMoneda(p.moneda) },
      empresa: {
        nombre: p.empresas?.razon_social ?? p.empresas?.nombre_fantasia ?? null,
        ruc: p.empresas?.ruc ?? null,
        dv: p.empresas?.dv ?? null,
        logo_url: p.empresas?.logo ?? null,
        telefono: p.empresas?.celular ?? null,
        email: p.empresas?.email ?? null,
      },
      cliente: {
        nombre: p.clientes?.personas?.razon_social ?? p.contacto_cliente ?? null,
        ruc: p.clientes?.personas?.ruc ?? null,
      },
      items: p.items.map((it) => ({
        orden: it.orden,
        tipo: it.tipo,
        descripcion: it.descripcion,
        notas_item: it.notas_item,
        cantidad: num(it.cantidad),
        unidad_medida: it.unidad_medida,
        precio_unitario: num(it.precio_unitario),
        descuento_pct: num(it.descuento_pct),
        descuento_monto: num(it.descuento_monto),
        iva_categoria: it.iva_categoria,
        subtotal_con_iva: num(it.subtotal_con_iva),
      })),
      totales: {
        subtotal_exento: num(p.subtotal_exento),
        base_iva5: num(p.base_iva5),
        iva5: num(p.iva5),
        base_iva10: num(p.base_iva10),
        iva10: num(p.iva10),
        descuento_global_pct: num(p.descuento_global_pct),
        descuento_global_monto: num(p.descuento_global_monto),
        total: num(p.total),
        total_equivalente: p.total_equivalente != null ? Number(p.total_equivalente) : null,
      },
    };
  }

  /** PDF del presupuesto para el portal (vía token). */
  async getPublicPdf(token: string): Promise<{ buffer: Buffer; numero: string }> {
    const p = await this.findByToken(token);
    const buffer = await this.pdfService.generatePdf(p.id, p.empresa_id, 'view');
    return { buffer, numero: p.numero };
  }

  /** Pixel de tracking de apertura del email (SENT → VIEWED). */
  async trackOpen(token: string): Promise<void> {
    const p = await this.prisma.presupuesto_cab.findFirst({
      where: { token_acceso_publico: token, deleted: false },
      select: { id: true, estado: true, fecha_vista_cliente: true },
    });
    if (!p) return; // pixel nunca debe fallar
    await this.marcarVisto(p.id, p.estado, p.fecha_vista_cliente, 'email');
  }

  private async marcarVisto(
    id: string,
    estado: presupuesto_estado,
    fechaVista: Date | null,
    origen: 'portal' | 'email' = 'portal',
  ) {
    const yaVisto = fechaVista != null;
    if (yaVisto && estado !== presupuesto_estado.SENT) return;

    await this.prisma.$transaction(async (tx) => {
      const pasaAViewed = estado === presupuesto_estado.SENT;
      await tx.presupuesto_cab.update({
        where: { id },
        data: {
          ...(fechaVista ? {} : { fecha_vista_cliente: new Date() }),
          ...(pasaAViewed ? { estado: presupuesto_estado.VIEWED } : {}),
          updated_at: new Date(),
        },
      });
      // Registrar apertura en el último envío
      const ultimo = await tx.presupuesto_email.findFirst({
        where: { presupuesto_id: id },
        orderBy: { fecha_envio: 'desc' },
        select: { id: true, fecha_apertura_email: true, fecha_apertura_link: true },
      });
      if (ultimo) {
        await tx.presupuesto_email.update({
          where: { id: ultimo.id },
          data:
            origen === 'email'
              ? { fecha_apertura_email: ultimo.fecha_apertura_email ?? new Date() }
              : { fecha_apertura_link: ultimo.fecha_apertura_link ?? new Date() },
        });
      }
      if (pasaAViewed) {
        await tx.presupuesto_historial.create({
          data: {
            presupuesto_id: id,
            estado_anterior: presupuesto_estado.SENT,
            estado_nuevo: presupuesto_estado.VIEWED,
            comentario: origen === 'email' ? 'Email abierto por el cliente' : 'Presupuesto visto en el portal',
          },
        });
      }
    });
  }

  /** El cliente acepta el presupuesto desde el portal. */
  async accept(token: string, meta?: { ip?: string; userAgent?: string }) {
    const p = await this.findByToken(token);
    this.validarRespondible(p.estado, p.fecha_vigencia);
    return this.responder(p.id, p.estado, presupuesto_estado.ACCEPTED, 'aceptado', meta);
  }

  /** El cliente rechaza el presupuesto desde el portal. */
  async reject(token: string, motivo?: string, meta?: { ip?: string; userAgent?: string }) {
    const p = await this.findByToken(token);
    this.validarRespondible(p.estado, p.fecha_vigencia);
    return this.responder(p.id, p.estado, presupuesto_estado.REJECTED, 'rechazado', meta, motivo);
  }

  private validarRespondible(estado: presupuesto_estado, fechaVigencia: Date | null) {
    if (estado === presupuesto_estado.ACCEPTED || estado === presupuesto_estado.REJECTED) {
      throw new BadRequestException('Este presupuesto ya fue respondido');
    }
    if (estado === presupuesto_estado.SUPERSEDED) {
      throw new BadRequestException(
        'Existe una versión más reciente de este presupuesto. Solicitá el enlace actualizado al proveedor.',
      );
    }
    if (!ESTADOS_RESPONDIBLES.includes(estado)) {
      throw new BadRequestException('Este presupuesto no está disponible para responder');
    }
    if (fechaVigencia && fechaVigencia < new Date()) {
      throw new BadRequestException('Este presupuesto está vencido. Contactá al proveedor para renovarlo.');
    }
  }

  private async responder(
    id: string,
    estadoAnterior: presupuesto_estado,
    estadoNuevo: presupuesto_estado,
    accion: string,
    meta?: { ip?: string; userAgent?: string },
    motivo?: string,
  ) {
    await this.prisma.$transaction(async (tx) => {
      await tx.presupuesto_cab.update({
        where: { id },
        data: {
          estado: estadoNuevo,
          fecha_respuesta_cliente: new Date(),
          ...(motivo ? { motivo_rechazo: motivo } : {}),
          updated_at: new Date(),
        },
      });
      const ultimo = await tx.presupuesto_email.findFirst({
        where: { presupuesto_id: id },
        orderBy: { fecha_envio: 'desc' },
        select: { id: true },
      });
      if (ultimo) {
        await tx.presupuesto_email.update({
          where: { id: ultimo.id },
          data: {
            accion_cliente: accion.toUpperCase(),
            motivo_rechazo: motivo ?? null,
            ip_cliente: meta?.ip ?? null,
            user_agent: meta?.userAgent ?? null,
          },
        });
      }
      await tx.presupuesto_historial.create({
        data: {
          presupuesto_id: id,
          estado_anterior: estadoAnterior,
          estado_nuevo: estadoNuevo,
          comentario: motivo ? `Cliente ${accion}: ${motivo}` : `Cliente ${accion} el presupuesto desde el portal`,
        },
      });
    });
    return { message: `Presupuesto ${accion} correctamente`, estado: estadoNuevo };
  }
}
