import { chromium } from 'playwright';
import { writeFileSync, unlinkSync, mkdirSync, rmdirSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { randomUUID } from 'crypto';
import type { Pool } from 'pg';
import { prisma, tenantDb } from '../config/database.js';
import { getDecryptedFiel } from './fiel.service.js';
import { getDecryptedFielContribuyente } from './contribuyente-fiel.service.js';
import { loginSatCsf } from './sat/sat-csf-login.js';
import { extractCsfPdf } from './sat/sat-csf-scraper.js';
import { parseCsfPdf, type ConstanciaSituacionFiscal, type Domicilio, type RegimenCsf } from './sat/sat-csf-parser.js';

const PROCESS_TIMEOUT = 180_000;

export interface ConstanciaRow {
  id: number;
  rfc: string;
  idCif: string | null;
  razonSocial: string | null;
  estatusPadron: string | null;
  fechaEmision: string | null;
  datos: ConstanciaSituacionFiscal;
  fechaConsulta: string;
  createdAt: string;
}

function rowToConstancia(r: any): ConstanciaRow {
  return {
    id: r.id,
    rfc: r.rfc,
    idCif: r.id_cif,
    razonSocial: r.razon_social,
    estatusPadron: r.estatus_padron,
    fechaEmision: r.fecha_emision,
    datos: r.datos,
    fechaConsulta: r.fecha_consulta.toISOString(),
    createdAt: r.created_at.toISOString(),
  };
}

/**
 * Descarga la CSF del portal SAT, la parsea, guarda en BD del tenant, y
 * sincroniza automáticamente domicilio + regímenes activos con lo que reporta
 * el SAT. El auto-fill NO es destructivo para datos custom del usuario:
 * solo sobreescribe campos si la CSF tiene un valor no-vacío.
 */
export async function consultarConstancia(tenantId: string): Promise<ConstanciaRow> {
  const fiel = await getDecryptedFiel(tenantId);
  if (!fiel) throw new Error('No hay FIEL configurada o está vencida');

  const tenant = await prisma.tenant.findUnique({
    where: { id: tenantId },
    select: { databaseName: true },
  });
  if (!tenant) throw new Error('Tenant no encontrado');

  const tempId = randomUUID();
  const tempDir = join(tmpdir(), `horux-csf-${tempId}`);
  mkdirSync(tempDir, { recursive: true, mode: 0o700 });
  const cerPath = join(tempDir, 'cert.cer');
  const keyPath = join(tempDir, 'key.key');

  try {
    writeFileSync(cerPath, Buffer.from(fiel.cerContent, 'binary'), { mode: 0o600 });
    writeFileSync(keyPath, Buffer.from(fiel.keyContent, 'binary'), { mode: 0o600 });

    // Headless por default. El fix de dispatchEvent en sat-csf-login cubre el
    // caso donde el click sintético no dispara el handler del SAT. Si algún
    // ambiente necesita ver el browser (debug), setear SAT_HEADLESS=false.
    const headless = process.env.SAT_HEADLESS !== 'false';
    const browser = await chromium.launch({ headless });
    try {
      const timeoutPromise = new Promise<never>((_, reject) =>
        setTimeout(() => reject(new Error('Timeout: proceso de CSF excedió 3 minutos')), PROCESS_TIMEOUT),
      );

      const resultPromise = (async () => {
        const session = await loginSatCsf(browser, cerPath, keyPath, fiel.password);
        const pdfBuffer = await extractCsfPdf(session);
        const csf = await parseCsfPdf(pdfBuffer);

        const pool = await tenantDb.getPool(tenantId, tenant.databaseName);
        const { rows } = await pool.query(
          `INSERT INTO constancias_situacion_fiscal
             (rfc, id_cif, razon_social, estatus_padron, fecha_emision, datos, pdf)
           VALUES ($1, $2, $3, $4, $5, $6, $7)
           RETURNING id, rfc, id_cif, razon_social, estatus_padron, fecha_emision,
                     datos, fecha_consulta, created_at`,
          [
            csf.rfc,
            csf.idCIF,
            csf.razonSocial ?? [csf.nombre, csf.primerApellido, csf.segundoApellido].filter(Boolean).join(' ') ?? null,
            csf.estatusPadron,
            csf.lugarFechaEmision,
            JSON.stringify(csf),
            pdfBuffer,
          ],
        );

        // Auto-fill domicilio del tenant + regímenes activos desde el CSF.
        // Se hace después del INSERT para que si algo falla en la sincronización
        // la CSF ya quedó guardada y el usuario puede verla.
        await sincronizarDatosFiscales(tenantId, csf).catch(err => {
          console.error(`[CSF] Error sincronizando datos fiscales para tenant ${tenantId}:`, err);
        });

        return rowToConstancia(rows[0]);
      })();

      return await Promise.race([resultPromise, timeoutPromise]);
    } finally {
      await browser.close();
    }
  } finally {
    try { unlinkSync(cerPath); } catch { /* ok */ }
    try { unlinkSync(keyPath); } catch { /* ok */ }
    try { rmdirSync(tempDir); } catch { /* ok */ }
  }
}

/**
 * Convierte el domicilio del CSF a los campos de `tenants` (calle compuesta
 * por tipoVialidad + nombreVialidad). Solo actualiza campos cuando el CSF
 * trae un valor — nunca pisa con null.
 */
function domicilioToTenantFields(d: Domicilio): Record<string, string | undefined> {
  const calleComponents = [d.tipoVialidad, d.nombreVialidad].filter(Boolean);
  const calle = calleComponents.length > 0 ? calleComponents.join(' ') : undefined;
  return {
    codigoPostal: d.codigoPostal,
    calle,
    numExterior: d.numeroExterior,
    numInterior: d.numeroInterior && d.numeroInterior.toUpperCase() !== 'SIN NUMERO' ? d.numeroInterior : undefined,
    colonia: d.colonia,
    ciudad: d.localidad,
    municipio: d.municipio,
    estado: d.entidadFederativa,
  };
}

/**
 * Matchea el nombre del régimen como aparece en la CSF contra el catálogo
 * `regimenes` (clave SAT + descripción). La CSF prefija "Régimen " o
 * "Régimen de " a veces, y el catálogo no — normalizamos ambos para matchear.
 */
function normalizeRegimenName(s: string): string {
  return s
    .normalize('NFD').replace(/[\u0300-\u036f]/g, '')
    .toLowerCase()
    .replace(/^r[eé]gimen\s+(?:de\s+(?:las?|los)?\s*)?/i, '')
    .replace(/\s+/g, ' ')
    .trim();
}

async function matchRegimenesToCatalogo(regimenesCsf: RegimenCsf[]): Promise<number[]> {
  const activos = regimenesCsf.filter(r => !r.fechaFin);
  if (activos.length === 0) return [];

  const catalogo = await prisma.regimen.findMany({ where: { activo: true } });
  const ids: number[] = [];

  for (const rc of activos) {
    const nNormalizado = normalizeRegimenName(rc.nombre);
    const match = catalogo.find(c => {
      const catNorm = normalizeRegimenName(c.descripcion);
      return catNorm === nNormalizado || catNorm.includes(nNormalizado) || nNormalizado.includes(catNorm);
    });
    if (match) ids.push(match.id);
  }
  return [...new Set(ids)];
}

/**
 * Aplica el domicilio + regímenes activos de la CSF al tenant. Idempotente:
 * se puede llamar N veces, el resultado final refleja el último CSF.
 */
export async function sincronizarDatosFiscales(
  tenantId: string,
  csf: ConstanciaSituacionFiscal,
): Promise<{ domicilioActualizado: boolean; regimenesSincronizados: number }> {
  // 1. Domicilio
  const fields = domicilioToTenantFields(csf.domicilio);
  const updates: Record<string, string> = {};
  for (const [k, v] of Object.entries(fields)) {
    if (v && v.trim().length > 0) updates[k] = v.trim();
  }

  if (Object.keys(updates).length > 0) {
    await prisma.tenant.update({ where: { id: tenantId }, data: updates });
  }

  // 2. Regímenes activos — sobreescribe la lista completa con lo que diga la CSF
  const regimenIds = await matchRegimenesToCatalogo(csf.regimenes);
  if (regimenIds.length > 0) {
    await prisma.$transaction([
      prisma.tenantRegimenActivo.deleteMany({ where: { tenantId } }),
      prisma.tenantRegimenActivo.createMany({ data: regimenIds.map(regimenId => ({ tenantId, regimenId })) }),
    ]);
  }

  return {
    domicilioActualizado: Object.keys(updates).length > 0,
    regimenesSincronizados: regimenIds.length,
  };
}

export async function listConstancias(pool: Pool, limit = 12, rfc?: string): Promise<ConstanciaRow[]> {
  const params: unknown[] = [limit];
  let rfcFilter = '';
  if (rfc) {
    rfcFilter = 'WHERE rfc = $2';
    params.push(rfc);
  }
  const { rows } = await pool.query(
    `SELECT id, rfc, id_cif, razon_social, estatus_padron, fecha_emision,
            datos, fecha_consulta, created_at
     FROM constancias_situacion_fiscal
     ${rfcFilter}
     ORDER BY fecha_consulta DESC
     LIMIT $1`,
    params,
  );
  return rows.map(rowToConstancia);
}

export async function getConstanciaPdf(pool: Pool, id: number): Promise<Buffer | null> {
  const { rows } = await pool.query(`SELECT pdf FROM constancias_situacion_fiscal WHERE id = $1`, [id]);
  return rows.length > 0 ? rows[0].pdf : null;
}

/**
 * Retención 5 años (CFF Art. 30). Se ejecuta en cron diario.
 */
export async function purgeConstanciasAntiguas(pool: Pool): Promise<{ deleted: number }> {
  const { rowCount } = await pool.query(
    `DELETE FROM constancias_situacion_fiscal WHERE created_at < NOW() - INTERVAL '5 years'`,
  );
  return { deleted: rowCount ?? 0 };
}

/**
 * Descarga la CSF para un contribuyente específico (modo despacho).
 * Usa la FIEL almacenada en la BD del tenant en lugar de la BD central.
 */
export async function consultarConstanciaContribuyente(
  pool: Pool,
  contribuyenteId: string,
): Promise<ConstanciaRow> {
  const safeId = contribuyenteId.replace(/[^a-f0-9-]/gi, '');
  const fiel = await getDecryptedFielContribuyente(pool, safeId);
  if (!fiel) throw new Error('No hay FIEL configurada para este contribuyente o está vencida');

  const tempId = randomUUID();
  const tempDir = join(tmpdir(), `horux-csf-${tempId}`);
  mkdirSync(tempDir, { recursive: true, mode: 0o700 });
  const cerPath = join(tempDir, 'cert.cer');
  const keyPath = join(tempDir, 'key.key');

  try {
    writeFileSync(cerPath, Buffer.from(fiel.cerContent, 'binary'), { mode: 0o600 });
    writeFileSync(keyPath, Buffer.from(fiel.keyContent, 'binary'), { mode: 0o600 });

    const headless = process.env.SAT_HEADLESS !== 'false';
    const browser = await chromium.launch({ headless });
    try {
      const timeoutPromise = new Promise<never>((_, reject) =>
        setTimeout(() => reject(new Error('Timeout: proceso de CSF excedió 3 minutos')), PROCESS_TIMEOUT),
      );

      const resultPromise = (async () => {
        const session = await loginSatCsf(browser, cerPath, keyPath, fiel.password);
        const pdfBuffer = await extractCsfPdf(session);
        const csf = await parseCsfPdf(pdfBuffer);

        const { rows } = await pool.query(
          `INSERT INTO constancias_situacion_fiscal
             (rfc, id_cif, razon_social, estatus_padron, fecha_emision, datos, pdf)
           VALUES ($1, $2, $3, $4, $5, $6, $7)
           RETURNING id, rfc, id_cif, razon_social, estatus_padron, fecha_emision,
                     datos, fecha_consulta, created_at`,
          [
            csf.rfc,
            csf.idCIF,
            csf.razonSocial ?? [csf.nombre, csf.primerApellido, csf.segundoApellido].filter(Boolean).join(' ') ?? null,
            csf.estatusPadron,
            csf.lugarFechaEmision,
            JSON.stringify(csf),
            pdfBuffer,
          ],
        );

        // Sync datos fiscales to contribuyente table
        try {
          const rawDom = csf.domicilio || {};

          // The PDF parser sometimes captures label prefixes inside values
          // when the PDF has a two-column layout. Clean them out.
          function cleanDomField(val: string | undefined): string {
            if (!val) return '';
            // Remove embedded label prefixes like "Nombre de la Colonia: "
            return val
              .replace(/^.*(?:Nombre de la Colonia|Nombre del Municipio|Nombre de la Localidad|Nombre de la Entidad|Número Exterior|Número Interior|Tipo de Vialidad|Entre Calle|Y Calle|Código Postal)\s*:\s*/i, '')
              .trim();
          }

          // Extract embedded values from fields that swallowed the next column
          function extractEmbedded(val: string | undefined, labelPrefix: string): string {
            if (!val) return '';
            const re = new RegExp(`${labelPrefix}\\s*:\\s*(.+)`, 'i');
            const m = val.match(re);
            return m ? m[1].trim() : '';
          }

          // Check if values have embedded labels and extract the correct fields
          const rawNumInterior = rawDom.numeroInterior || '';
          const rawLocalidad = rawDom.localidad || '';

          const colonia = rawDom.colonia
            || extractEmbedded(rawNumInterior, 'Nombre de la Colonia')
            || extractEmbedded(rawLocalidad, 'Nombre de la Colonia')
            || '';
          const municipio = rawDom.municipio
            || extractEmbedded(rawLocalidad, 'Nombre del Municipio o Demarcación Territorial')
            || extractEmbedded(rawNumInterior, 'Nombre del Municipio')
            || '';

          // Map CSF field names → UI field names
          const domicilioMapped = {
            codigoPostal: cleanDomField(rawDom.codigoPostal),
            calle: cleanDomField(rawDom.nombreVialidad) || '',
            numExterior: cleanDomField(rawDom.numeroExterior),
            numInterior: cleanDomField(rawDom.numeroInterior),
            colonia: cleanDomField(colonia),
            ciudad: cleanDomField(rawDom.localidad) || cleanDomField(rawDom.municipio) || '',
            municipio: cleanDomField(municipio),
            estado: cleanDomField(rawDom.entidadFederativa),
            entreCalle: cleanDomField(rawDom.entreCalle),
            yCalle: cleanDomField(rawDom.yCalle),
          };

          // Resolve ALL regímenes (not just the first)
          let regimenClaves: string[] = [];
          if (csf.regimenes?.length) {
            const { prisma: centralPrisma } = await import('../config/database.js');
            const allRegimenes = await centralPrisma.regimen.findMany({
              where: { activo: true },
              select: { clave: true, descripcion: true },
            });

            // Normalize for accent-insensitive comparison
            const norm = (s: string) => s.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().trim();

            for (const reg of csf.regimenes) {
              if (reg.fechaFin) continue; // Skip inactive regimenes
              const regNorm = norm(reg.nombre);
              // Score-based: prefer the match with the highest overlap
              let bestMatch: { clave: string; score: number } | null = null;
              for (const r of allRegimenes) {
                const catNorm = norm(r.descripcion);
                // Exact match or containment
                if (regNorm === catNorm || regNorm.includes(catNorm) || catNorm.includes(regNorm)) {
                  const score = catNorm.length; // Longer match = more specific = better
                  if (!bestMatch || score > bestMatch.score) {
                    bestMatch = { clave: r.clave, score };
                  }
                }
              }
              if (bestMatch) regimenClaves.push(bestMatch.clave);
            }
          }

          await pool.query(`
            UPDATE contribuyentes SET
              regimen_fiscal = COALESCE($2, regimen_fiscal),
              codigo_postal = COALESCE($3, codigo_postal),
              domicilio = COALESCE($4, domicilio)
            WHERE entidad_id = $1
          `, [
            contribuyenteId,
            regimenClaves.length > 0 ? regimenClaves.join(',') : null,
            domicilioMapped.codigoPostal || null,
            JSON.stringify(domicilioMapped),
          ]);
          console.log(`[CSF] Datos fiscales sincronizados para contribuyente ${contribuyenteId}: regímenes=${regimenClaves.join(',')}, CP=${domicilioMapped.codigoPostal}`);
        } catch (syncErr: any) {
          console.error(`[CSF] Error sincronizando datos fiscales:`, syncErr.message);
        }

        return rowToConstancia(rows[0]);
      })();

      return await Promise.race([resultPromise, timeoutPromise]);
    } finally {
      await browser.close();
    }
  } finally {
    try { unlinkSync(cerPath); } catch { /* ok */ }
    try { unlinkSync(keyPath); } catch { /* ok */ }
    try { rmdirSync(tempDir); } catch { /* ok */ }
  }
}
