import type { Pool } from 'pg';

// Mapeo: impuesto de la declaración → reglas para matchear obligaciones del
// contribuyente. `include` son substrings que DEBE contener el nombre de la
// obligación; `exclude` son substrings que NO debe contener. El exclude
// resuelve ambigüedades como "IVA" matcheando "Declaración de proveedores
// de IVA" (DIOT) — cuando subes pago de IVA normal, NO debe cerrar DIOT.
const IMPUESTO_A_OBLIGACION_KEYWORDS: Record<string, { include: string[]; exclude: string[] }> = {
  IVA: { include: ['iva'], exclude: ['diot', 'proveedores de iva', 'informativa'] },
  ISR: { include: ['isr'], exclude: ['retenciones', 'asimilados a salarios'] },
  IEPS: { include: ['ieps'], exclude: [] },
  SUELDOS: { include: ['sueldos', 'salarios', 'nómina'], exclude: [] },
  DIOT: { include: ['diot', 'proveedores de iva'], exclude: [] },
  OTRO: { include: [], exclude: [] },
};

/**
 * After uploading a declaration, find matching obligations for the contribuyente
 * and mark them as completed for the period. Also resolve the ob-* alerts.
 *
 * Guarda `declaracion_id` en `obligacion_periodos` para que la UI pueda
 * mostrar "Completada via Declaración #123" y permitir cross-link. Si la
 * declaración se borra, el FK pasa a NULL (ON DELETE SET NULL) y el
 * periodo sigue marcado completado — el usuario decidirá si re-abrirlo
 * manualmente.
 */
async function completarObligacionesPorDeclaracion(
  pool: Pool,
  contribuyenteId: string,
  impuestos: string[],
  periodo: string,
  /** UUID del usuario que subió la declaración (obligacion_periodos.completada_por es uuid). */
  completadaPor: string,
  declaracionId: number,
  /** Periodicidad de la declaración. Si no se provee, se asume 'mensual'. */
  periodicidad: string = 'mensual',
): Promise<number> {
  // Get active obligations for this contribuyente (incluye frecuencia para filtrar)
  const { rows: obligaciones } = await pool.query<{ id: string; nombre: string; frecuencia: string | null }>(
    `SELECT id, nombre, frecuencia FROM obligaciones_contribuyente WHERE contribuyente_id = $1 AND activa = true`,
    [contribuyenteId],
  );

  let count = 0;

  for (const impuesto of impuestos) {
    const rules = IMPUESTO_A_OBLIGACION_KEYWORDS[impuesto];
    if (!rules || rules.include.length === 0) continue;

    for (const ob of obligaciones) {
      const nombreLower = ob.nombre.toLowerCase();
      const matches = rules.include.some(kw => nombreLower.includes(kw))
        && !rules.exclude.some(kw => nombreLower.includes(kw));
      if (!matches) continue;

      // Filtro por periodicidad/frecuencia: una declaración mensual no debe
      // cerrar obligaciones anuales del mismo impuesto (ej. ISR mensual no
      // cubre "Declaración anual de ISR"). Si la obligación tiene frecuencia
      // explícita y no coincide con la periodicidad de la declaración, skip.
      // `eventual` obligaciones no se tocan automáticamente.
      const obFrec = (ob.frecuencia || '').toLowerCase();
      if (obFrec === 'eventual') continue;
      if (obFrec && obFrec !== periodicidad.toLowerCase()) continue;

      // Mark obligation as completed for this period, with FK a la declaración
      await pool.query(`
        INSERT INTO obligacion_periodos (obligacion_id, periodo, completada, completada_at, completada_por, notas, declaracion_id)
        VALUES ($1, $2, true, now(), $3, $4, $5)
        ON CONFLICT (obligacion_id, periodo)
        DO UPDATE SET completada = true, completada_at = now(), completada_por = $3, declaracion_id = $5
      `, [ob.id, periodo, completadaPor, `Declaración ${impuesto} subida`, declaracionId]);

      // Resolve the ob-* alert for this obligation+period
      await pool.query(
        `UPDATE alertas SET resuelta = true WHERE tipo = $1 AND resuelta = false`,
        [`ob-${ob.id}-${periodo}`],
      );

      count++;
    }
  }

  return count;
}

/**
 * Declaraciones provisionales: PDF subido por el contador con la declaración
 * presentada al SAT + opcionalmente comprobante de pago. Al subir, se marcan
 * como resueltas las alertas correspondientes en la tabla `alertas` del tenant.
 *
 * El método legacy "marcar como realizado" desde /alertas sigue funcionando
 * para usuarios que no quieran subir el documento. Esta automatización es
 * adicional, no reemplaza.
 */

export type Impuesto = 'IVA' | 'ISR' | 'IEPS' | 'SUELDOS' | 'DIOT' | 'OTRO';

export type Periodicidad = 'mensual' | 'bimestral' | 'trimestral' | 'semestral' | 'anual';

export interface DeclaracionRow {
  id: number;
  año: number;
  mes: number;
  tipo: 'normal' | 'complementaria';
  periodicidad: Periodicidad;
  impuestos: string[];
  montoPago: number | null;
  pdfFilename: string | null;
  ligaPagoFilename: string | null;
  pdfPagoFilename: string | null;
  pagadoAt: string | null;
  creadoPor: string | null;
  notas: string | null;
  createdAt: string;
  updatedAt: string;
  tieneLigaPago: boolean;
  tienePagoPdf: boolean;
}

// Mapeo Impuesto → prefijo de tipo de alerta (debe coincidir con
// EVENTO_A_ALERTA en alertas-manuales.service.ts).
const IMPUESTO_A_PREFIJO_DECL: Record<string, string[]> = {
  IVA: ['decl-iva'],
  ISR: ['decl-isr'],
  IEPS: ['decl-ieps'],
  SUELDOS: ['decl-sueldos'],
  DIOT: ['diot'],
  OTRO: [],
};
const IMPUESTO_A_PREFIJO_PAGO: Record<string, string[]> = {
  IVA: ['pago-iva'],
  ISR: ['pago-isr'],
  IEPS: ['pago-ieps'],
  SUELDOS: [], // sueldos solo es declaración informativa, no tiene pago provisional
  DIOT: [],
  OTRO: [],
};

/**
 * Marca como resueltas las alertas cuyo `tipo` empieza con cualquiera de los
 * prefijos dados Y cuyo `fecha_vencimiento` cae en el mes/año dados.
 * Idempotente: re-llamar no crea efectos secundarios extra.
 */
async function resolverAlertasPorPeriodo(
  pool: Pool,
  prefijos: string[],
  año: number,
  mes: number,
): Promise<number> {
  if (prefijos.length === 0) return 0;
  // El tipo es `prefijo-YYYY-MM-DD`. Buscar por LIKE prefijo-año-mes-%
  const mesStr = String(mes).padStart(2, '0');
  const conditions = prefijos.map((_, i) => `tipo LIKE $${i + 1}`).join(' OR ');
  const params = prefijos.map(p => `${p}-${año}-${mesStr}-%`);
  const { rowCount } = await pool.query(
    `UPDATE alertas SET resuelta = true
     WHERE (${conditions}) AND resuelta = false`,
    params,
  );
  return rowCount ?? 0;
}

function rowToDeclaracion(r: any): DeclaracionRow {
  return {
    id: r.id,
    año: r.año,
    mes: r.mes,
    tipo: r.tipo,
    periodicidad: r.periodicidad || 'mensual',
    impuestos: r.impuestos || [],
    montoPago: r.monto_pago != null ? Number(r.monto_pago) : null,
    pdfFilename: r.pdf_filename,
    ligaPagoFilename: r.pdf_liga_pago_filename,
    pdfPagoFilename: r.pdf_pago_filename,
    pagadoAt: r.pagado_at?.toISOString() ?? null,
    creadoPor: r.creado_por,
    notas: r.notas,
    createdAt: r.created_at.toISOString(),
    updatedAt: r.updated_at.toISOString(),
    tieneLigaPago: !!r.pdf_liga_pago_filename,
    tienePagoPdf: !!r.pdf_pago_filename,
  };
}

export async function listDeclaraciones(
  pool: Pool,
  fechaDesde?: string,
  fechaHasta?: string,
  contribuyenteId?: string | null,
): Promise<DeclaracionRow[]> {
  const conditions: string[] = [];
  const params: unknown[] = [];

  if (fechaDesde) {
    params.push(fechaDesde);
    conditions.push(`created_at >= $${params.length}::date`);
  }
  if (fechaHasta) {
    params.push(fechaHasta);
    conditions.push(`created_at < ($${params.length}::date + interval '1 day')`);
  }
  if (contribuyenteId) {
    // Sanitize UUID (hex + hyphens only)
    const safe = contribuyenteId.replace(/[^a-f0-9-]/gi, '');
    if (safe) {
      params.push(safe);
      conditions.push(`contribuyente_id = $${params.length}`);
    }
  }

  const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';

  const { rows } = await pool.query(
    `SELECT id, año, mes, tipo, periodicidad, impuestos, monto_pago, pdf_filename,
            pdf_liga_pago_filename, pdf_pago_filename, pagado_at, creado_por, notas,
            created_at, updated_at
     FROM declaraciones_provisionales
     ${where}
     ORDER BY created_at DESC, año DESC, mes DESC`,
    params,
  );
  return rows.map(rowToDeclaracion);
}

export async function createDeclaracion(
  pool: Pool,
  data: {
    año: number;
    mes: number;
    tipo: 'normal' | 'complementaria';
    periodicidad?: Periodicidad;
    impuestos: string[];
    montoPago?: number | null;
    pdfBase64: string;        // PDF de la declaración (base64)
    pdfFilename: string;
    ligaPagoBase64?: string;  // PDF de la liga de pago (opcional, base64)
    ligaPagoFilename?: string;
    notas?: string;
    /** Email del usuario (para declaraciones_provisionales.creado_por VARCHAR). */
    creadoPor: string;
    /** UUID del usuario (para obligacion_periodos.completada_por UUID). Opcional. */
    creadoPorUserId?: string;
    contribuyenteId?: string;
  },
): Promise<{ declaracion: DeclaracionRow; alertasResueltas: number }> {
  const buf = Buffer.from(data.pdfBase64, 'base64');
  const ligaBuf = data.ligaPagoBase64 ? Buffer.from(data.ligaPagoBase64, 'base64') : null;
  const periodicidad = data.periodicidad || 'mensual';
  const montoPago = data.montoPago ?? null;
  // If monto_pago is exactly 0, auto-mark as paid (no payment receipt needed)
  const pagadoAt = montoPago === 0 ? new Date() : null;

  try {
    const { rows } = await pool.query(
      `INSERT INTO declaraciones_provisionales
        (año, mes, tipo, periodicidad, impuestos, monto_pago, pdf_declaracion, pdf_filename,
         pdf_liga_pago, pdf_liga_pago_filename, notas, creado_por, pagado_at, contribuyente_id)
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
       RETURNING id, año, mes, tipo, periodicidad, impuestos, monto_pago, pdf_filename,
                 pdf_liga_pago_filename, pdf_pago_filename, pagado_at, creado_por, notas,
                 created_at, updated_at`,
      [data.año, data.mes, data.tipo, periodicidad, data.impuestos, montoPago,
       buf, data.pdfFilename, ligaBuf, data.ligaPagoFilename ?? null,
       data.notas ?? null, data.creadoPor, pagadoAt, data.contribuyenteId ?? null],
    );

    const declaracion = rowToDeclaracion(rows[0]);

    // Auto-resolver alertas. Reglas:
    //  - tipo='normal': resuelve alertas de declaración (decl-*) del mes.
    //    El pago se resuelve por separado al subir comprobante.
    //  - tipo='complementaria': sustituye a la normal en términos de
    //    obligación de pago — al subirla se resuelven AMBAS (decl-* y
    //    pago-*) porque el cliente pagará usando la complementaria,
    //    no la normal. La alerta de declaración ya estaría resuelta
    //    si la normal se subió antes; el resolver es idempotente.
    const prefijosDecl = data.impuestos.flatMap(i => IMPUESTO_A_PREFIJO_DECL[i] || []);
    let alertasResueltas = await resolverAlertasPorPeriodo(pool, prefijosDecl, data.año, data.mes);
    if (data.tipo === 'complementaria' || montoPago === 0) {
      // complementaria: sustituye normal para pago → resolver ambas
      // monto 0: nada que pagar → resolver alertas de pago también
      const prefijosPago = data.impuestos.flatMap(i => IMPUESTO_A_PREFIJO_PAGO[i] || []);
      alertasResueltas += await resolverAlertasPorPeriodo(pool, prefijosPago, data.año, data.mes);
    }

    // Auto-complete obligaciones del contribuyente SOLO si la declaración
    // también cubre el pago (complementaria sustituye a la normal para el
    // pago; monto=0 significa "nada que pagar"). Una declaración normal con
    // monto>0 solo presenta el acuse — la obligación de pago sigue abierta
    // y se marca completada hasta que se suba el comprobante via
    // `uploadComprobantePago`. Esto mantiene las alertas `pago-*` y `ob-*`
    // visibles hasta que realmente se cierre el ciclo.
    const cubrePago = data.tipo === 'complementaria' || montoPago === 0;
    if (data.contribuyenteId && cubrePago) {
      if (!data.creadoPorUserId) {
        console.warn('[createDeclaracion] Sin creadoPorUserId — no se auto-completan obligaciones del contribuyente');
      } else {
        const periodo = `${data.año}-${String(data.mes).padStart(2, '0')}`;
        alertasResueltas += await completarObligacionesPorDeclaracion(
          pool, data.contribuyenteId, data.impuestos, periodo, data.creadoPorUserId, declaracion.id, periodicidad,
        );
      }
    }

    return { declaracion, alertasResueltas };
  } catch (err: any) {
    if (err?.code === '23505') {
      throw new Error(`Ya existe una declaración tipo "normal" para ${data.mes}/${data.año}. Solo se permite una normal por mes; agrega una complementaria si necesitas corregirla.`);
    }
    throw err;
  }
}

export async function uploadComprobantePago(
  pool: Pool,
  id: number,
  data: {
    pdfBase64: string;
    pdfFilename: string;
    /** UUID del usuario que sube el comprobante (para obligacion_periodos.completada_por). */
    uploadedByUserId?: string;
  },
): Promise<{ declaracion: DeclaracionRow; alertasResueltas: number }> {
  const buf = Buffer.from(data.pdfBase64, 'base64');

  const { rows } = await pool.query(
    `UPDATE declaraciones_provisionales
     SET pdf_pago = $1, pdf_pago_filename = $2, pagado_at = NOW(), updated_at = NOW()
     WHERE id = $3
     RETURNING id, año, mes, tipo, periodicidad, impuestos, pdf_filename, pdf_liga_pago_filename,
               pdf_pago_filename, pagado_at, creado_por, notas, created_at, updated_at,
               contribuyente_id`,
    [buf, data.pdfFilename, id],
  );

  if (rows.length === 0) throw new Error('Declaración no encontrada');
  const row = rows[0];
  const declaracion = rowToDeclaracion(row);

  // Auto-resolver alertas de pago para los impuestos del periodo
  const prefijosPago = declaracion.impuestos.flatMap(i => IMPUESTO_A_PREFIJO_PAGO[i] || []);
  let alertasResueltas = await resolverAlertasPorPeriodo(pool, prefijosPago, declaracion.año, declaracion.mes);

  // Al subirse el comprobante de pago, la obligación ahora SÍ está completada
  // (declaración + pago). Marcar `obligacion_periodos.completada=true` y
  // resolver los `ob-*` alerts. Requires contribuyenteId (guardado en la
  // declaración) y userId (del caller).
  if (row.contribuyente_id && data.uploadedByUserId) {
    const periodo = `${declaracion.año}-${String(declaracion.mes).padStart(2, '0')}`;
    const periodicidad = row.periodicidad || 'mensual';
    alertasResueltas += await completarObligacionesPorDeclaracion(
      pool, row.contribuyente_id, declaracion.impuestos, periodo, data.uploadedByUserId, declaracion.id, periodicidad,
    );
  }

  return { declaracion, alertasResueltas };
}

export async function deleteDeclaracion(pool: Pool, id: number): Promise<void> {
  const { rowCount } = await pool.query(
    `DELETE FROM declaraciones_provisionales WHERE id = $1`,
    [id],
  );
  if (rowCount === 0) throw new Error('Declaración no encontrada');
}

/**
 * Cleanup: borra declaraciones con created_at < hoy - 5 años. Cumple con
 * el plazo de retención del Art. 30 del CFF (contabilidad por 5 años).
 * Llamado por cron diario. Idempotente: si no hay viejas, no-op.
 *
 * Se ejecuta por-tenant (caller pasa el pool). Returns { deleted } para log.
 */
export async function purgeDeclaracionesAntiguas(pool: Pool): Promise<{ deleted: number }> {
  const { rowCount } = await pool.query(
    `DELETE FROM declaraciones_provisionales
     WHERE created_at < NOW() - INTERVAL '5 years'`,
  );
  return { deleted: rowCount ?? 0 };
}

export async function getDeclaracionPdf(
  pool: Pool,
  id: number,
  variant: 'declaracion' | 'liga' | 'pago',
): Promise<{ buffer: Buffer; filename: string } | null> {
  const col =
    variant === 'declaracion' ? 'pdf_declaracion' :
    variant === 'liga' ? 'pdf_liga_pago' : 'pdf_pago';
  const colName =
    variant === 'declaracion' ? 'pdf_filename' :
    variant === 'liga' ? 'pdf_liga_pago_filename' : 'pdf_pago_filename';
  const { rows } = await pool.query(
    `SELECT ${col} as data, ${colName} as filename FROM declaraciones_provisionales WHERE id = $1`,
    [id],
  );
  if (rows.length === 0 || !rows[0].data) return null;
  return { buffer: Buffer.from(rows[0].data), filename: rows[0].filename || `declaracion-${id}.pdf` };
}
