import { prisma } from '../config/database.js';
import { getRegimenesActivosClaves } from './regimen.service.js';
import { getObligaciones } from './obligaciones.service.js';
import type { Pool } from 'pg';

interface EventoGenerado {
  titulo: string;
  tipo: string;
  fechaLimite: string;
  recurrencia: string;
  completado: boolean;
  descripcion: string;
}

/**
 * Obtener días inhábiles del año como Set de strings 'YYYY-MM-DD'
 */
async function getDiasInhabiles(año: number): Promise<Set<string>> {
  const rows = await prisma.diaInhabil.findMany({
    where: {
      fecha: {
        gte: new Date(`${año}-01-01`),
        lte: new Date(`${año}-12-31`),
      },
    },
  });
  return new Set(rows.map(r => r.fecha.toISOString().split('T')[0]));
}

/**
 * Si la fecha cae en día inhábil (sábado, domingo, festivo), recorrer al siguiente día hábil
 */
function siguienteDiaHabil(fecha: Date, inhabiles: Set<string>): Date {
  const d = new Date(fecha);
  while (true) {
    const dow = d.getDay();
    const str = d.toISOString().split('T')[0];
    if (dow !== 0 && dow !== 6 && !inhabiles.has(str)) {
      return d;
    }
    d.setDate(d.getDate() + 1);
  }
}

/**
 * Agregar N días hábiles a una fecha
 */
function agregarDiasHabiles(fecha: Date, dias: number, inhabiles: Set<string>): Date {
  const d = new Date(fecha);
  let added = 0;
  while (added < dias) {
    d.setDate(d.getDate() + 1);
    const dow = d.getDay();
    const str = d.toISOString().split('T')[0];
    if (dow !== 0 && dow !== 6 && !inhabiles.has(str)) {
      added++;
    }
  }
  return d;
}

/**
 * Calcula días adicionales por RFC según Resolución Miscelánea Fiscal
 * Sexto dígito numérico del RFC:
 * 1-2: +1 día, 3-4: +2, 5-6: +3, 7-8: +4, 9-0: +5
 */
function diasExtensionRfc(rfc: string): number {
  // Extraer sexto dígito numérico
  const numeros = rfc.replace(/[^0-9]/g, '');
  if (numeros.length < 6) return 0;
  const sexto = parseInt(numeros[5]);

  if (sexto === 1 || sexto === 2) return 1;
  if (sexto === 3 || sexto === 4) return 2;
  if (sexto === 5 || sexto === 6) return 3;
  if (sexto === 7 || sexto === 8) return 4;
  return 5; // 9 o 0
}

/**
 * Genera los eventos fiscales para un tenant en un año dado,
 * basándose en el catálogo central y las reglas del SAT.
 */
export async function generarEventosFiscales(
  tenantId: string,
  año: number,
): Promise<EventoGenerado[]> {
  const tenant = await prisma.tenant.findUnique({
    where: { id: tenantId },
    select: { rfc: true },
  });
  if (!tenant) return [];

  const rfc = tenant.rfc;
  const inhabiles = await getDiasInhabiles(año);

  // Regímenes activos del tenant
  const activos = await getRegimenesActivosClaves(tenantId);
  const activosSet = new Set(activos);

  const catalogo = await prisma.eventoFiscalCatalogo.findMany({
    where: { activo: true },
  });

  const eventos: EventoGenerado[] = [];
  const hoy = new Date();

  for (const cat of catalogo) {
    // Filtrar por régimen: si el evento es para regímenes específicos,
    // verificar que el tenant tenga al menos uno de ellos activo
    if (cat.regimenes !== 'todos' && activos.length > 0) {
      const regimenesEvento = cat.regimenes.split(',').map(r => r.trim());
      const aplica = regimenesEvento.some(r => activosSet.has(r));
      if (!aplica) continue;
    }

    if (cat.recurrencia === 'mensual') {
      for (let mes = 1; mes <= 12; mes++) {
        // Mes relativo: 1 = mes posterior al que se declara
        const mesObligacion = mes; // mes que se declara
        const mesVencimiento = mes + cat.mesRelativo;
        const añoVencimiento = mesVencimiento > 12 ? año + 1 : año;
        const mesReal = mesVencimiento > 12 ? mesVencimiento - 12 : mesVencimiento;

        // Fecha base
        const lastDay = new Date(añoVencimiento, mesReal, 0).getDate();
        const dia = Math.min(cat.diaBase, lastDay);
        let fechaLimite = new Date(añoVencimiento, mesReal - 1, dia);

        // Ajustar a día hábil
        fechaLimite = siguienteDiaHabil(fechaLimite, inhabiles);

        // Extensión por RFC
        if (cat.usaExtensionRfc) {
          const diasExtra = diasExtensionRfc(rfc);
          fechaLimite = agregarDiasHabiles(fechaLimite, diasExtra, inhabiles);
        }

        const completado = fechaLimite < hoy;

        const meses = ['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun', 'Jul', 'Ago', 'Sep', 'Oct', 'Nov', 'Dic'];

        eventos.push({
          titulo: cat.titulo,
          tipo: cat.tipo,
          fechaLimite: fechaLimite.toISOString().split('T')[0],
          recurrencia: cat.recurrencia,
          completado,
          descripcion: `${cat.titulo} — ${meses[mesObligacion - 1]} ${año}`,
        });
      }
    } else if (cat.recurrencia === 'anual' && cat.mesFijo) {
      const lastDay = new Date(año, cat.mesFijo, 0).getDate();
      const dia = Math.min(cat.diaBase, lastDay);
      let fechaLimite = new Date(año, cat.mesFijo - 1, dia);

      fechaLimite = siguienteDiaHabil(fechaLimite, inhabiles);

      eventos.push({
        titulo: cat.titulo,
        tipo: cat.tipo,
        fechaLimite: fechaLimite.toISOString().split('T')[0],
        recurrencia: cat.recurrencia,
        completado: fechaLimite < hoy,
        descripcion: `${cat.titulo} — Ejercicio ${año - 1}`,
      });
    }
  }

  // Ordenar por fecha
  eventos.sort((a, b) => a.fechaLimite.localeCompare(b.fechaLimite));

  return eventos;
}

/**
 * Genera eventos de calendario a partir de las obligaciones reales de un contribuyente.
 * Usado en tenants despacho — reemplaza el catálogo estático por las obligaciones
 * registradas en obligaciones_contribuyente con su estado de cumplimiento en obligacion_periodos.
 */
export async function generarEventosDesdeObligaciones(
  pool: Pool,
  contribuyenteId: string | null,
  año: number,
): Promise<EventoGenerado[]> {
  if (!contribuyenteId) return [];

  const inhabiles = await getDiasInhabiles(año);
  const obligaciones = await getObligaciones(pool, contribuyenteId);
  const activas = obligaciones.filter(o => o.activa);
  const eventos: EventoGenerado[] = [];

  // Get completion records for this contribuyente
  const { rows: completions } = await pool.query(`
    SELECT op.obligacion_id, op.periodo, op.completada
    FROM obligacion_periodos op
    JOIN obligaciones_contribuyente oc ON oc.id = op.obligacion_id
    WHERE oc.contribuyente_id = $1
  `, [contribuyenteId]);

  const completionMap = new Map<string, boolean>();
  for (const c of completions) {
    completionMap.set(`${c.obligacion_id}:${c.periodo}`, c.completada);
  }

  const meses = ['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun', 'Jul', 'Ago', 'Sep', 'Oct', 'Nov', 'Dic'];

  for (const ob of activas) {
    const freq = ob.frecuencia || 'mensual';

    // Determine which months this obligation applies to
    const monthsToGenerate: number[] = [];
    for (let m = 1; m <= 12; m++) {
      if (freq === 'mensual') monthsToGenerate.push(m);
      else if (freq === 'bimestral' && m % 2 === 1) monthsToGenerate.push(m);
      else if (freq === 'trimestral' && [1, 4, 7, 10].includes(m)) monthsToGenerate.push(m);
      else if (freq === 'cuatrimestral' && [1, 5, 9].includes(m)) monthsToGenerate.push(m);
      else if (freq === 'anual' && (m === 3 || m === 4)) monthsToGenerate.push(m);
      // 'eventual' and unknown: skip auto-generation
    }

    for (const mes of monthsToGenerate) {
      // Parse day from fechaLimite text; default to 17
      let diaBase = 17;
      if (ob.fechaLimite) {
        const matchDia = ob.fechaLimite.match(/d[íi]a?\s*(\d+)/i);
        if (matchDia) diaBase = parseInt(matchDia[1]);
        // "Último día" → last day of month
        if (ob.fechaLimite.toLowerCase().includes('ltimo d')) diaBase = 0;
      }

      // Deadline is usually next month for mensual/bimestral/trimestral obligations
      let mesVencimiento = mes + 1;
      let añoVencimiento = año;
      if (mesVencimiento > 12) { mesVencimiento = 1; añoVencimiento++; }

      // For annual obligations the deadline month IS the month (marzo/abril)
      if (freq === 'anual') {
        mesVencimiento = mes;
        añoVencimiento = año;
      }

      const lastDayOfMonth = new Date(añoVencimiento, mesVencimiento, 0).getDate();
      const dia = diaBase === 0 ? lastDayOfMonth : Math.min(diaBase, lastDayOfMonth);
      let fechaLimite = new Date(añoVencimiento, mesVencimiento - 1, dia);
      fechaLimite = siguienteDiaHabil(fechaLimite, inhabiles);

      const periodo = `${año}-${String(mes).padStart(2, '0')}`;
      const isCompleted = completionMap.get(`${ob.id}:${periodo}`) === true;
      const isPastDue = !isCompleted && fechaLimite < new Date();

      // Type encodes the status for calendar coloring
      const tipoEvento = isCompleted
        ? 'obligacion-completada'
        : isPastDue
          ? 'obligacion-atrasada'
          : 'obligacion-pendiente';

      eventos.push({
        titulo: ob.nombre,
        tipo: tipoEvento,
        fechaLimite: fechaLimite.toISOString().split('T')[0],
        recurrencia: freq,
        completado: isCompleted,
        descripcion: `${ob.nombre} — ${meses[mes - 1]} ${año}`,
      });
    }
  }

  eventos.sort((a, b) => a.fechaLimite.localeCompare(b.fechaLimite));
  return eventos;
}
