import Facturapi from 'facturapi';
import type { Pool } from 'pg';
import { Credential } from '@nodecfdi/credentials/node';
import { env } from '../config/env.js';
import { encryptString, decryptToString } from './sat/sat-crypto.service.js';

function getUserClient(): Facturapi {
  if (!env.FACTURAPI_USER_KEY) throw new Error('FACTURAPI_USER_KEY no configurada');
  return new Facturapi(env.FACTURAPI_USER_KEY);
}

/**
 * Genera una Live Secret Key para una organización Facturapi via PUT idempotente.
 * Si la org ya tiene live key, devuelve la existente; si no, crea una nueva.
 * Endpoint oficial Facturapi: PUT /v2/organizations/{id}/apikeys/live
 */
async function generateLiveKey(orgId: string): Promise<string> {
  const userKey = env.FACTURAPI_USER_KEY!;
  const res = await fetch(`https://www.facturapi.io/v2/organizations/${orgId}/apikeys/live`, {
    method: 'PUT',
    headers: {
      'Authorization': `Bearer ${userKey}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({}),
  });
  if (!res.ok) {
    const errBody = await res.text().catch(() => '');
    throw new Error(`Facturapi PUT /apikeys/live falló (${res.status}): ${errBody}`);
  }
  const key = (await res.text()).replace(/"/g, '').trim();
  if (!key.startsWith('sk_live_')) {
    throw new Error(`Respuesta inesperada de Facturapi (no es sk_live_*): ${key.slice(0, 10)}...`);
  }
  return key;
}

/**
 * Cifra y persiste la Live Secret Key de una organización.
 * AES-256-GCM con la clave derivada de FIEL_ENCRYPTION_KEY.
 */
async function persistEncryptedKey(pool: Pool, orgId: string, plaintextKey: string): Promise<void> {
  const { encrypted, iv, tag } = encryptString(plaintextKey);
  await pool.query(
    `UPDATE facturapi_orgs SET api_key_enc = $1, api_key_iv = $2, api_key_tag = $3 WHERE facturapi_org_id = $4`,
    [encrypted, iv, tag, orgId],
  );
}

/**
 * Obtiene la Live Secret Key cacheada (descifra de BD) o la genera vía PUT
 * y la persiste si no existe (caso de orgs legacy creadas antes del refactor live).
 */
async function getOrgApiKey(pool: Pool, orgId: string): Promise<string> {
  const { rows } = await pool.query<{ api_key_enc: Buffer | null; api_key_iv: Buffer | null; api_key_tag: Buffer | null }>(
    `SELECT api_key_enc, api_key_iv, api_key_tag FROM facturapi_orgs WHERE facturapi_org_id = $1 LIMIT 1`,
    [orgId],
  );
  if (rows.length === 0) throw new Error(`Organización ${orgId} no encontrada en BD tenant`);

  const row = rows[0];
  if (row.api_key_enc && row.api_key_iv && row.api_key_tag) {
    return decryptToString(row.api_key_enc, row.api_key_iv, row.api_key_tag);
  }

  // Org legacy sin live key cacheada — generar y guardar (idempotente).
  const apiKey = await generateLiveKey(orgId);
  await persistEncryptedKey(pool, orgId, apiKey);
  return apiKey;
}

export async function createOrgContribuyente(
  pool: Pool,
  contribuyenteId: string,
  nombre: string
): Promise<{ orgId: string; reused?: boolean; recreated?: boolean }> {
  const { rows: existing } = await pool.query(
    'SELECT facturapi_org_id FROM facturapi_orgs WHERE contribuyente_id = $1',
    [contribuyenteId]
  );
  const client = getUserClient();

  // Caso 1: hay fila local → verificar si la org sigue viva en Facturapi.
  // Si existe en ambos lados, idempotente (devolver la existente).
  // Si existe solo local pero Facturapi no la tiene (eliminada allá, API key
  // cambió, etc.), recrear y actualizar el FK local — desbloquea el flujo
  // de CSD que si no se quedaba trabado.
  if (existing.length > 0) {
    const existingId = existing[0].facturapi_org_id;
    try {
      await client.organizations.retrieve(existingId);
      // Idempotente: si existe en ambos lados, asegurar que la live key está
      // cacheada (puede faltar en orgs legacy creadas antes del refactor live).
      await ensureLiveKeyCached(pool, existingId);
      // Backfill: si la org fue creada antes de este fix, sincronizar datos fiscales.
      await updateOrgLegalOnCreate(pool, existingId, contribuyenteId);
      return { orgId: existingId, reused: true };
    } catch {
      const org = await client.organizations.create({ name: nombre });
      await pool.query(
        'UPDATE facturapi_orgs SET facturapi_org_id = $2, csd_uploaded = false, active = true, api_key_enc = NULL, api_key_iv = NULL, api_key_tag = NULL WHERE contribuyente_id = $1',
        [contribuyenteId, org.id]
      );
      // Eager: generar y cachear live key para que la org quede lista para emitir.
      await ensureLiveKeyCached(pool, org.id);
      await updateOrgLegalOnCreate(pool, org.id, contribuyenteId);
      return { orgId: org.id, recreated: true };
    }
  }

  // Caso 2: no hay fila local → crear fresh.
  const org = await client.organizations.create({ name: nombre });
  await pool.query(
    'INSERT INTO facturapi_orgs (contribuyente_id, facturapi_org_id) VALUES ($1, $2)',
    [contribuyenteId, org.id]
  );
  // Eager: generar y cachear live key inmediatamente tras crear la org.
  await ensureLiveKeyCached(pool, org.id);
  await updateOrgLegalOnCreate(pool, org.id, contribuyenteId);
  return { orgId: org.id };
}

/**
 * Garantiza que la org tiene su Live Secret Key cifrada en BD. Si ya existe,
 * no-op. Si no, hace PUT live y la persiste. Idempotente — el endpoint
 * Facturapi PUT /apikeys/live es idempotente, devuelve la misma key si ya
 * existe en su lado.
 */
async function ensureLiveKeyCached(pool: Pool, orgId: string): Promise<void> {
  const { rows } = await pool.query(
    `SELECT 1 FROM facturapi_orgs WHERE facturapi_org_id = $1 AND api_key_enc IS NOT NULL LIMIT 1`,
    [orgId],
  );
  if (rows.length > 0) return;
  const apiKey = await generateLiveKey(orgId);
  await persistEncryptedKey(pool, orgId, apiKey);
}

interface ContribuyenteFiscalData {
  rfc: string;
  razon_social: string | null;
  regimen_fiscal: string | null;
  codigo_postal: string | null;
  domicilio: any;
}

async function fetchContribuyenteFiscalData(
  pool: Pool,
  contribuyenteId: string,
): Promise<ContribuyenteFiscalData> {
  const { rows } = await pool.query<ContribuyenteFiscalData>(
    `SELECT c.rfc, r.razon_social, c.regimen_fiscal, c.codigo_postal, c.domicilio
       FROM contribuyentes c
       LEFT JOIN rfcs r ON UPPER(r.rfc) = UPPER(c.rfc)
      WHERE c.entidad_id = $1`,
    [contribuyenteId],
  );
  if (rows.length === 0) throw new Error('Contribuyente no encontrado');
  return rows[0];
}

async function buildLegalPayload(
  contrib: ContribuyenteFiscalData,
  chosenTaxSystem: string,
  currentLegal?: any,
) {
  const domicilio = (contrib.domicilio || {}) as any;
  return {
    name: contrib.razon_social || currentLegal?.name || '',
    legal_name: contrib.razon_social || currentLegal?.legal_name || '',
    tax_system: chosenTaxSystem,
    address: {
      street: domicilio.calle || currentLegal?.address?.street || '',
      exterior: domicilio.numExterior || currentLegal?.address?.exterior || '',
      interior: domicilio.numInterior || currentLegal?.address?.interior || '',
      neighborhood: domicilio.colonia || currentLegal?.address?.neighborhood || '',
      city: domicilio.ciudad || currentLegal?.address?.city || '',
      municipality: domicilio.municipio || currentLegal?.address?.municipality || '',
      state: domicilio.estado || currentLegal?.address?.state || '',
      zip: contrib.codigo_postal || domicilio.codigoPostal || currentLegal?.address?.zip || '',
    },
  };
}

async function putOrgLegal(orgId: string, payload: any): Promise<void> {
  const userKey = env.FACTURAPI_USER_KEY;
  if (!userKey) throw new Error('FACTURAPI_USER_KEY no configurada');
  const putRes = await fetch(`https://www.facturapi.io/v2/organizations/${orgId}/legal`, {
    method: 'PUT',
    headers: {
      'Authorization': `Bearer ${userKey}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(payload),
  });
  if (!putRes.ok) {
    const errText = await putRes.text();
    throw new Error(
      `Error actualizando datos fiscales de la organización Facturapi (${putRes.status}): ${errText}`,
    );
  }
}

/**
 * Actualiza los datos fiscales de una organización Facturapi recién creada
 * usando la información del contribuyente. Se usa el primer régimen fiscal
 * registrado. No-op si no hay régimen fiscal o razón social que setear.
 */
async function updateOrgLegalOnCreate(
  pool: Pool,
  orgId: string,
  contribuyenteId: string,
): Promise<void> {
  try {
    const contrib = await fetchContribuyenteFiscalData(pool, contribuyenteId);
    const allowed = (contrib.regimen_fiscal || '')
      .split(',')
      .map(s => s.trim())
      .filter(Boolean);
    if (!allowed.length || !contrib.razon_social) {
      // Datos incompletos: no fallar la creación de la org, solo loguear silenciosamente.
      return;
    }
    const payload = await buildLegalPayload(contrib, allowed[0]);
    await putOrgLegal(orgId, payload);
  } catch {
    // No bloquear la creación de la org si el update legal falla.
  }
}

export async function getOrgStatusContribuyente(
  pool: Pool,
  contribuyenteId: string
): Promise<{ configured: boolean; orgId?: string; legalName?: string; hasCsd?: boolean }> {
  const { rows } = await pool.query(
    'SELECT facturapi_org_id, csd_uploaded FROM facturapi_orgs WHERE contribuyente_id = $1 AND active = true',
    [contribuyenteId]
  );
  if (rows.length === 0) return { configured: false };

  try {
    const client = getUserClient();
    const org = await client.organizations.retrieve(rows[0].facturapi_org_id);
    return {
      configured: true,
      orgId: org.id,
      legalName: org.legal?.name || undefined,
      hasCsd: !!org.certificate?.has_certificate,
    };
  } catch {
    return { configured: false };
  }
}

export async function uploadCsdContribuyente(
  pool: Pool,
  contribuyenteId: string,
  cerFile: string,
  keyFile: string,
  password: string
): Promise<{ success: boolean; message: string }> {
  const { rows } = await pool.query<{ facturapi_org_id: string; rfc: string }>(
    `SELECT fo.facturapi_org_id, c.rfc
     FROM facturapi_orgs fo
     JOIN contribuyentes c ON c.entidad_id = fo.contribuyente_id
     WHERE fo.contribuyente_id = $1 AND fo.active = true`,
    [contribuyenteId]
  );
  if (rows.length === 0) throw new Error('Primero debe crearse la organización Facturapi del contribuyente');

  const { facturapi_org_id, rfc: contribuyenteRfc } = rows[0];

  // Validación preventiva: que el certificado sea CSD (no FIEL), que el RFC
  // coincida con el contribuyente y que no esté vencido. Facturapi también
  // valida, pero su mensaje de error es poco específico ("Certificado no
  // válido") — el nuestro dice exactamente qué pasa.
  const cerData = Buffer.from(cerFile, 'base64');
  const keyData = Buffer.from(keyFile, 'base64');

  let credential: Credential;
  try {
    credential = Credential.create(cerData.toString('binary'), keyData.toString('binary'), password);
  } catch {
    return { success: false, message: 'Los archivos .cer/.key no son válidos o la contraseña es incorrecta' };
  }

  // Debe ser CSD (sello digital para facturar), no FIEL (e.firma para trámites).
  if (credential.isFiel()) {
    return { success: false, message: 'El certificado es una FIEL (e.firma), no un CSD. Sube el Certificado de Sello Digital.' };
  }

  const certRfc = credential.certificate().rfc().toUpperCase();
  if (certRfc !== contribuyenteRfc.toUpperCase()) {
    return {
      success: false,
      message: `El RFC del CSD (${certRfc}) no coincide con el del contribuyente (${contribuyenteRfc}). Verifica que estés subiendo los archivos correctos.`,
    };
  }

  const validUntil = new Date(String(credential.certificate().validToDateTime()));
  if (new Date() > validUntil) {
    return { success: false, message: `El CSD está vencido desde ${validUntil.toLocaleDateString('es-MX')}. Solicita al SAT uno nuevo.` };
  }

  const client = getUserClient();
  try {
    await client.organizations.uploadCertificate(
      facturapi_org_id,
      cerData,
      keyData,
      password,
    );
    await pool.query(
      'UPDATE facturapi_orgs SET csd_uploaded = true WHERE contribuyente_id = $1',
      [contribuyenteId]
    );
    return { success: true, message: 'CSD subido correctamente' };
  } catch (error: any) {
    return { success: false, message: error.message || 'Error al subir CSD a Facturapi' };
  }
}

export async function getOrgClientContribuyente(
  pool: Pool,
  contribuyenteId: string
): Promise<Facturapi> {
  const { rows } = await pool.query(
    'SELECT facturapi_org_id FROM facturapi_orgs WHERE contribuyente_id = $1 AND active = true',
    [contribuyenteId]
  );
  if (rows.length === 0) throw new Error('Contribuyente no tiene organización Facturapi configurada');

  const apiKey = await getOrgApiKey(pool, rows[0].facturapi_org_id);
  return new Facturapi(apiKey);
}

export async function cancelInvoiceContribuyente(
  pool: Pool,
  contribuyenteId: string,
  facturapiId: string,
  motive: '01' | '02' | '03' | '04' = '02',
  substitution?: string,
): Promise<any> {
  const client = await getOrgClientContribuyente(pool, contribuyenteId);
  const cancelData: any = { motive };
  if (motive === '01' && substitution) cancelData.substitution = substitution;
  return client.invoices.cancel(facturapiId, cancelData);
}

function streamToBuffer(stream: any): Promise<Buffer> {
  return new Promise((resolve, reject) => {
    if (Buffer.isBuffer(stream)) return resolve(stream);
    const chunks: Buffer[] = [];
    stream.on('data', (chunk: Buffer) => chunks.push(chunk));
    stream.on('end', () => resolve(Buffer.concat(chunks)));
    stream.on('error', reject);
  });
}

export async function downloadPdfContribuyente(
  pool: Pool,
  contribuyenteId: string,
  facturapiId: string,
): Promise<Buffer> {
  const client = await getOrgClientContribuyente(pool, contribuyenteId);
  const stream = await client.invoices.downloadPdf(facturapiId);
  return streamToBuffer(stream);
}

export async function downloadXmlContribuyente(
  pool: Pool,
  contribuyenteId: string,
  facturapiId: string,
): Promise<Buffer> {
  const client = await getOrgClientContribuyente(pool, contribuyenteId);
  const stream = await client.invoices.downloadXml(facturapiId);
  return streamToBuffer(stream);
}

export async function sendInvoiceByEmailContribuyente(
  pool: Pool,
  contribuyenteId: string,
  facturapiId: string,
  email: string,
): Promise<void> {
  const client = await getOrgClientContribuyente(pool, contribuyenteId);
  await client.invoices.sendByEmail(facturapiId, { email });
}

export async function createInvoiceContribuyente(
  pool: Pool,
  contribuyenteId: string,
  data: any
): Promise<any> {
  const client = await getOrgClientContribuyente(pool, contribuyenteId);

  // Create/update customer in Facturapi
  const isForiegn = !!data.customer?.country && data.customer.country !== 'MEX';
  const customerData: any = {
    legal_name: data.customer?.legalName,
    tax_id: data.customer?.taxId,
    email: data.customer?.email,
    address: { zip: data.customer?.zip, ...(isForiegn ? { country: data.customer.country } : {}) },
  };
  if (!isForiegn && data.customer?.taxSystem) customerData.tax_system = data.customer.taxSystem;

  let customerId: string;
  try {
    const existing = await client.customers.list({ search: data.customer?.taxId });
    const match = existing.data?.find((c: any) => c.tax_id === data.customer?.taxId);
    if (match) {
      await client.customers.update(match.id, customerData);
      customerId = match.id;
    } else {
      const created = await client.customers.create(customerData);
      customerId = created.id;
    }
  } catch {
    const created = await client.customers.create(customerData);
    customerId = created.id;
  }

  // Build invoice payload (mirrors createInvoice logic in facturapi.service.ts)
  const tipo = data.type || 'I';
  const invoicePayload: any = { customer: customerId };

  if (tipo !== 'I') invoicePayload.type = tipo;

  if (data.items?.length) {
    invoicePayload.items = data.items.map((item: any) => ({
      quantity: item.quantity,
      product: {
        description: item.description,
        product_key: item.productKey,
        unit_key: item.unitKey || 'E48',
        unit_name: item.unitName || 'Servicio',
        price: item.price,
        tax_included: item.taxIncluded ?? false,
        taxes: item.taxes?.map((t: any) => ({
          type: t.type,
          rate: t.rate,
          factor: t.factor || 'Tasa',
          ...(t.withholding ? { withholding: true } : {}),
        })) || [{ type: 'IVA', rate: 0.16 }],
      },
      ...(data.cuentaPredial ? { property_tax_account: data.cuentaPredial } : {}),
    }));
  }

  if (tipo === 'I' || tipo === 'E') {
    invoicePayload.use = data.use || 'G01';
    invoicePayload.payment_form = data.paymentForm || '99';
    invoicePayload.payment_method = data.paymentMethod || 'PUE';
    invoicePayload.currency = data.currency || 'MXN';
    if (data.exchangeRate && data.currency !== 'MXN') invoicePayload.exchange = data.exchangeRate;
    if (data.conditions) invoicePayload.conditions = data.conditions;
  }

  if (data.series) invoicePayload.series = data.series;
  if (data.folioNumber) invoicePayload.folio_number = data.folioNumber;
  if (data.fechaEmision) invoicePayload.date = data.fechaEmision;

  if (data.relatedDocuments?.length) {
    // Estructura SAT 4.0: agrupa N uuids por tipo de relación. Acepta tanto
    // el formato nuevo {relationship, uuids[]} como el legacy {relationship,
    // uuid} para compat durante transición de callers frontend.
    invoicePayload.related_documents = data.relatedDocuments.map((r: any) => ({
      relationship: r.relationship,
      documents: Array.isArray(r.uuids) ? r.uuids : (r.uuid ? [r.uuid] : []),
    }));
  }

  if (data.complements?.length) invoicePayload.complements = data.complements;
  if (data.global) invoicePayload.global = data.global;

  // Régimen fiscal del emisor: Facturapi NO acepta override per-invoice via
  // campo `issuer` (rechaza con "issuer is not allowed"). La única forma es
  // actualizar el `legal.tax_system` de la organización antes del emit.
  // Para contribuyentes con múltiples regímenes, esto significa un sync en
  // cada emit cuando el seleccionado difiere del actual en la org.
  if (data.issuerTaxSystem) {
    const { rows } = await pool.query<{ facturapi_org_id: string }>(
      `SELECT facturapi_org_id FROM facturapi_orgs WHERE contribuyente_id = $1 AND active = true`,
      [contribuyenteId],
    );
    if (rows.length > 0) {
      await ensureOrgLegalForEmit(pool, contribuyenteId, rows[0].facturapi_org_id, data.issuerTaxSystem);
    }
  }

  return client.invoices.create(invoicePayload);
}

/**
 * Sincroniza los datos fiscales de la organización Facturapi con la
 * información del contribuyente, usando el régimen seleccionado. Se llama
 * antes de cada emit cuando el user elige un régimen en el form, porque
 * Facturapi toma el TaxSystem del CFDI del `legal.tax_system` de la org
 * (no acepta override per-invoice). No-op si el `legal` ya coincide.
 */
async function ensureOrgLegalForEmit(
  pool: Pool,
  contribuyenteId: string,
  orgId: string,
  chosenTaxSystem: string,
): Promise<void> {
  const userKey = env.FACTURAPI_USER_KEY;
  if (!userKey) throw new Error('FACTURAPI_USER_KEY no configurada');

  const contrib = await fetchContribuyenteFiscalData(pool, contribuyenteId);

  // Validar que el régimen elegido esté entre los registrados del contrib
  const allowed = (contrib.regimen_fiscal || '')
    .split(',')
    .map(s => s.trim())
    .filter(Boolean);
  if (allowed.length > 0 && !allowed.includes(chosenTaxSystem)) {
    throw new Error(
      `El régimen ${chosenTaxSystem} no está registrado para este contribuyente ` +
        `(registrados: ${allowed.join(', ')})`,
    );
  }

  // Leer el legal actual de la org en Facturapi
  const getRes = await fetch(`https://www.facturapi.io/v2/organizations/${orgId}`, {
    headers: { 'Authorization': `Bearer ${userKey}` },
  });
  if (!getRes.ok) {
    throw new Error(`No se pudo leer organización Facturapi (${getRes.status})`);
  }
  const org = (await getRes.json()) as any;
  const currentLegal = org.legal || {};

  // Si el tax_system ya coincide y la razón social está seteada, no tocar
  // (evita updates innecesarios con latencia extra).
  if (
    currentLegal.tax_system === chosenTaxSystem &&
    currentLegal.legal_name &&
    currentLegal.legal_name === contrib.razon_social
  ) {
    return;
  }

  const payload = await buildLegalPayload(contrib, chosenTaxSystem, currentLegal);
  await putOrgLegal(orgId, payload);
}

// ── Personalización (logo, color) per-contribuyente ──

export async function getCustomizationContribuyente(
  pool: Pool,
  contribuyenteId: string,
): Promise<{ logoUrl?: string; color?: string } | null> {
  const { rows } = await pool.query<{ facturapi_org_id: string }>(
    'SELECT facturapi_org_id FROM facturapi_orgs WHERE contribuyente_id = $1 AND active = true',
    [contribuyenteId],
  );
  if (rows.length === 0) return null;

  const userClient = getUserClient();
  try {
    const org = await userClient.organizations.retrieve(rows[0].facturapi_org_id);
    return {
      logoUrl: org.customization?.has_logo ? (org.logo_url ?? undefined) : undefined,
      color: org.customization?.color || undefined,
    };
  } catch { return null; }
}

export async function uploadLogoContribuyente(
  pool: Pool,
  contribuyenteId: string,
  logoBase64: string,
): Promise<{ success: boolean; message: string }> {
  const { rows } = await pool.query<{ facturapi_org_id: string }>(
    'SELECT facturapi_org_id FROM facturapi_orgs WHERE contribuyente_id = $1 AND active = true',
    [contribuyenteId],
  );
  if (rows.length === 0) throw new Error('Organización no configurada');

  const userClient = getUserClient();
  try {
    const buffer = Buffer.from(logoBase64, 'base64');
    await userClient.organizations.uploadLogo(rows[0].facturapi_org_id, buffer);
    return { success: true, message: 'Logo subido correctamente' };
  } catch (error: any) {
    return { success: false, message: error.message || 'Error al subir logo' };
  }
}

export async function updateColorContribuyente(
  pool: Pool,
  contribuyenteId: string,
  color: string,
): Promise<{ success: boolean; message: string }> {
  const { rows } = await pool.query<{ facturapi_org_id: string }>(
    'SELECT facturapi_org_id FROM facturapi_orgs WHERE contribuyente_id = $1 AND active = true',
    [contribuyenteId],
  );
  if (rows.length === 0) throw new Error('Organización no configurada');

  const userClient = getUserClient();
  try {
    await userClient.organizations.updateCustomization(rows[0].facturapi_org_id, { color });
    return { success: true, message: 'Color actualizado correctamente' };
  } catch (error: any) {
    return { success: false, message: error.message || 'Error al actualizar color' };
  }
}
