/**
 * Backfill de cfdis.cfdi_tipo_relacion + cfdis.cfdis_relacionados desde
 * xml_original para CFDIs pre-migración 032.
 *
 * Criterio: WHERE xml_original IS NOT NULL AND cfdi_tipo_relacion IS NULL.
 * Re-usa `parseXml()` para mantener la lógica de extracción idéntica al sync.
 * Solo escribe si el parser extrae `cfdiTipoRelacion` no-nulo — los CFDIs sin
 * CfdiRelacionados se siguen dejando con NULL (distinguible de "no procesado"
 * via el filtro `cfdi_tipo_relacion IS NULL` porque el WHERE al final del run
 * ya no los va a volver a tocar — pero cada invocación empieza desde el mismo
 * filtro, por eso es idempotente: los sin-relación se re-parsean cada vez pero
 * no se escribe nada).
 *
 * Uso:
 *   pnpm --filter @horux/api exec tsx scripts/backfill-cfdis-relaciones.ts        # ejecuta
 *   pnpm --filter @horux/api exec tsx scripts/backfill-cfdis-relaciones.ts --dry  # reporta sin escribir
 */
import { prisma, tenantDb } from '../src/config/database.js';
import { parseXml } from '../src/services/sat/sat-parser.service.js';

const DRY_RUN = process.argv.includes('--dry') || process.argv.includes('--dry-run');

interface PerTenantResult {
  tenantId: string;
  rfc: string;
  databaseName: string;
  scanned: number;
  parsedOk: number;
  parseFailed: number;
  withRelation: number;
  updated: number;
  byTipoRelacion: Record<string, number>;
  error?: string;
}

async function backfillTenant(
  tenantId: string,
  rfc: string,
  databaseName: string,
): Promise<PerTenantResult> {
  const result: PerTenantResult = {
    tenantId,
    rfc,
    databaseName,
    scanned: 0,
    parsedOk: 0,
    parseFailed: 0,
    withRelation: 0,
    updated: 0,
    byTipoRelacion: {},
  };

  const pool = await tenantDb.getPool(tenantId, databaseName);

  const { rows } = await pool.query<{
    id: number;
    uuid: string;
    type: string;
    xml_original: string | null;
  }>(
    `SELECT id, uuid, type, xml_original
     FROM cfdis
     WHERE xml_original IS NOT NULL
       AND cfdi_tipo_relacion IS NULL
     ORDER BY id`,
  );

  result.scanned = rows.length;
  if (rows.length === 0) return result;

  const client = await pool.connect();
  try {
    await client.query('BEGIN');

    for (const row of rows) {
      if (!row.xml_original) continue;

      const downloadType = row.type === 'EMITIDO' ? 'emitidos' : 'recibidos';
      let parsed;
      try {
        parsed = parseXml(row.xml_original, downloadType);
      } catch {
        result.parseFailed++;
        continue;
      }

      if (!parsed) {
        result.parseFailed++;
        continue;
      }
      result.parsedOk++;

      if (!parsed.cfdiTipoRelacion) continue;

      result.withRelation++;
      const tr = parsed.cfdiTipoRelacion;
      result.byTipoRelacion[tr] = (result.byTipoRelacion[tr] || 0) + 1;

      await client.query(
        `UPDATE cfdis
         SET cfdi_tipo_relacion = $2,
             cfdis_relacionados = $3,
             actualizado_en = NOW()
         WHERE id = $1`,
        [row.id, parsed.cfdiTipoRelacion, parsed.cfdisRelacionados],
      );
      result.updated++;
    }

    if (DRY_RUN) {
      await client.query('ROLLBACK');
    } else {
      await client.query('COMMIT');
    }
  } catch (err: any) {
    await client.query('ROLLBACK').catch(() => {});
    result.error = err?.message || String(err);
  } finally {
    client.release();
  }

  return result;
}

async function main() {
  console.log(`=== Backfill cfdis CfdiRelacionados ${DRY_RUN ? '(DRY RUN — no writes)' : ''} ===\n`);

  const tenants = await prisma.tenant.findMany({
    where: { active: true },
    select: { id: true, rfc: true, databaseName: true },
    orderBy: { rfc: 'asc' },
  });

  console.log(`Tenants activos: ${tenants.length}\n`);

  const results: PerTenantResult[] = [];
  for (const t of tenants) {
    process.stdout.write(`[${t.rfc}] (${t.databaseName}) ... `);
    try {
      const r = await backfillTenant(t.id, t.rfc, t.databaseName);
      results.push(r);
      if (r.error) {
        console.log(`ERROR: ${r.error}`);
      } else if (r.scanned === 0) {
        console.log(`sin CFDIs candidatos (skip)`);
      } else {
        const tiposStr = Object.entries(r.byTipoRelacion)
          .sort((a, b) => b[1] - a[1])
          .map(([tr, n]) => `${tr}:${n}`)
          .join(', ');
        console.log(
          `scan=${r.scanned} parsed=${r.parsedOk} fail=${r.parseFailed} rel=${r.withRelation} upd=${r.updated}${
            tiposStr ? ` [${tiposStr}]` : ''
          }`,
        );
      }
    } catch (err: any) {
      console.log(`FATAL: ${err?.message || err}`);
      results.push({
        tenantId: t.id,
        rfc: t.rfc,
        databaseName: t.databaseName,
        scanned: 0,
        parsedOk: 0,
        parseFailed: 0,
        withRelation: 0,
        updated: 0,
        byTipoRelacion: {},
        error: err?.message || String(err),
      });
    }
  }

  const totalScanned = results.reduce((s, r) => s + r.scanned, 0);
  const totalUpdated = results.reduce((s, r) => s + r.updated, 0);
  const totalParseFailed = results.reduce((s, r) => s + r.parseFailed, 0);
  const tenantsTouched = results.filter(r => r.updated > 0).length;
  const tenantsFailed = results.filter(r => r.error).length;

  const tiposGlobales: Record<string, number> = {};
  for (const r of results) {
    for (const [tr, n] of Object.entries(r.byTipoRelacion)) {
      tiposGlobales[tr] = (tiposGlobales[tr] || 0) + n;
    }
  }

  console.log(`\n=== Resumen ===`);
  console.log(`  Tenants procesados:  ${results.length}`);
  console.log(`  Tenants con backfill: ${tenantsTouched}`);
  console.log(`  CFDIs escaneados:    ${totalScanned}`);
  console.log(`  CFDIs actualizados:  ${totalUpdated}${DRY_RUN ? ' (rolled back)' : ''}`);
  if (totalParseFailed > 0) console.log(`  CFDIs parse falló:   ${totalParseFailed}`);
  if (tenantsFailed > 0) console.log(`  Tenants con error:   ${tenantsFailed}`);
  if (Object.keys(tiposGlobales).length > 0) {
    console.log(`  Desglose TipoRelacion:`);
    for (const [tr, n] of Object.entries(tiposGlobales).sort((a, b) => b[1] - a[1])) {
      console.log(`    ${tr}: ${n}`);
    }
  }

  await prisma.$disconnect();
  process.exit(tenantsFailed > 0 ? 1 : 0);
}

main().catch(async (err) => {
  console.error('Fatal:', err);
  await prisma.$disconnect().catch(() => {});
  process.exit(1);
});
