import type { Request, Response, NextFunction } from 'express';
import type { Pool } from 'pg';
import { z } from 'zod';
import * as facturapiService from '../services/facturapi.service.js';
import {
  createInvoiceContribuyente,
  cancelInvoiceContribuyente,
  downloadPdfContribuyente,
  downloadXmlContribuyente,
  sendInvoiceByEmailContribuyente,
} from '../services/contribuyente-facturapi.service.js';
import { parseXml } from '../services/sat/sat-parser.service.js';
import * as tenantsService from '../services/tenants.service.js';
import { prisma } from '../config/database.js';
import { AppError } from '../middlewares/error.middleware.js';
import { hasPlatformRole } from '../utils/platform-admin.js';
import { auditFromReq } from '../utils/audit.js';

function effectiveTenantId(req: Request): string {
  return req.viewingTenantId || req.user!.tenantId;
}

/**
 * Detecta si un mensaje de error del SAT (propagado por Facturapi) indica
 * que el CSD aún no está en la Lista de Contribuyentes Obligados (LCO).
 * El SAT tarda 24-72h en propagar un CSD nuevo; durante esa ventana todo
 * intento de emisión falla. Cuando se detecta este patrón se marca la
 * org con `last_lco_rejection_at` para que el frontend muestre un banner.
 */
function isLcoRejection(errorMessage: string): boolean {
  if (!errorMessage) return false;
  const msg = errorMessage.toLowerCase();
  return (
    /no se encontr.*rfc.*lco/.test(msg) ||
    /rfc.*no.*registrado.*lco/.test(msg) ||
    /lista.*contribuyentes.*obligados/.test(msg) ||
    /csd.*no.*registrad/.test(msg) ||
    msg.includes('lco')
  );
}

/**
 * Registra el timestamp del rechazo LCO en la fila correspondiente de
 * `facturapi_orgs`. Fire-and-forget: un fallo aquí no bloquea la
 * propagación del error al frontend.
 */
async function markLcoRejection(
  pool: import('pg').Pool,
  contribuyenteId: string | undefined,
): Promise<void> {
  try {
    if (contribuyenteId) {
      await pool.query(
        `UPDATE facturapi_orgs SET last_lco_rejection_at = NOW() WHERE contribuyente_id = $1`,
        [contribuyenteId],
      );
    }
    // Nota: Horux360 single-tenant usaría `tenants.facturapi_org_id` en
    // BD central; en el fork multi-contribuyente solo marcamos la fila
    // por-contribuyente. Si el user emite desde el org del tenant (sin
    // contribuyenteId), el banner no aplicaría aquí.
  } catch (e: any) {
    console.error('[facturacion.markLcoRejection] falló UPDATE:', e?.message || e);
  }
}

// ── Organización ──

export async function getOrgStatus(req: Request, res: Response, next: NextFunction) {
  try {
    const status = await facturapiService.getOrganizationStatus(effectiveTenantId(req));
    res.json(status);
  } catch (error) { next(error); }
}

export async function createOrg(req: Request, res: Response, next: NextFunction) {
  try {
    const result = await facturapiService.createOrganization(effectiveTenantId(req));
    res.status(201).json(result);
  } catch (error) { next(error); }
}

// ── CSD ──

export async function uploadCsd(req: Request, res: Response, next: NextFunction) {
  try {
    const { cerFile, keyFile, password } = req.body;
    if (!cerFile || !keyFile || !password) {
      return res.status(400).json({ message: 'cerFile, keyFile y password son requeridos' });
    }
    const result = await facturapiService.uploadCsd(effectiveTenantId(req), cerFile, keyFile, password);
    if (!result.success) return res.status(400).json({ message: result.message });
    res.json(result);
  } catch (error) { next(error); }
}

// ── Emisión ──

export async function emitir(req: Request, res: Response, next: NextFunction) {
  try {
    const tenantId = effectiveTenantId(req);
    const contribuyenteId = req.body.contribuyenteId as string | undefined;

    // Reservar timbre — si falla emisión en Facturapi, revertimos abajo
    const consumedTimbre = await facturapiService.consumeTimbre(tenantId);

    // Emitir factura en Facturapi
    // Si hay contribuyenteId, usar la org Facturapi del contribuyente (tenant BD).
    // Si no, usar la org del tenant (BD central).
    let invoice;
    try {
      if (contribuyenteId) {
        invoice = await createInvoiceContribuyente(req.tenantPool!, contribuyenteId, req.body);
      } else {
        invoice = await facturapiService.createInvoice(tenantId, req.body);
      }
    } catch (err: any) {
      // SAT nunca selló → revertir el timbre reservado (fire-and-forget; no bloquear la respuesta
      // de error si el refund falla, solo loggear la inconsistencia)
      facturapiService.refundTimbre(tenantId, consumedTimbre).catch(refundErr => {
        console.error('[facturacion.emitir] Falló refund de timbre tras rechazo Facturapi:', {
          tenantId,
          consumedTimbre,
          refundError: refundErr?.message || String(refundErr),
        });
      });
      // Loggea el payload que causó el rechazo para diagnóstico server-side
      console.error('[facturacion.emitir] Rechazo al crear factura:', {
        tenantId,
        contribuyenteId: contribuyenteId || null,
        type: req.body?.type,
        items: req.body?.items?.map((it: any) => ({
          description: it.description,
          taxes: it.taxes,
        })),
        error: err?.message || String(err),
      });
      // Detectar rechazo por CSD aún no propagado a la LCO y marcar la org
      // para que el frontend muestre banner informativo durante 24h.
      if (isLcoRejection(err?.message || '')) {
        await markLcoRejection(req.tenantPool!, contribuyenteId);
      }
      // Propaga el mensaje real (Facturapi suele explicar la validación)
      throw new AppError(400, err?.message || 'Error al emitir factura');
    }

    // Guardar en tabla cfdis del tenant.
    // El response de `invoices.create` de Facturapi NO incluye `issuer`/`subtotal`/`taxes`
    // como campos top-level (usa `issuer_info` y los impuestos viven dentro de `items[*].product.taxes`).
    // La forma más fiable y consistente con el sync SAT es descargar el XML timbrado y
    // reutilizar el mismo parser que ya procesa los CFDIs descargados del SAT.
    const pool = req.tenantPool!;
    const xmlBuffer = contribuyenteId
      ? await downloadXmlContribuyente(pool, contribuyenteId, invoice.id)
      : await facturapiService.downloadXml(tenantId, invoice.id);
    const xmlString = xmlBuffer.toString('utf-8');
    const parsed = parseXml(xmlString, 'emitidos');
    if (!parsed) {
      throw new AppError(500, `Factura ${invoice.uuid} emitida en Facturapi pero el XML no pudo parsearse`);
    }

    const fecha = parsed.fechaEmision;
    const year = String(fecha.getFullYear());
    const month = String(fecha.getMonth() + 1).padStart(2, '0');

    // Upsert RFCs desde datos del XML (fuente autoritativa — igual al sync SAT)
    const { rows: [emisorRow] } = await pool.query(
      `INSERT INTO rfcs (rfc, razon_social, regimen_fiscal) VALUES ($1, $2, $3)
       ON CONFLICT (rfc) DO UPDATE SET
         razon_social = COALESCE(NULLIF($2, ''), rfcs.razon_social),
         regimen_fiscal = CASE WHEN $3 IS NOT NULL AND $3 != '' THEN $3 ELSE rfcs.regimen_fiscal END
       RETURNING id`,
      [parsed.rfcEmisor, parsed.nombreEmisor || null, parsed.regimenFiscalEmisor || null],
    );
    const { rows: [receptorRow] } = await pool.query(
      `INSERT INTO rfcs (rfc, razon_social, regimen_fiscal, codigo_postal) VALUES ($1, $2, $3, $4)
       ON CONFLICT (rfc) DO UPDATE SET
         razon_social = COALESCE(NULLIF($2, ''), rfcs.razon_social),
         regimen_fiscal = CASE WHEN $3 IS NOT NULL AND $3 != '' THEN $3 ELSE rfcs.regimen_fiscal END,
         codigo_postal = CASE WHEN $4 IS NOT NULL AND $4 != '' THEN $4 ELSE rfcs.codigo_postal END
       RETURNING id`,
      [parsed.rfcReceptor, parsed.nombreReceptor || null, parsed.regimenFiscalReceptor || null, req.body.customer?.zip || null],
    );

    await pool.query(`
      INSERT INTO cfdis (
        year, month, type, uuid, serie, folio, status, fecha_emision, fecha_cert_sat,
        rfc_emisor_id, rfc_emisor, nombre_emisor, regimen_fiscal_emisor,
        rfc_receptor_id, rfc_receptor, nombre_receptor, regimen_fiscal_receptor,
        subtotal, subtotal_mxn, total, total_mxn,
        moneda, tipo_comprobante, metodo_pago, forma_pago, uso_cfdi,
        iva_traslado, iva_traslado_mxn,
        iva_retencion, iva_retencion_mxn,
        source, facturapi_id,
        contribuyente_id, xml_original
      ) VALUES (
        $1, $2, 'EMITIDO', $3, $4, $5, 'Vigente', $6, $7,
        $8, $9, $10, $11,
        $12, $13, $14, $15,
        $16, $16, $17, $17,
        $18, $19, $20, $21, $22,
        $23, $23,
        $24, $24,
        'facturapi', $25,
        $26, $27
      )
    `, [
      year, month, parsed.uuid, parsed.serie, parsed.folio, fecha, parsed.fechaCertSat,
      emisorRow.id, parsed.rfcEmisor, parsed.nombreEmisor, parsed.regimenFiscalEmisor,
      receptorRow.id, parsed.rfcReceptor, parsed.nombreReceptor, parsed.regimenFiscalReceptor,
      parsed.subtotal, parsed.total,
      parsed.moneda, parsed.tipoComprobante, parsed.metodoPago, parsed.formaPago, parsed.usoCfdi,
      parsed.ivaTraslado,
      parsed.ivaRetencion,
      invoice.id,
      contribuyenteId ?? null, xmlString,
    ]);

    // Enviar por email si el receptor tiene email — ruteado a la org correcta
    const customerEmail = req.body.customer?.email;
    if (customerEmail) {
      const sendPromise = contribuyenteId
        ? sendInvoiceByEmailContribuyente(req.tenantPool!, contribuyenteId, invoice.id, customerEmail)
        : facturapiService.sendInvoiceByEmail(tenantId, invoice.id, customerEmail);
      sendPromise.catch(err => console.error('[Facturapi] Error enviando email:', err.message));
    }

    res.status(201).json({
      id: invoice.id,
      uuid: invoice.uuid,
      total: invoice.total,
      status: invoice.status,
    });
  } catch (error: any) {
    // Los errores de emisión ya hacen refund dentro del inner catch.
    // Aquí solo propagamos — incluye errores del INSERT post-emisión (CFDI ya sellado,
    // no refund) y errores de validación de timbre (ocurrieron antes del consume).
    next(error);
  }
}

// Estado LCO: si hubo un rechazo del SAT por CSD no propagado en las últimas 24h,
// el frontend muestra un banner informativo en la pantalla de emisión.
export async function getLcoStatus(req: Request, res: Response, next: NextFunction) {
  try {
    const contribuyenteId = req.query.contribuyenteId as string | undefined;
    if (!contribuyenteId) {
      return res.json({ hasRecentLcoRejection: false, rejectedAt: null });
    }

    const { rows } = await req.tenantPool!.query<{ last_lco_rejection_at: Date | null }>(
      `SELECT last_lco_rejection_at FROM facturapi_orgs WHERE contribuyente_id = $1 AND active = true`,
      [contribuyenteId],
    );

    const rejectedAt = rows[0]?.last_lco_rejection_at || null;
    const hasRecentLcoRejection =
      rejectedAt !== null && Date.now() - new Date(rejectedAt).getTime() < 24 * 60 * 60 * 1000;

    res.json({ hasRecentLcoRejection, rejectedAt });
  } catch (error) {
    next(error);
  }
}

// ── Cancelación ──

export async function cancelar(req: Request, res: Response, next: NextFunction) {
  try {
    const tenantId = effectiveTenantId(req);
    const { uuid } = req.params;
    const { motive, substitution } = req.body;

    const pool = req.tenantPool!;
    const { rows } = await pool.query(
      `SELECT facturapi_id, contribuyente_id FROM cfdis WHERE uuid = $1 AND source = 'facturapi'`,
      [uuid]
    );

    if (rows.length === 0 || !rows[0].facturapi_id) {
      return res.status(404).json({ message: 'CFDI no encontrado o no fue emitido por Facturapi' });
    }

    const facturapiId = rows[0].facturapi_id;
    const cfdiContribuyenteId = rows[0].contribuyente_id as string | null;

    const result = cfdiContribuyenteId
      ? await cancelInvoiceContribuyente(pool, cfdiContribuyenteId, facturapiId, motive || '02', substitution)
      : await facturapiService.cancelInvoice(tenantId, facturapiId, motive || '02', substitution);

    // Capturamos la fecha del CFDI antes del UPDATE para saber qué mes marcar
    // como invalidado (la cancelación afecta las métricas del mes del CFDI,
    // no del mes actual).
    const { rows: fechas } = await pool.query<{ fecha_emision: Date; fecha_pago_p: Date | null; tipo_comprobante: string }>(
      `SELECT fecha_emision, fecha_pago_p, tipo_comprobante FROM cfdis WHERE uuid = $1`,
      [uuid],
    );

    await pool.query(
      `UPDATE cfdis SET status = 'Cancelado', fecha_cancelacion = NOW(), actualizado_en = NOW() WHERE uuid = $1`,
      [uuid]
    );

    // Invalidar métricas del mes afectado (usa fecha_pago_p para P, fecha_emision para el resto)
    if (cfdiContribuyenteId && fechas[0]) {
      const f = fechas[0];
      const fechaContable = f.tipo_comprobante === 'P' && f.fecha_pago_p ? f.fecha_pago_p : f.fecha_emision;
      const { markForInvalidation } = await import('../services/metricas.service.js');
      await markForInvalidation(
        pool,
        cfdiContribuyenteId,
        fechaContable.getFullYear(),
        fechaContable.getMonth() + 1,
        'CFDI_CANCEL',
      ).catch(err => console.warn('[Cancelar] markForInvalidation falló:', err?.message || err));
    }

    res.json({ message: 'CFDI cancelado', result });
  } catch (error) { next(error); }
}

// ── Descargas ──

async function resolveCfdiContribuyenteId(
  pool: Pool,
  facturapiId: string,
): Promise<string | null> {
  const { rows } = await pool.query<{ contribuyente_id: string | null }>(
    `SELECT contribuyente_id FROM cfdis WHERE facturapi_id = $1 LIMIT 1`,
    [facturapiId],
  );
  return rows[0]?.contribuyente_id ?? null;
}

export async function downloadPdf(req: Request, res: Response, next: NextFunction) {
  try {
    const id = String(req.params.id);
    const pool = req.tenantPool!;
    const cfdiContribuyenteId = await resolveCfdiContribuyenteId(pool, id);
    const buffer = cfdiContribuyenteId
      ? await downloadPdfContribuyente(pool, cfdiContribuyenteId, id)
      : await facturapiService.downloadPdf(effectiveTenantId(req), id);
    res.setHeader('Content-Type', 'application/pdf');
    res.setHeader('Content-Disposition', `attachment; filename=factura-${id}.pdf`);
    res.send(buffer);
  } catch (error) { next(error); }
}

export async function downloadXml(req: Request, res: Response, next: NextFunction) {
  try {
    const id = String(req.params.id);
    const pool = req.tenantPool!;
    const cfdiContribuyenteId = await resolveCfdiContribuyenteId(pool, id);
    const buffer = cfdiContribuyenteId
      ? await downloadXmlContribuyente(pool, cfdiContribuyenteId, id)
      : await facturapiService.downloadXml(effectiveTenantId(req), id);
    res.setHeader('Content-Type', 'application/xml');
    res.setHeader('Content-Disposition', `attachment; filename=factura-${id}.xml`);
    res.send(buffer);
  } catch (error) { next(error); }
}

// ── Timbres ──

export async function getTimbres(req: Request, res: Response, next: NextFunction) {
  try {
    const status = await facturapiService.getTimbreStatus(effectiveTenantId(req));
    res.json(status);
  } catch (error) { next(error); }
}

// ── Personalización (logo, color) ──

export async function getCustomization(req: Request, res: Response, next: NextFunction) {
  try {
    const data = await facturapiService.getCustomization(effectiveTenantId(req));
    res.json(data || {});
  } catch (error) { next(error); }
}

export async function uploadLogo(req: Request, res: Response, next: NextFunction) {
  try {
    const { logo } = req.body; // base64
    if (!logo) return res.status(400).json({ message: 'Logo es requerido (base64)' });
    const result = await facturapiService.uploadLogo(effectiveTenantId(req), logo);
    if (!result.success) return res.status(400).json({ message: result.message });
    res.json(result);
  } catch (error) { next(error); }
}

export async function updateColor(req: Request, res: Response, next: NextFunction) {
  try {
    const { color } = req.body;
    if (!color) return res.status(400).json({ message: 'Color es requerido' });
    const result = await facturapiService.updateColor(effectiveTenantId(req), color);
    if (!result.success) return res.status(400).json({ message: result.message });
    res.json(result);
  } catch (error) { next(error); }
}

// ── Datos fiscales del tenant ──

export async function getDatosFiscales(req: Request, res: Response, next: NextFunction) {
  try {
    const data = await tenantsService.getDatosFiscales(effectiveTenantId(req));
    res.json(data || {});
  } catch (error) { next(error); }
}

export async function updateDatosFiscales(req: Request, res: Response, next: NextFunction) {
  try {
    if (req.user!.role !== 'owner') {
      return res.status(403).json({ message: 'Solo el dueño puede actualizar datos fiscales' });
    }
    const data = await tenantsService.updateDatosFiscales(effectiveTenantId(req), req.body);
    res.json(data);
  } catch (error) { next(error); }
}

// ── Búsqueda de conceptos previos ──

export async function searchConceptos(req: Request, res: Response, next: NextFunction) {
  try {
    const q = (req.query.q as string || '').trim();
    const tipo = (req.query.tipo as string || 'todos'); // emitidos, recibidos, todos
    const contribuyenteId = (req.query.contribuyenteId as string || '').replace(/[^a-f0-9-]/gi, '');
    const pool = req.tenantPool!;

    let whereType = '';
    if (tipo === 'emitidos') {
      whereType = `AND c.type = 'EMITIDO'`;
    } else if (tipo === 'recibidos') {
      whereType = `AND c.type = 'RECIBIDO' AND c.uso_cfdi = 'G01'`;
    } else {
      whereType = `AND (c.type = 'EMITIDO' OR (c.type = 'RECIBIDO' AND c.uso_cfdi = 'G01'))`;
    }

    const whereContrib = contribuyenteId ? `AND c.contribuyente_id = '${contribuyenteId}'` : '';

    let whereSearch = '';
    const params: any[] = [];
    if (q.length >= 2) {
      params.push(`%${q}%`);
      whereSearch = `AND (cc.descripcion ILIKE $1 OR cc.clave_prod_serv ILIKE $1)`;
    }

    const { rows } = await pool.query(`
      SELECT DISTINCT ON (cc.clave_prod_serv, cc.descripcion)
        cc.clave_prod_serv as "claveProdServ",
        cc.descripcion,
        cc.clave_unidad as "claveUnidad",
        cc.unidad,
        cc.valor_unitario_mxn as "valorUnitario",
        cc.importe_mxn as "importe",
        cc.iva_traslado_mxn as "ivaTraslado",
        cc.isr_retencion_mxn as "isrRetencion",
        cc.iva_retencion_mxn as "ivaRetencion",
        c.type as "tipoCfdi",
        c.rfc_emisor as "rfcEmisor",
        c.nombre_emisor as "nombreEmisor",
        c.rfc_receptor as "rfcReceptor",
        c.nombre_receptor as "nombreReceptor",
        c.fecha_emision as "fechaEmision"
      FROM cfdi_conceptos cc
      JOIN cfdis c ON cc.cfdi_id = c.id
      WHERE c.status NOT IN ('Cancelado', '0')
        ${whereType}
        ${whereContrib}
        ${whereSearch}
      ORDER BY cc.clave_prod_serv, cc.descripcion, c.fecha_emision DESC
      LIMIT 30
    `, params);

    res.json(rows);
  } catch (error) { next(error); }
}

// ── CFDIs PPD pendientes ──

export async function getCfdisPpdPendientes(req: Request, res: Response, next: NextFunction) {
  try {
    const rfc = (req.query.rfc as string || '').trim().toUpperCase();
    if (rfc.length < 3) return res.json([]);

    const pool = req.tenantPool!;

    // Buscar CFDIs emitidos PPD vigentes para este RFC receptor
    // Calcular saldo pendiente: total - pagos previos aplicados
    const { rows } = await pool.query(`
      SELECT
        c.uuid, c.serie, c.folio, c.total_mxn as "totalMxn",
        c.fecha_emision as "fechaEmision",
        c.rfc_receptor as "rfcReceptor",
        c.nombre_receptor as "nombreReceptor",
        c.iva_traslado_mxn as "ivaTrasladoMxn",
        c.total_mxn - COALESCE((
          SELECT SUM(p.monto_pago_mxn)
          FROM cfdis p
          WHERE p.tipo_comprobante = 'P'
            AND p.uuid_relacionado LIKE '%' || c.uuid || '%'
            AND p.status NOT IN ('Cancelado', '0')
        ), 0) as "saldoPendiente"
      FROM cfdis c
      WHERE c.type = 'EMITIDO'
        AND c.metodo_pago = 'PPD'
        AND c.tipo_comprobante = 'I'
        AND c.status NOT IN ('Cancelado', '0')
        AND c.rfc_receptor = $1
      HAVING c.total_mxn - COALESCE((
        SELECT SUM(p.monto_pago_mxn)
        FROM cfdis p
        WHERE p.tipo_comprobante = 'P'
          AND p.uuid_relacionado LIKE '%' || c.uuid || '%'
          AND p.status NOT IN ('Cancelado', '0')
      ), 0) > 0
      ORDER BY c.fecha_emision DESC
      LIMIT 20
    `, [rfc]);

    res.json(rows);
  } catch (error) { next(error); }
}

// ── Búsqueda de RFCs ──

export async function searchRfcs(req: Request, res: Response, next: NextFunction) {
  try {
    const q = (req.query.q as string || '').trim();
    if (q.length < 3) return res.json([]);

    const pool = req.tenantPool!;

    // Obtener RFC del tenant para excluirlo
    const tenantId = effectiveTenantId(req);
    const tenant = await prisma.tenant.findUnique({
      where: { id: tenantId },
      select: { rfc: true },
    });
    const tenantRfc = tenant?.rfc || '';

    const { rows } = await pool.query(`
      SELECT id, rfc, razon_social as "razonSocial",
             regimen_fiscal as "regimenFiscal",
             codigo_postal as "codigoPostal"
      FROM rfcs
      WHERE rfc != $1
        AND (rfc ILIKE $2 OR razon_social ILIKE $2)
      ORDER BY razon_social
      LIMIT 10
    `, [tenantRfc, `%${q}%`]);

    res.json(rows);
  } catch (error) { next(error); }
}

// ── Timbres adicionales: catálogo + compra ──

export async function getPaquetesCatalogo(req: Request, res: Response, next: NextFunction) {
  try {
    const catalogo = await facturapiService.listPaquetesCatalogo();
    res.json(catalogo);
  } catch (error) { next(error); }
}

const comprarPaqueteSchema = z.object({
  catalogoId: z.number().int().positive(),
});

// Admin global: catálogo completo incluyendo inactivos + edit
export async function getPaquetesCatalogoAdmin(req: Request, res: Response, next: NextFunction) {
  try {
    if (!(await hasPlatformRole(req.user!.userId, 'platform_admin'))) {
      return res.status(403).json({ message: 'Solo admin global puede ver el catálogo completo' });
    }
    const catalogo = await facturapiService.listAllPaquetesCatalogo();
    res.json(catalogo);
  } catch (error) { next(error); }
}

const updatePaqueteSchema = z.object({
  precio: z.number().positive().optional(),
  active: z.boolean().optional(),
});

export async function updatePaqueteCatalogo(req: Request, res: Response, next: NextFunction) {
  try {
    if (!(await hasPlatformRole(req.user!.userId, 'platform_admin'))) {
      return res.status(403).json({ message: 'Solo admin global puede editar el catálogo' });
    }
    const id = parseInt(String(req.params.id));
    if (isNaN(id)) return next(new AppError(400, 'id inválido'));

    const data = updatePaqueteSchema.parse(req.body);
    const before = await facturapiService.listAllPaquetesCatalogo().then(r => r.find(p => p.id === id));
    const updated = await facturapiService.updatePaqueteCatalogo({ id, ...data });

    auditFromReq(req, 'timbres.catalogo_updated', {
      entityType: 'TimbrePaqueteCatalogo',
      entityId: String(id),
      metadata: {
        cantidad: updated.cantidad,
        from: { precio: before?.precio, active: before?.active },
        to: { precio: updated.precio, active: updated.active },
      },
    });

    res.json(updated);
  } catch (error: any) {
    if (error instanceof z.ZodError) return next(new AppError(400, error.errors[0].message));
    if (error?.message?.includes('precio') || error?.message?.includes('actualizar')) {
      return next(new AppError(400, error.message));
    }
    next(error);
  }
}

export async function comprarPaquete(req: Request, res: Response, next: NextFunction) {
  try {
    if (!['owner', 'cfo'].includes(req.user!.role)) {
      return res.status(403).json({ message: 'Solo owner/cfo pueden comprar timbres adicionales' });
    }
    const { catalogoId } = comprarPaqueteSchema.parse(req.body);
    const result = await facturapiService.iniciarCompraPaquete({
      tenantId: effectiveTenantId(req),
      catalogoId,
      callerEmail: req.user!.email,
    });
    res.json(result);
  } catch (error: any) {
    if (error instanceof z.ZodError) return next(new AppError(400, error.errors[0].message));
    // Errores de negocio esperados → 400 con mensaje para el usuario
    const msg = error?.message || '';
    if (msg.includes('no disponible') || msg.includes('dueño') || msg.includes('email') || msg.includes('MercadoPago')) {
      return next(new AppError(400, msg));
    }
    console.error('[comprarPaquete] Error no esperado:', error);
    next(error);
  }
}
