import type { Request, Response, NextFunction } from 'express';
import * as cfdiService from '../services/cfdi.service.js';
import { AppError } from '../middlewares/error.middleware.js';
import { GRUPO_PF_EMPRESARIAL, GRUPO_PM_OTROS } from '../services/dashboard.service.js';
import { getRegimenesIgnoradosClaves } from '../services/regimen.service.js';
import { resolveContribuyenteContext } from '../utils/contribuyente-context.js';
import type { CfdiFilters } from '@horux/shared';

export async function getCfdis(req: Request, res: Response, next: NextFunction) {
  try {
    if (!req.tenantPool) {
      return next(new AppError(400, 'Tenant no configurado'));
    }

    const filters: CfdiFilters = {
      tipo: req.query.tipo as any,
      tipoComprobante: req.query.tipoComprobante as any,
      estado: req.query.estado as any,
      fechaInicio: req.query.fechaInicio as string,
      fechaFin: req.query.fechaFin as string,
      rfc: req.query.rfc as string,
      emisor: req.query.emisor as string,
      receptor: req.query.receptor as string,
      search: req.query.search as string,
      contribuyenteId: req.query.contribuyenteId as string,
      page: parseInt(req.query.page as string) || 1,
      limit: parseInt(req.query.limit as string) || 20,
    };

    const result = await cfdiService.getCfdis(req.tenantPool, filters);
    res.json(result);
  } catch (error) {
    next(error);
  }
}

export async function getCfdiById(req: Request, res: Response, next: NextFunction) {
  try {
    if (!req.tenantPool) {
      return next(new AppError(400, 'Tenant no configurado'));
    }

    const cfdi = await cfdiService.getCfdiById(req.tenantPool, String(req.params.id));

    if (!cfdi) {
      return next(new AppError(404, 'CFDI no encontrado'));
    }

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

export async function getXml(req: Request, res: Response, next: NextFunction) {
  try {
    if (!req.tenantPool) {
      return next(new AppError(400, 'Tenant no configurado'));
    }

    const xml = await cfdiService.getXmlById(req.tenantPool, String(req.params.id));

    if (!xml) {
      return next(new AppError(404, 'XML no encontrado para este CFDI'));
    }

    res.set('Content-Type', 'application/xml');
    res.set('Content-Disposition', `attachment; filename="cfdi-${req.params.id}.xml"`);
    res.send(xml);
  } catch (error) {
    next(error);
  }
}

export async function getConceptos(req: Request, res: Response, next: NextFunction) {
  try {
    if (!req.tenantPool) {
      return next(new AppError(400, 'Tenant no configurado'));
    }

    const conceptos = await cfdiService.getConceptos(req.tenantPool, String(req.params.id));
    res.json(conceptos);
  } catch (error) {
    next(error);
  }
}

export async function drillDown(req: Request, res: Response, next: NextFunction) {
  try {
    if (!req.tenantPool) {
      return next(new AppError(400, 'Tenant no configurado'));
    }

    const {
      fechaInicio, fechaFin, type, tipoComprobante, metodoPago,
      regimenEmisor, regimenReceptor, status, contribuyenteId,
      bucket,
    } = req.query;

    let where = 'WHERE 1=1';
    const params: any[] = [];
    let pi = 1;

    // `bucket` expande la combinación (type, tipo_comprobante, metodo_pago,
    // régimen) exactamente igual a la fórmula de KPIs/tarjetas — para que
    // el drill-down cuadre línea a línea con el total del header.
    //
    // Reglas por bucket (alineado con dashboard.service y impuestos.service):
    //   ingresos: 3 grupos de régimen del emisor con fórmulas distintas.
    //     Grupo 1 (PF Empresarial 606/612/621/625/626):
    //       EMIT I PUE + EMIT P + EMIT E PUE (excl. E/07)
    //     Grupo 2 (Sueldos 605, recibido como N):
    //       RECIB N PUE con receptor=605
    //     Grupo 3 (PM y otros): EMIT I PUE+PPD + EMIT E PUE
    //   gastos:  uniforme todos los regímenes del receptor
    //       RECIB I PUE + RECIB P + RECIB E PUE (excl. E/07)
    //   causado (IVA): EMIT I PUE + EMIT P + EMIT E PUE (excl. E/07)
    //   acreditable (IVA): RECIB I PUE + RECIB P + RECIB E PUE (excl. E/07)
    //
    // Régimenes "ignorados" por el tenant se excluyen en todos los buckets.
    // Las NC que restan se muestran como filas con signo (frontend las resta
    // del total del header). Si `bucket` se pasa, se ignoran filtros
    // type/tipoComprobante/metodoPago de entrada.
    const bucketStr = typeof bucket === 'string' ? bucket.toLowerCase() : '';
    const bucketApplied = bucketStr === 'ingresos' || bucketStr === 'gastos' ||
                          bucketStr === 'causado'  || bucketStr === 'acreditable';

    // Régimenes ignorados por el tenant (configurable en /regimenes). Se
    // excluyen del lado correspondiente según el bucket.
    const ignorados = req.user?.tenantId
      ? await getRegimenesIgnoradosClaves(req.user.tenantId)
      : [];

    // Resolver condiciones esEmisor/esReceptor basadas en RFC del contribuyente.
    // Reemplaza `type = 'EMITIDO/RECIBIDO' AND contribuyente_id = X` por un
    // filtro por RFC — fuente de verdad cuando dos contribuyentes del tenant
    // se facturan entre sí (type/contribuyente_id pueden ser inconsistentes).
    const contribIdStr = typeof contribuyenteId === 'string' ? contribuyenteId : undefined;
    const cfdiCtx = req.user?.tenantId
      ? await resolveContribuyenteContext(req.tenantPool, req.user.tenantId, contribIdStr)
      : null;
    const esEmisor = cfdiCtx?.esEmisor || `type = 'EMITIDO'`;
    const esReceptor = cfdiCtx?.esReceptor || `type = 'RECIBIDO'`;

    const NO_IGNORADO_EMISOR = ignorados.length > 0
      ? `AND (regimen_fiscal_emisor IS NULL OR regimen_fiscal_emisor NOT IN (${ignorados.map(r => `'${r}'`).join(',')}))`
      : '';
    const NO_IGNORADO_RECEPTOR = ignorados.length > 0
      ? `AND (regimen_fiscal_receptor IS NULL OR regimen_fiscal_receptor NOT IN (${ignorados.map(r => `'${r}'`).join(',')}))`
      : '';

    const g1 = GRUPO_PF_EMPRESARIAL.map(r => `'${r}'`).join(',');
    const g3 = GRUPO_PM_OTROS.map(r => `'${r}'`).join(',');
    // Conjunto canónico de regímenes que el dashboard considera (excluye 616
    // extranjero y otros fuera del catálogo). El drill debe respetarlo para
    // cuadrar con los KPIs/tarjetas.
    const TODOS_REGS = [...GRUPO_PF_EMPRESARIAL, '605', ...GRUPO_PM_OTROS]
      .map(r => `'${r}'`)
      .join(',');
    const E_NO_ANTICIPO = `COALESCE(cfdi_tipo_relacion, '') <> '07'`;

    if (bucketStr === 'ingresos') {
      // 3 grupos con fórmulas distintas. Filtro por RFC (esEmisor/esReceptor).
      // Grupo 1 usa Método A: todas las I/07 y E/07 se incluyen (sin filtro
      // `E_NO_ANTICIPO`) — la suma algebraica se neutraliza correctamente
      // cuando anticipo, I/07 y E/07 están en el mismo universo de la query.
      where += ` AND (
        ( -- Grupo 1 PF Empresarial
          ${esEmisor}
          AND regimen_fiscal_emisor IN (${g1})
          AND (
            (tipo_comprobante = 'I' AND metodo_pago = 'PUE')
            OR (tipo_comprobante = 'P')
            OR (tipo_comprobante = 'E' AND metodo_pago = 'PUE')
          )
        )
        OR ( -- Grupo 2 Sueldos: nómina recibida 605
          ${esReceptor}
          AND tipo_comprobante = 'N'
          AND metodo_pago = 'PUE'
          AND regimen_fiscal_receptor = '605'
        )
        OR ( -- Grupo 3 PM y otros
          ${esEmisor}
          AND regimen_fiscal_emisor IN (${g3})
          AND (
            (tipo_comprobante = 'I' AND metodo_pago IN ('PUE','PPD'))
            OR (tipo_comprobante = 'E' AND metodo_pago = 'PUE')
          )
        )
      ) ${NO_IGNORADO_EMISOR.replace('regimen_fiscal_emisor', `CASE WHEN ${esEmisor} THEN regimen_fiscal_emisor ELSE regimen_fiscal_receptor END`)}`;
    } else if (bucketStr === 'gastos') {
      // Método A: sin E_NO_ANTICIPO — las E/07 también aparecen en el
      // drill (restan del gasto al igual que en el KPI).
      where += ` AND (
        ${esReceptor} AND (
          (tipo_comprobante = 'I' AND metodo_pago = 'PUE')
          OR (tipo_comprobante = 'P')
          OR (tipo_comprobante = 'E' AND metodo_pago = 'PUE')
        )
        AND regimen_fiscal_receptor IN (${TODOS_REGS})
      ) ${NO_IGNORADO_RECEPTOR}`;
    } else if (bucketStr === 'causado') {
      where += ` AND (
        ${esEmisor} AND (
          (tipo_comprobante = 'I' AND metodo_pago = 'PUE')
          OR (tipo_comprobante = 'P')
          OR (tipo_comprobante = 'E' AND metodo_pago = 'PUE' AND ${E_NO_ANTICIPO})
        )
        AND regimen_fiscal_emisor IN (${TODOS_REGS})
      ) ${NO_IGNORADO_EMISOR}`;
    } else if (bucketStr === 'acreditable') {
      where += ` AND (
        ${esReceptor} AND (
          (tipo_comprobante = 'I' AND metodo_pago = 'PUE')
          OR (tipo_comprobante = 'P')
          OR (tipo_comprobante = 'E' AND metodo_pago = 'PUE' AND ${E_NO_ANTICIPO})
        )
        AND regimen_fiscal_receptor IN (${TODOS_REGS})
      ) ${NO_IGNORADO_RECEPTOR}`;
    }

    // Fecha efectiva: para CFDIs tipo P (complementos de pago) usa fecha_pago_p
    // (cuándo el cliente cobró) en vez de fecha_emision (cuándo se emitió el
    // complemento). Así el drill-down es coherente con los KPIs — un P emitido
    // en mayo que cobró una PPD de noviembre aparece en noviembre, no en mayo.
    const FECHA_EFECTIVA = `CASE WHEN tipo_comprobante = 'P' THEN fecha_pago_p ELSE fecha_emision END`;
    if (fechaInicio) {
      where += ` AND ${FECHA_EFECTIVA} >= $${pi++}::date`;
      params.push(fechaInicio);
    }
    if (fechaFin) {
      where += ` AND ${FECHA_EFECTIVA} < ($${pi++}::date + interval '1 day')`;
      params.push(fechaFin);
    }
    if (!bucketApplied) {
      if (type) {
        where += ` AND type = $${pi++}`;
        params.push(type);
      }
      // tipoComprobante acepta valor único ('I') o CSV ('I,P'). Cuando la lista
      // incluye P, el filtro metodoPago NO se aplica a los P (que no tienen),
      // para que un drill-down "Ingresos del Mes" muestre I PUE + todos los P.
      const tiposList = tipoComprobante
        ? (tipoComprobante as string).split(',').map(t => t.trim()).filter(Boolean)
        : [];
      const includesP = tiposList.includes('P');
      if (tiposList.length === 1) {
        where += ` AND tipo_comprobante = $${pi++}`;
        params.push(tiposList[0]);
      } else if (tiposList.length > 1) {
        where += ` AND tipo_comprobante = ANY($${pi++})`;
        params.push(tiposList);
      }
      if (metodoPago) {
        const metodos = (metodoPago as string).split(',');
        if (includesP) {
          // P no tiene metodo_pago: el filtro aplica solo a los no-P
          where += ` AND (tipo_comprobante = 'P' OR metodo_pago = ANY($${pi++}))`;
          params.push(metodos);
        } else {
          where += ` AND metodo_pago = ANY($${pi++})`;
          params.push(metodos);
        }
      }
    }
    if (regimenEmisor) {
      where += ` AND regimen_fiscal_emisor = $${pi++}`;
      params.push(regimenEmisor);
    }
    if (regimenReceptor) {
      where += ` AND regimen_fiscal_receptor = $${pi++}`;
      params.push(regimenReceptor);
    }
    if (status) {
      if (status === 'vigente') {
        where += ` AND status NOT IN ('Cancelado', '0')`;
      } else {
        where += ` AND status IN ('Cancelado', '0')`;
      }
    }
    if (contribuyenteId && !bucketApplied) {
      // Solo aplica cuando NO hay bucket (drill crudo, sin semantic de lado).
      // Con bucket, esEmisor/esReceptor ya restringen por RFC del contribuyente.
      // Sin bucket, filtramos inclusivo: contribuyente_id O RFC en cualquier lado.
      if (cfdiCtx) {
        where += ` AND ${cfdiCtx.contribFilter.replace(/^AND /, '')}`;
      }
    }

    const { rows } = await req.tenantPool.query(`
      SELECT id, uuid, type, tipo_comprobante as "tipoComprobante",
        fecha_emision as "fechaEmision", status,
        rfc_emisor as "rfcEmisor", nombre_emisor as "nombreEmisor",
        rfc_receptor as "rfcReceptor", nombre_receptor as "nombreReceptor",
        subtotal, subtotal_mxn as "subtotalMxn",
        total, total_mxn as "totalMxn",
        moneda, metodo_pago as "metodoPago",
        iva_traslado_mxn as "ivaTrasladoMxn",
        iva_retencion_mxn as "ivaRetencionMxn",
        isr_retencion_mxn as "isrRetencionMxn",
        monto_pago_mxn as "montoPagoMxn",
        regimen_fiscal_emisor as "regimenEmisor",
        regimen_fiscal_receptor as "regimenReceptor"
      FROM cfdis
      ${where}
      ORDER BY fecha_emision DESC
      LIMIT 500
    `, params);

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

export async function getEmisores(req: Request, res: Response, next: NextFunction) {
  try {
    if (!req.tenantPool) {
      return next(new AppError(400, 'Tenant no configurado'));
    }

    const search = (req.query.search as string) || '';
    if (search.length < 2) {
      return res.json([]);
    }

    const contribuyenteId = req.query.contribuyenteId as string | undefined;
    const emisores = await cfdiService.getEmisores(req.tenantPool, search, 10, contribuyenteId);
    res.json(emisores);
  } catch (error) {
    next(error);
  }
}

export async function getReceptores(req: Request, res: Response, next: NextFunction) {
  try {
    if (!req.tenantPool) {
      return next(new AppError(400, 'Tenant no configurado'));
    }

    const search = (req.query.search as string) || '';
    if (search.length < 2) {
      return res.json([]);
    }

    const contribuyenteId = req.query.contribuyenteId as string | undefined;
    const receptores = await cfdiService.getReceptores(req.tenantPool, search, 10, contribuyenteId);
    res.json(receptores);
  } catch (error) {
    next(error);
  }
}

export async function getResumen(req: Request, res: Response, next: NextFunction) {
  try {
    if (!req.tenantPool) {
      return next(new AppError(400, 'Tenant no configurado'));
    }

    const año = parseInt(req.query.año as string) || new Date().getFullYear();
    const mes = parseInt(req.query.mes as string) || new Date().getMonth() + 1;
    const contribuyenteId = req.query.contribuyenteId as string | undefined;

    const resumen = await cfdiService.getResumenCfdis(req.tenantPool, año, mes, contribuyenteId);
    res.json(resumen);
  } catch (error) {
    next(error);
  }
}

export async function createCfdi(req: Request, res: Response, next: NextFunction) {
  try {
    if (!req.tenantPool) {
      return next(new AppError(400, 'Tenant no configurado'));
    }

    if (!['owner', 'contador'].includes(req.user!.role)) {
      return next(new AppError(403, 'No tienes permisos para agregar CFDIs'));
    }

    const cfdi = await cfdiService.createCfdi(req.tenantPool, req.body);
    res.status(201).json(cfdi);
  } catch (error: any) {
    if (error.message?.includes('duplicate')) {
      return next(new AppError(409, 'Este CFDI ya existe (UUID duplicado)'));
    }
    next(error);
  }
}

export async function createManyCfdis(req: Request, res: Response, next: NextFunction) {
  try {
    if (!req.tenantPool) {
      return next(new AppError(400, 'Tenant no configurado'));
    }

    if (!['owner', 'contador'].includes(req.user!.role)) {
      return next(new AppError(403, 'No tienes permisos para agregar CFDIs'));
    }

    if (!Array.isArray(req.body.cfdis)) {
      return next(new AppError(400, 'Se requiere un array de CFDIs'));
    }

    const batchInfo = {
      batchNumber: req.body.batchNumber || 1,
      totalBatches: req.body.totalBatches || 1,
      totalFiles: req.body.totalFiles || req.body.cfdis.length
    };

    console.log(`[CFDI Bulk] Lote ${batchInfo.batchNumber}/${batchInfo.totalBatches} - ${req.body.cfdis.length} CFDIs`);

    const result = await cfdiService.createManyCfdisBatch(req.tenantPool, req.body.cfdis);

    res.status(201).json({
      message: `Lote ${batchInfo.batchNumber} procesado`,
      batchNumber: batchInfo.batchNumber,
      totalBatches: batchInfo.totalBatches,
      inserted: result.inserted,
      duplicates: result.duplicates,
      errors: result.errors,
      errorMessages: result.errorMessages.slice(0, 5)
    });
  } catch (error: any) {
    console.error('[CFDI Bulk Error]', error.message, error.stack);
    next(new AppError(400, error.message || 'Error al procesar CFDIs'));
  }
}

export async function deleteCfdi(req: Request, res: Response, next: NextFunction) {
  try {
    if (!req.tenantPool) {
      return next(new AppError(400, 'Tenant no configurado'));
    }

    if (!['owner', 'contador'].includes(req.user!.role)) {
      return next(new AppError(403, 'No tienes permisos para eliminar CFDIs'));
    }

    await cfdiService.deleteCfdi(req.tenantPool, String(req.params.id));
    res.status(204).send();
  } catch (error) {
    next(error);
  }
}
