/**
 * Notificaciones email automáticas (Option B — por evento).
 *
 * Cron diario 8:30 AM (`notifications.job.ts`) llama a las dos funciones
 * principales de este servicio para cada tenant activo:
 *
 *   - `processNewAlertas(pool, tenantId)`: detecta alertas que aparecen por
 *     primera vez (no están en `alertas_notificadas`) y manda un email
 *     batched al supervisor + auxiliares + clientes del contribuyente.
 *     Las alertas que dejaron de estar activas se marcan `resuelta_at`.
 *
 *   - `processProximosRecordatorios(pool, tenantId)`: detecta recordatorios
 *     cuya `fecha_limite` cae en las ventanas 3 días / 1 día / mismo día
 *     y manda email a los responsables (cliente + auxiliar; si no hay
 *     auxiliar también supervisor; si owner es supervisor sin auxiliares
 *     también owner). Cada ventana se envía una sola vez (columnas
 *     `email_3d_at`, `email_1d_at`, `email_0d_at`).
 *
 * Decisión MVP: una alerta solo se notifica una vez. Si vuelve a activarse
 * después de resolverse, no re-notifica (sería opt-in al borrar la fila
 * cuando `resuelta_at` se setea).
 */
import type { Pool } from 'pg';
import { prisma } from '../config/database.js';
import { generarAlertasAutomaticas, type AlertaAuto } from './alertas-auto.service.js';
import { emailService } from './email/email.service.js';
import type { AlertaItem } from './email/templates/alertas-nuevas.js';
import type { VentanaRecordatorio } from './email/templates/recordatorio-proximo.js';

const FRONTEND_URL = process.env.FRONTEND_URL || 'http://localhost:3000';

// ────────────────────────────────────────────────────────────────────────
// Resolución de destinatarios
// ────────────────────────────────────────────────────────────────────────

interface UserContact {
  userId: string;
  email: string;
  active: boolean;
}

/**
 * Resuelve user IDs ligados a un contribuyente (supervisor + auxiliares de
 * carteras donde aparece + clientes con acceso). Retorna lista deduplicada.
 */
async function getUserIdsContribuyente(
  pool: Pool,
  contribuyenteId: string,
): Promise<{ supervisor: string | null; auxiliares: string[]; clientes: string[] }> {
  const safeId = contribuyenteId.replace(/[^a-f0-9-]/gi, '');
  const { rows } = await pool.query<{
    supervisor_user_id: string | null;
    auxiliar_user_ids: string[];
    cliente_user_ids: string[];
  }>(`
    SELECT
      eg.supervisor_user_id,
      COALESCE((
        SELECT array_agg(DISTINCT c.auxiliar_user_id) FILTER (WHERE c.auxiliar_user_id IS NOT NULL)
        FROM cartera_entidades ce
        JOIN carteras c ON c.id = ce.cartera_id
        WHERE ce.entidad_id = eg.id
      ), ARRAY[]::uuid[]) AS auxiliar_user_ids,
      COALESCE((
        SELECT array_agg(DISTINCT user_id) FROM cliente_accesos WHERE entidad_id = eg.id
      ), ARRAY[]::uuid[]) AS cliente_user_ids
    FROM entidades_gestionadas eg
    WHERE eg.id = $1::uuid
  `, [safeId]);

  if (rows.length === 0) {
    return { supervisor: null, auxiliares: [], clientes: [] };
  }
  const r = rows[0];
  return {
    supervisor: r.supervisor_user_id ?? null,
    auxiliares: r.auxiliar_user_ids ?? [],
    clientes: r.cliente_user_ids ?? [],
  };
}

/** Owners activos del tenant (BD central). */
async function getOwnerUserIds(tenantId: string): Promise<string[]> {
  const owners = await prisma.tenantMembership.findMany({
    where: { tenantId, isOwner: true, active: true },
    select: { userId: true },
  });
  return owners.map(o => o.userId);
}

/** Resuelve emails para una lista de userIds; filtra inactivos. */
async function getUserContacts(userIds: string[]): Promise<UserContact[]> {
  if (userIds.length === 0) return [];
  const users = await prisma.user.findMany({
    where: { id: { in: userIds }, active: true },
    select: { id: true, email: true, active: true },
  });
  return users.map(u => ({ userId: u.id, email: u.email, active: u.active }));
}

/**
 * Destinatarios de una alerta: supervisor + auxiliares + clientes del
 * contribuyente. Si el owner del tenant es supervisor, ya queda incluido
 * (no se duplica).
 */
async function recipientsForAlerta(
  pool: Pool,
  tenantId: string,
  contribuyenteId: string,
): Promise<string[]> {
  const ids = await getUserIdsContribuyente(pool, contribuyenteId);
  const userIds = new Set<string>();
  if (ids.supervisor) userIds.add(ids.supervisor);
  ids.auxiliares.forEach(id => userIds.add(id));
  ids.clientes.forEach(id => userIds.add(id));
  const contacts = await getUserContacts([...userIds]);
  return [...new Set(contacts.map(c => c.email))];
}

/**
 * Destinatarios de un recordatorio. Los recordatorios del despacho son
 * tenant-level (no atados a contribuyente). Para públicos: clientes con
 * algún acceso + auxiliares de cualquier cartera; si no hay auxiliares,
 * supervisores; si owner aparece como supervisor, también recibe.
 *
 * Privados: solo el creador.
 */
async function recipientsForRecordatorio(
  pool: Pool,
  tenantId: string,
  recordatorio: { creadoPor: string; privado: boolean },
): Promise<string[]> {
  if (recordatorio.privado) {
    const contacts = await getUserContacts([recordatorio.creadoPor]);
    return [...new Set(contacts.map(c => c.email))];
  }

  // Recordatorio público: lee universos relevantes del tenant.
  const { rows: [r] } = await pool.query<{
    auxiliar_user_ids: string[];
    supervisor_user_ids: string[];
    cliente_user_ids: string[];
  }>(`
    SELECT
      COALESCE((
        SELECT array_agg(DISTINCT auxiliar_user_id)
        FROM carteras WHERE auxiliar_user_id IS NOT NULL
      ), ARRAY[]::uuid[]) AS auxiliar_user_ids,
      COALESCE((
        SELECT array_agg(DISTINCT supervisor_user_id) FROM (
          SELECT supervisor_user_id FROM entidades_gestionadas WHERE supervisor_user_id IS NOT NULL
          UNION
          SELECT supervisor_user_id FROM carteras WHERE supervisor_user_id IS NOT NULL
        ) sup
      ), ARRAY[]::uuid[]) AS supervisor_user_ids,
      COALESCE((
        SELECT array_agg(DISTINCT user_id) FROM cliente_accesos
      ), ARRAY[]::uuid[]) AS cliente_user_ids
  `);

  const auxiliares = r?.auxiliar_user_ids ?? [];
  const supervisores = r?.supervisor_user_ids ?? [];
  const clientes = r?.cliente_user_ids ?? [];
  const owners = await getOwnerUserIds(tenantId);

  // Regla del owner: clientes y auxiliares siempre. Si no hay auxiliares,
  // agregar supervisores. Si owner es supervisor y no hay auxiliares,
  // owner queda incluido vía la lista de supervisores.
  const userIds = new Set<string>();
  clientes.forEach(id => userIds.add(id));
  auxiliares.forEach(id => userIds.add(id));
  if (auxiliares.length === 0) {
    supervisores.forEach(id => userIds.add(id));
    // Solo si owner aparece como supervisor (intersección):
    for (const ownerId of owners) {
      if (supervisores.includes(ownerId)) userIds.add(ownerId);
    }
  }

  const contacts = await getUserContacts([...userIds]);
  return [...new Set(contacts.map(c => c.email))];
}

// ────────────────────────────────────────────────────────────────────────
// Procesamiento de alertas
// ────────────────────────────────────────────────────────────────────────

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

/** Lista contribuyentes activos del tenant. */
async function listContribuyentes(pool: Pool): Promise<ContribuyenteInfo[]> {
  const { rows } = await pool.query<{ entidad_id: string; rfc: string; nombre: string }>(`
    SELECT eg.id AS entidad_id, c.rfc, eg.nombre
    FROM entidades_gestionadas eg
    JOIN contribuyentes c ON c.entidad_id = eg.id
    WHERE eg.active = true AND eg.tipo = 'CONTRIBUYENTE'
  `);
  return rows.map(r => ({ entidadId: r.entidad_id, rfc: r.rfc, nombre: r.nombre }));
}

function mapAlertaToItem(a: AlertaAuto): AlertaItem {
  return {
    alertaId: a.id,
    nivel: a.prioridad === 'alta' ? 'high' : a.prioridad === 'media' ? 'medium' : 'low',
    titulo: a.titulo,
    mensaje: a.mensaje,
  };
}

/**
 * Para un (tenant, contribuyente):
 *   1. Genera alertas activas vía `generarAlertasAutomaticas`.
 *   2. Inserta filas nuevas en `alertas_notificadas` (ON CONFLICT DO NOTHING).
 *   3. Marca como resueltas las alertas previamente notificadas que NO están
 *      activas hoy (UPDATE resuelta_at).
 *   4. Si hay alertas nuevas, envía email batched a los responsables.
 */
async function processAlertasContribuyente(
  pool: Pool,
  tenantId: string,
  tenant: { rfc: string; nombre: string },
  contribuyente: ContribuyenteInfo,
): Promise<{ nuevas: number; resueltas: number }> {
  const alertasActivas = await generarAlertasAutomaticas(pool, tenantId, contribuyente.entidadId);
  const activosIds = alertasActivas.map(a => a.id);

  // Re-notificación tras 30 días (D7, 2026-04-26): borra registros de
  // alertas que estuvieron resueltas más de 30 días. Si la alerta vuelve
  // a aparecer ahora, el INSERT siguiente la detecta como "nueva" y
  // vuelve a notificar. Si nunca se resolvió (resuelta_at IS NULL) o se
  // resolvió hace menos de 30 días, la fila se conserva y el INSERT no
  // dispara email.
  await pool.query(`
    DELETE FROM alertas_notificadas
    WHERE contribuyente_id = $1::uuid
      AND resuelta_at IS NOT NULL
      AND resuelta_at < NOW() - INTERVAL '30 days'
  `, [contribuyente.entidadId]);

  // Detecta alertas nuevas: INSERT con ON CONFLICT DO NOTHING. RETURNING id
  // solo trae las filas insertadas (no las que chocaron con el UNIQUE),
  // así sabemos cuáles eran realmente nuevas. Tras la re-notificación de
  // 30 días, una alerta puede volver a notificarse si reapareció después
  // de >30 días resuelta.
  const nuevas: AlertaAuto[] = [];
  for (const a of alertasActivas) {
    const { rows } = await pool.query<{ id: number }>(`
      INSERT INTO alertas_notificadas (alerta_id, contribuyente_id)
      VALUES ($1, $2::uuid)
      ON CONFLICT (alerta_id, COALESCE(contribuyente_id::text, '')) DO NOTHING
      RETURNING id
    `, [a.id, contribuyente.entidadId]);
    if (rows.length > 0) nuevas.push(a);
  }

  // Marca como resueltas las alertas previamente notificadas que ya no
  // aparecen activas hoy. Informativo (no genera email).
  let resueltas = 0;
  const updateQuery = activosIds.length > 0
    ? `UPDATE alertas_notificadas SET resuelta_at = NOW()
       WHERE contribuyente_id = $1::uuid AND resuelta_at IS NULL
         AND alerta_id <> ALL($2::text[])`
    : `UPDATE alertas_notificadas SET resuelta_at = NOW()
       WHERE contribuyente_id = $1::uuid AND resuelta_at IS NULL`;
  const params: any[] = activosIds.length > 0
    ? [contribuyente.entidadId, activosIds]
    : [contribuyente.entidadId];
  const { rowCount } = await pool.query(updateQuery, params);
  resueltas = rowCount ?? 0;

  if (nuevas.length === 0) {
    return { nuevas: 0, resueltas };
  }

  // Envía email batched a los responsables del contribuyente.
  const recipients = await recipientsForAlerta(pool, tenantId, contribuyente.entidadId);
  if (recipients.length === 0) {
    console.warn(`[Notifications] Sin destinatarios para alertas de ${contribuyente.rfc} (tenant ${tenant.rfc})`);
    return { nuevas: nuevas.length, resueltas };
  }

  await emailService.sendAlertasNuevas(recipients, {
    contribuyenteRfc: contribuyente.rfc,
    contribuyenteNombre: contribuyente.nombre,
    despachoNombre: tenant.nombre,
    alertas: nuevas.map(mapAlertaToItem),
    link: `${FRONTEND_URL}/alertas`,
  });

  return { nuevas: nuevas.length, resueltas };
}

/** Procesa todas las alertas del tenant — itera contribuyentes activos. */
export async function processNewAlertas(
  pool: Pool,
  tenantId: string,
  tenant: { rfc: string; nombre: string },
): Promise<{ contribuyentes: number; nuevasTotal: number }> {
  const contribuyentes = await listContribuyentes(pool);
  let nuevasTotal = 0;
  for (const c of contribuyentes) {
    try {
      const { nuevas } = await processAlertasContribuyente(pool, tenantId, tenant, c);
      nuevasTotal += nuevas;
    } catch (err: any) {
      console.error(`[Notifications] Error procesando alertas de ${c.rfc} (tenant ${tenant.rfc}):`, err.message || err);
    }
  }
  return { contribuyentes: contribuyentes.length, nuevasTotal };
}

// ────────────────────────────────────────────────────────────────────────
// Procesamiento de recordatorios próximos
// ────────────────────────────────────────────────────────────────────────

interface RecordatorioRow {
  id: number;
  titulo: string;
  descripcion: string | null;
  notas: string | null;
  fecha_limite: string;
  privado: boolean;
  creado_por: string;
  email_3d_at: Date | null;
  email_1d_at: Date | null;
  email_0d_at: Date | null;
}

const VENTANA_DIAS: Record<VentanaRecordatorio, number> = {
  '3d': 3,
  '1d': 1,
  '0d': 0,
};

/**
 * Procesa recordatorios cuya `fecha_limite` cae en alguna ventana (3d/1d/0d)
 * y que aún no tienen email enviado para esa ventana específica. Manda email
 * y marca la columna correspondiente.
 */
export async function processProximosRecordatorios(
  pool: Pool,
  tenantId: string,
  tenant: { rfc: string; nombre: string },
): Promise<{ enviados: number }> {
  let enviados = 0;
  for (const ventana of (['3d', '1d', '0d'] as const)) {
    const dias = VENTANA_DIAS[ventana];
    const col = `email_${ventana}_at`;
    const { rows } = await pool.query<RecordatorioRow>(`
      SELECT id, titulo, descripcion, notas, fecha_limite::text AS fecha_limite,
             privado, creado_por, email_3d_at, email_1d_at, email_0d_at
      FROM recordatorios
      WHERE completado = false
        AND fecha_limite = (CURRENT_DATE + ${dias})::date
        AND ${col} IS NULL
    `);

    for (const r of rows) {
      try {
        const recipients = await recipientsForRecordatorio(pool, tenantId, {
          creadoPor: r.creado_por,
          privado: r.privado,
        });
        if (recipients.length === 0) {
          console.warn(`[Notifications] Recordatorio ${r.id} (${tenant.rfc}) sin destinatarios — skip ${ventana}`);
          continue;
        }
        await emailService.sendRecordatorioProximo(recipients, {
          titulo: r.titulo,
          descripcion: r.descripcion,
          notas: r.notas,
          fechaLimite: r.fecha_limite,
          ventana,
          despachoNombre: tenant.nombre,
          link: `${FRONTEND_URL}/calendario`,
        });
        // Marca columna de ventana enviada.
        await pool.query(`UPDATE recordatorios SET ${col} = NOW() WHERE id = $1`, [r.id]);
        enviados++;
      } catch (err: any) {
        console.error(`[Notifications] Error en recordatorio ${r.id} (${tenant.rfc}, ${ventana}):`, err.message || err);
      }
    }
  }
  return { enviados };
}
