import cron from 'node-cron';
import { prisma, tenantDb } from '../config/database.js';
import { env } from '../config/env.js';
import { emailService } from '../services/email/email.service.js';
import { sweepStaleSatJobs } from '../services/sat/sweep-stale-jobs.service.js';
import type { SatSyncAlertData } from '../services/email/templates/sat-sync-alert.js';

let monitorTask: ReturnType<typeof cron.schedule> | null = null;

interface TenantInfo {
  id: string;
  rfc: string;
  nombre: string;
  databaseName: string | null;
}

interface ContribuyenteInfo {
  id: string;
  rfc: string;
  nombre: string;
}

function hoursAgo(hours: number): Date {
  return new Date(Date.now() - hours * 60 * 60 * 1000);
}

async function loadActiveTenants(): Promise<Map<string, TenantInfo>> {
  const tenants = await prisma.tenant.findMany({
    where: { active: true },
    select: { id: true, rfc: true, nombre: true, databaseName: true },
  });
  const map = new Map<string, TenantInfo>();
  for (const t of tenants) {
    map.set(t.id, t);
  }
  return map;
}

async function loadContribuyentesForTenant(tenant: TenantInfo): Promise<Map<string, ContribuyenteInfo>> {
  const map = new Map<string, ContribuyenteInfo>();
  if (!tenant.databaseName) return map;
  try {
    const pool = await tenantDb.getPool(tenant.id, tenant.databaseName);
    const { rows } = await pool.query(`
      SELECT c.entidad_id AS id, c.rfc, eg.nombre
      FROM contribuyentes c
      JOIN entidades_gestionadas eg ON eg.id = c.entidad_id
    `);
    for (const r of rows) {
      map.set(r.id, { id: r.id, rfc: r.rfc, nombre: r.nombre });
    }
  } catch (err: any) {
    console.error(`[SAT Monitor] Error cargando contribuyentes para tenant ${tenant.rfc}:`, err.message);
  }
  return map;
}

async function findFailedJobs(lookbackHours: number) {
  const cutoff = hoursAgo(lookbackHours);
  return prisma.satSyncJob.findMany({
    where: {
      status: 'failed',
      completedAt: { gte: cutoff },
    },
    orderBy: { completedAt: 'desc' },
  });
}

async function findPendingOldJobs(pendingHours: number) {
  const cutoff = hoursAgo(pendingHours);
  return prisma.satSyncJob.findMany({
    where: {
      status: 'pending',
      createdAt: { lte: cutoff },
      OR: [
        { nextRetryAt: null },
        { nextRetryAt: { lte: new Date() } },
      ],
    },
    orderBy: { createdAt: 'asc' },
  });
}

async function findMissingInitialSync(tenants: Map<string, TenantInfo>): Promise<SatSyncAlertData['missingInitial']> {
  const missing: SatSyncAlertData['missingInitial'] = [];

  for (const tenant of tenants.values()) {
    if (!tenant.databaseName) continue;
    try {
      const pool = await tenantDb.getPool(tenant.id, tenant.databaseName);
      const { rows: contribuyentes } = await pool.query(`
        SELECT c.entidad_id AS id, c.rfc, eg.nombre
        FROM contribuyentes c
        JOIN entidades_gestionadas eg ON eg.id = c.entidad_id
        JOIN fiel_contribuyente f ON f.contribuyente_id = c.entidad_id
        WHERE f.is_active = true
          AND f.valid_until >= NOW()
        ORDER BY eg.nombre
      `);

      if (contribuyentes.length === 0) continue;

      const contribuyenteIds = contribuyentes.map((c: any) => c.id);
      const initialJobs = await prisma.satSyncJob.findMany({
        where: {
          tenantId: tenant.id,
          contribuyenteId: { in: contribuyenteIds },
          type: 'initial',
          status: { in: ['completed', 'running', 'pending'] },
        },
        select: { contribuyenteId: true, status: true },
      });
      const completedOrInProgressIds = new Set(initialJobs.map(j => j.contribuyenteId));

      for (const c of contribuyentes) {
        if (!completedOrInProgressIds.has(c.id)) {
          missing.push({
            tenantName: tenant.nombre,
            tenantRfc: tenant.rfc,
            contribuyenteName: c.nombre,
            contribuyenteRfc: c.rfc,
          });
        }
      }
    } catch (err: any) {
      console.error(`[SAT Monitor] Error revisando FIEL sin sync inicial para tenant ${tenant.rfc}:`, err.message);
    }
  }

  return missing;
}

function buildAlertData(
  tenants: Map<string, TenantInfo>,
  contribuyenteMaps: Map<string, Map<string, ContribuyenteInfo>>,
  failed: Awaited<ReturnType<typeof findFailedJobs>>,
  stale: Array<{ id: string; tenantId: string; kind: 'pending-stale' | 'running-stale'; ageHours: number }>,
  staleJobsById: Map<string, Awaited<ReturnType<typeof prisma.satSyncJob.findFirst>>>,
  pendingOld: Awaited<ReturnType<typeof findPendingOldJobs>>,
  missingInitial: SatSyncAlertData['missingInitial']
): SatSyncAlertData {
  const now = new Date();
  const generatedAt = now.toLocaleString('es-MX', { timeZone: 'America/Mexico_City' });

  const resolveJob = (job: { tenantId: string; contribuyenteId: string | null }) => {
    const tenant = tenants.get(job.tenantId);
    const contribMap = contribuyenteMaps.get(job.tenantId);
    const contrib = job.contribuyenteId ? contribMap?.get(job.contribuyenteId) : undefined;
    return {
      tenantName: tenant?.nombre || job.tenantId,
      tenantRfc: tenant?.rfc || '—',
      contribuyenteName: contrib?.nombre || null,
      contribuyenteRfc: contrib?.rfc || null,
    };
  };

  return {
    generatedAt,
    recipient: env.SAT_ALERT_EMAIL ?? env.ADMIN_EMAIL,
    summary: {
      failed: failed.length,
      stale: stale.length,
      stuckRunning: 0,
      pendingOld: pendingOld.length,
      missingInitial: missingInitial.length,
    },
    failed: failed.map(j => ({
      ...resolveJob(j),
      type: j.type,
      errorMessage: j.errorMessage,
      completedAt: j.completedAt,
    })),
    stale: stale.map(e => {
      const job = staleJobsById.get(e.id);
      return {
        id: e.id,
        ...resolveJob({ tenantId: e.tenantId, contribuyenteId: job?.contribuyenteId ?? null }),
        type: job?.type || '—',
        kind: e.kind,
        ageHours: e.ageHours,
      };
    }),
    stuckRunning: [],
    pendingOld: pendingOld.map(j => ({
      id: j.id,
      ...resolveJob(j),
      type: j.type,
      createdAt: j.createdAt,
      nextRetryAt: j.nextRetryAt,
      hoursPending: Math.round((now.getTime() - j.createdAt.getTime()) / 3_600_000),
    })),
    missingInitial,
  };
}

export async function runSatSyncMonitor(): Promise<void> {
  console.log('[SAT Monitor] Iniciando revisión de sincronizaciones SAT');

  const pendingHours = env.SAT_STUCK_RUNNING_HOURS;
  const failedLookbackHours = env.SAT_FAILED_LOOKBACK_HOURS;

  try {
    const tenants = await loadActiveTenants();

    const [failed, staleResult, pendingOld, missingInitial] = await Promise.all([
      findFailedJobs(failedLookbackHours),
      sweepStaleSatJobs({ apply: false }),
      findPendingOldJobs(pendingHours),
      findMissingInitialSync(tenants),
    ]);

    const jobTenantIds = new Set<string>();
    for (const j of [...failed, ...pendingOld]) {
      jobTenantIds.add(j.tenantId);
    }
    for (const e of staleResult.entries) {
      jobTenantIds.add(e.tenantId);
    }

    const staleJobsById = new Map(
      (
        await prisma.satSyncJob.findMany({
          where: { id: { in: staleResult.entries.map(e => e.id) } },
        })
      ).map(j => [j.id, j])
    );

    for (const e of staleResult.entries) {
      const job = staleJobsById.get(e.id);
      if (job?.contribuyenteId && job.tenantId) {
        jobTenantIds.add(job.tenantId);
      }
    }

    const contribuyenteMaps = new Map<string, Map<string, ContribuyenteInfo>>();
    for (const tenantId of jobTenantIds) {
      const tenant = tenants.get(tenantId);
      if (!tenant) continue;
      const map = await loadContribuyentesForTenant(tenant);
      contribuyenteMaps.set(tenantId, map);
    }

    const alertData = buildAlertData(
      tenants,
      contribuyenteMaps,
      failed,
      staleResult.entries,
      staleJobsById,
      pendingOld,
      missingInitial
    );

    const hasIssues =
      alertData.summary.failed > 0 ||
      alertData.summary.stale > 0 ||
      alertData.summary.pendingOld > 0 ||
      alertData.summary.missingInitial > 0;

    if (!hasIssues) {
      console.log('[SAT Monitor] Sin anomalías detectadas');
      return;
    }

    const recipient = env.SAT_ALERT_EMAIL ?? env.ADMIN_EMAIL;
    console.log(`[SAT Monitor] Enviando alerta a ${recipient}:`, alertData.summary);
    await emailService.sendSatSyncAlert(recipient, alertData);
    console.log('[SAT Monitor] Alerta enviada');
  } catch (error: any) {
    console.error('[SAT Monitor] Error en revisión:', error.message || error);
  }
}

export function startSatSyncMonitorJob(): void {
  if (monitorTask) {
    console.log('[SAT Monitor] Job ya está programado');
    return;
  }

  const schedule = env.SAT_MONITOR_SCHEDULE;
  if (!cron.validate(schedule)) {
    console.error('[SAT Monitor] Expresión cron inválida:', schedule);
    return;
  }

  monitorTask = cron.schedule(schedule, async () => {
    try {
      await runSatSyncMonitor();
    } catch (error: any) {
      console.error('[SAT Monitor Cron] Error:', error.message || error);
    }
  }, {
    timezone: 'America/Mexico_City',
  });

  console.log(`[SAT Monitor] Programado: ${schedule}`);
}

export function stopSatSyncMonitorJob(): void {
  if (monitorTask) {
    monitorTask.stop();
    monitorTask = null;
  }
}
