import type { Pool } from 'pg';

export type Recurrencia =
  | 'semanal' | 'quincenal' | 'mensual'
  | 'bimestral' | 'trimestral' | 'semestral' | 'anual';

export interface TareaCatalogo {
  id: string;
  contribuyenteId: string;
  nombre: string;
  descripcion: string | null;
  recurrencia: Recurrencia;
  diaSemana: number | null;
  diaMes: number | null;
  soloSupervisorCompleta: boolean;
  esDefault: boolean;
  active: boolean;
  orden: number;
  createdAt: Date;
}

export interface TareaPeriodo {
  id: string;
  tareaId: string;
  periodo: string;
  fechaLimite: Date;
  completada: boolean;
  completadaAt: Date | null;
  completadaPor: string | null;
  notas: string | null;
}

export interface TareaConPeriodo extends TareaCatalogo {
  periodoActual: TareaPeriodo | null;
}

const ROW_TO_TAREA = (r: any): TareaCatalogo => ({
  id: r.id,
  contribuyenteId: r.contribuyente_id,
  nombre: r.nombre,
  descripcion: r.descripcion,
  recurrencia: r.recurrencia,
  diaSemana: r.dia_semana,
  diaMes: r.dia_mes,
  soloSupervisorCompleta: r.solo_supervisor_completa,
  esDefault: r.es_default,
  active: r.active,
  orden: r.orden,
  createdAt: r.created_at,
});

const ROW_TO_PERIODO = (r: any): TareaPeriodo => ({
  id: r.id,
  tareaId: r.tarea_id,
  periodo: r.periodo,
  fechaLimite: r.fecha_limite,
  completada: r.completada,
  completadaAt: r.completada_at,
  completadaPor: r.completada_por,
  notas: r.notas,
});

function sanitizeUuid(id: string): string {
  return id.replace(/[^a-f0-9-]/gi, '');
}

// ─── Catálogo CRUD ───

export async function listTareas(pool: Pool, contribuyenteId: string): Promise<TareaCatalogo[]> {
  const { rows } = await pool.query(
    `SELECT * FROM tareas_catalogo
      WHERE contribuyente_id = $1 AND active = true
      ORDER BY orden, nombre`,
    [sanitizeUuid(contribuyenteId)],
  );
  return rows.map(ROW_TO_TAREA);
}

export interface TareaInput {
  nombre: string;
  descripcion?: string | null;
  recurrencia: Recurrencia;
  diaSemana?: number | null;
  diaMes?: number | null;
  soloSupervisorCompleta?: boolean;
  orden?: number;
}

export async function createTarea(
  pool: Pool,
  contribuyenteId: string,
  data: TareaInput,
): Promise<TareaCatalogo> {
  const { rows: [r] } = await pool.query(
    `INSERT INTO tareas_catalogo
      (contribuyente_id, nombre, descripcion, recurrencia,
       dia_semana, dia_mes, solo_supervisor_completa, orden)
     VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
     RETURNING *`,
    [
      sanitizeUuid(contribuyenteId),
      data.nombre,
      data.descripcion ?? null,
      data.recurrencia,
      data.diaSemana ?? null,
      data.diaMes ?? null,
      data.soloSupervisorCompleta ?? false,
      data.orden ?? 0,
    ],
  );
  return ROW_TO_TAREA(r);
}

export async function updateTarea(
  pool: Pool,
  tareaId: string,
  data: Partial<TareaInput>,
): Promise<TareaCatalogo | null> {
  const fields: string[] = [];
  const values: unknown[] = [];
  let i = 1;
  if (data.nombre !== undefined) { fields.push(`nombre = $${i++}`); values.push(data.nombre); }
  if (data.descripcion !== undefined) { fields.push(`descripcion = $${i++}`); values.push(data.descripcion); }
  if (data.recurrencia !== undefined) { fields.push(`recurrencia = $${i++}`); values.push(data.recurrencia); }
  if (data.diaSemana !== undefined) { fields.push(`dia_semana = $${i++}`); values.push(data.diaSemana); }
  if (data.diaMes !== undefined) { fields.push(`dia_mes = $${i++}`); values.push(data.diaMes); }
  if (data.soloSupervisorCompleta !== undefined) { fields.push(`solo_supervisor_completa = $${i++}`); values.push(data.soloSupervisorCompleta); }
  if (data.orden !== undefined) { fields.push(`orden = $${i++}`); values.push(data.orden); }
  if (fields.length === 0) return null;
  values.push(sanitizeUuid(tareaId));
  const { rows: [r] } = await pool.query(
    `UPDATE tareas_catalogo SET ${fields.join(', ')} WHERE id = $${i} RETURNING *`,
    values,
  );
  return r ? ROW_TO_TAREA(r) : null;
}

export async function deleteTarea(pool: Pool, tareaId: string): Promise<boolean> {
  const { rowCount } = await pool.query(
    `UPDATE tareas_catalogo SET active = false WHERE id = $1`,
    [sanitizeUuid(tareaId)],
  );
  return (rowCount ?? 0) > 0;
}

// ─── Materialización de periodos ───

/** Convierte una fecha a su `periodo` según recurrencia. */
function periodoForDate(date: Date, recurrencia: Recurrencia): string {
  const año = date.getFullYear();
  const mes = date.getMonth() + 1;
  if (recurrencia === 'semanal' || recurrencia === 'quincenal') {
    return `${año}-W${String(isoWeek(date)).padStart(2, '0')}`;
  }
  if (recurrencia === 'mensual') return `${año}-${String(mes).padStart(2, '0')}`;
  if (recurrencia === 'bimestral') return `${año}-B${Math.ceil(mes / 2)}`;
  if (recurrencia === 'trimestral') return `${año}-Q${Math.ceil(mes / 3)}`;
  if (recurrencia === 'semestral') return `${año}-S${Math.ceil(mes / 6)}`;
  return `${año}`;
}

function isoWeek(date: Date): number {
  const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
  const dayNum = d.getUTCDay() || 7;
  d.setUTCDate(d.getUTCDate() + 4 - dayNum);
  const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
  return Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7);
}

/**
 * Calcula la fecha límite del próximo periodo de una tarea, basado en
 * `recurrencia`, `dia_semana`/`dia_mes` y un punto de referencia.
 *
 * Para semanal/quincenal: primera ocurrencia del dia_semana en/después de
 * `fromDate`. (Quincenal alterna con el mismo dia_semana cada 14 días.)
 * Para mensual+: dia_mes del periodo en curso de `fromDate`. Si el dia_mes
 * excede el último día del mes, se clampa al último día.
 */
function computeFechaLimite(
  recurrencia: Recurrencia,
  diaSemana: number | null,
  diaMes: number | null,
  fromDate: Date,
): Date {
  if (recurrencia === 'semanal' || recurrencia === 'quincenal') {
    const target = (diaSemana ?? 5);
    const d = new Date(fromDate);
    const current = d.getDay() === 0 ? 7 : d.getDay();
    const diff = (target - current + 7) % 7;
    d.setDate(d.getDate() + diff);
    return d;
  }
  // mensual a anual: usar el mes "ancla" del periodo
  const año = fromDate.getFullYear();
  let mesAncla = fromDate.getMonth() + 1;
  if (recurrencia === 'bimestral') mesAncla = Math.ceil(mesAncla / 2) * 2;
  else if (recurrencia === 'trimestral') mesAncla = Math.ceil(mesAncla / 3) * 3;
  else if (recurrencia === 'semestral') mesAncla = Math.ceil(mesAncla / 6) * 6;
  else if (recurrencia === 'anual') mesAncla = 12;
  const lastDay = new Date(año, mesAncla, 0).getDate();
  const dia = Math.min(diaMes ?? lastDay, lastDay);
  return new Date(año, mesAncla - 1, dia);
}

/**
 * Asegura que existan los periodos vigentes (presente + futuros próximos)
 * para todas las tareas activas del contribuyente. Solo crea periodos
 * cuya `fecha_limite >= today` (no retroactivos, igual que obligaciones).
 *
 * Para cada tarea genera el periodo CURRENT (donde cae hoy) si su fecha
 * límite aún no ha pasado, o el NEXT si ya pasó.
 */
export async function materializarPeriodos(
  pool: Pool,
  contribuyenteId: string,
): Promise<void> {
  const tareas = await listTareas(pool, contribuyenteId);
  const today = new Date();
  today.setHours(0, 0, 0, 0);

  for (const t of tareas) {
    let fechaLimite = computeFechaLimite(t.recurrencia, t.diaSemana, t.diaMes, today);
    // Si la fecha límite calculada para "hoy" ya pasó, salta al siguiente periodo
    while (fechaLimite < today) {
      const next = new Date(fechaLimite);
      const recurrenciaIncrement: Record<Recurrencia, () => void> = {
        semanal: () => next.setDate(next.getDate() + 7),
        quincenal: () => next.setDate(next.getDate() + 14),
        mensual: () => next.setMonth(next.getMonth() + 1),
        bimestral: () => next.setMonth(next.getMonth() + 2),
        trimestral: () => next.setMonth(next.getMonth() + 3),
        semestral: () => next.setMonth(next.getMonth() + 6),
        anual: () => next.setFullYear(next.getFullYear() + 1),
      };
      recurrenciaIncrement[t.recurrencia]();
      fechaLimite = computeFechaLimite(t.recurrencia, t.diaSemana, t.diaMes, next);
    }

    const periodo = periodoForDate(fechaLimite, t.recurrencia);
    await pool.query(
      `INSERT INTO tarea_periodos (tarea_id, periodo, fecha_limite)
       VALUES ($1, $2, $3)
       ON CONFLICT (tarea_id, periodo) DO NOTHING`,
      [t.id, periodo, fechaLimite.toISOString().split('T')[0]],
    );
  }
}

/**
 * Lee tareas activas del contribuyente con su periodo más cercano (vigente
 * o pendiente). Materializa los faltantes antes de leer.
 */
export async function listTareasConPeriodoActual(
  pool: Pool,
  contribuyenteId: string,
): Promise<TareaConPeriodo[]> {
  await materializarPeriodos(pool, contribuyenteId);
  const tareas = await listTareas(pool, contribuyenteId);
  if (tareas.length === 0) return [];

  const ids = tareas.map(t => t.id);
  const today = new Date().toISOString().split('T')[0];
  const { rows } = await pool.query(
    `SELECT DISTINCT ON (tarea_id) *
       FROM tarea_periodos
      WHERE tarea_id = ANY($1::uuid[])
        AND (completada = false OR fecha_limite >= $2::date)
      ORDER BY tarea_id, fecha_limite ASC`,
    [ids, today],
  );
  const periodos = new Map(rows.map(r => [r.tarea_id, ROW_TO_PERIODO(r)]));
  return tareas.map(t => ({ ...t, periodoActual: periodos.get(t.id) ?? null }));
}

// ─── Completar / descompletar periodo ───

const ROLES_SUPERVISOR = new Set(['owner', 'cfo', 'supervisor']);

/**
 * Marca un periodo como completado. Si la tarea tiene
 * `solo_supervisor_completa=true`, valida que el rol del usuario sea
 * owner/cfo/supervisor. Devuelve el periodo actualizado y la tarea
 * (para que el caller pueda disparar notificaciones).
 */
export async function completarPeriodo(
  pool: Pool,
  periodoId: string,
  userId: string,
  userRole: string,
  notas: string | null = null,
): Promise<{ periodo: TareaPeriodo; tarea: TareaCatalogo } | null> {
  const { rows: [pRow] } = await pool.query(
    `SELECT tp.*, tc.solo_supervisor_completa
       FROM tarea_periodos tp
       JOIN tareas_catalogo tc ON tc.id = tp.tarea_id
      WHERE tp.id = $1`,
    [sanitizeUuid(periodoId)],
  );
  if (!pRow) return null;
  if (pRow.solo_supervisor_completa && !ROLES_SUPERVISOR.has(userRole)) {
    throw new Error('Solo supervisor o owner pueden marcar esta tarea como completada');
  }
  const { rows: [updated] } = await pool.query(
    `UPDATE tarea_periodos
        SET completada = true, completada_at = NOW(), completada_por = $2, notas = $3
      WHERE id = $1
      RETURNING *`,
    [sanitizeUuid(periodoId), userId, notas],
  );
  const { rows: [tareaRow] } = await pool.query(
    `SELECT * FROM tareas_catalogo WHERE id = $1`,
    [pRow.tarea_id],
  );
  return { periodo: ROW_TO_PERIODO(updated), tarea: ROW_TO_TAREA(tareaRow) };
}

export async function descompletarPeriodo(
  pool: Pool,
  periodoId: string,
): Promise<boolean> {
  const { rowCount } = await pool.query(
    `UPDATE tarea_periodos
        SET completada = false, completada_at = NULL, completada_por = NULL
      WHERE id = $1`,
    [sanitizeUuid(periodoId)],
  );
  return (rowCount ?? 0) > 0;
}

// ─── Calendario / alertas ───

export interface TareaEventoCalendario {
  titulo: string;
  descripcion: string;
  tipo: 'tarea';
  fechaLimite: string;
  recurrencia: string;
  completado: boolean;
  notas: string | null;
  contribuyenteId: string;
  tareaId: string;
  periodoId: string;
}

/**
 * Lee tareas + sus periodos del año para mostrar en /calendario. Materializa
 * los faltantes antes de leer (mismo patrón que listTareasConPeriodoActual).
 * Si no hay contribuyenteId, no retorna nada (las tareas son siempre por
 * contribuyente).
 */
export async function getEventosTareasParaCalendario(
  pool: Pool,
  contribuyenteId: string,
  año: number,
): Promise<TareaEventoCalendario[]> {
  await materializarPeriodos(pool, contribuyenteId);
  const { rows } = await pool.query(
    `SELECT tp.id AS periodo_id, tp.tarea_id, tp.fecha_limite, tp.completada, tp.notas,
            tc.nombre, tc.descripcion, tc.recurrencia, tc.contribuyente_id
       FROM tarea_periodos tp
       JOIN tareas_catalogo tc ON tc.id = tp.tarea_id
      WHERE tc.contribuyente_id = $1
        AND tc.active = true
        AND EXTRACT(YEAR FROM tp.fecha_limite) = $2`,
    [sanitizeUuid(contribuyenteId), año],
  );
  return rows.map(r => ({
    titulo: r.nombre,
    descripcion: r.descripcion ?? '',
    tipo: 'tarea' as const,
    fechaLimite: r.fecha_limite instanceof Date ? r.fecha_limite.toISOString().split('T')[0] : String(r.fecha_limite),
    recurrencia: r.recurrencia,
    completado: r.completada,
    notas: r.notas,
    contribuyenteId: r.contribuyente_id,
    tareaId: r.tarea_id,
    periodoId: r.periodo_id,
  }));
}

/**
 * Cuenta tareas próximas a vencer (≤3 días) para alertas auto. Solo cuenta
 * los periodos pendientes del contribuyente filtrado o de todo el tenant.
 */
export async function contarTareasProximasVencer(
  pool: Pool,
  contribuyenteId: string | null | undefined,
): Promise<{ total: number; monto?: number }> {
  const safeId = contribuyenteId ? sanitizeUuid(contribuyenteId) : null;
  if (safeId) await materializarPeriodos(pool, safeId);
  const cf = safeId ? `AND tc.contribuyente_id = '${safeId}'` : '';
  const { rows: [r] } = await pool.query(
    `SELECT COUNT(*)::int AS total
       FROM tarea_periodos tp
       JOIN tareas_catalogo tc ON tc.id = tp.tarea_id
      WHERE tc.active = true
        AND tp.completada = false
        AND tp.fecha_limite >= CURRENT_DATE
        AND tp.fecha_limite <= CURRENT_DATE + INTERVAL '3 days'
        ${cf}`,
  );
  return { total: r?.total || 0 };
}

/**
 * Resuelve el auxiliar asignado a la cartera del contribuyente (si existe).
 * Busca primero en subcarteras (más específico) y luego en la top-level.
 * Retorna null si el contribuyente no está en ninguna cartera o ninguna
 * tiene auxiliar asignado.
 */
export async function getAuxiliarUserIdDeContribuyente(
  pool: Pool,
  contribuyenteId: string,
): Promise<string | null> {
  const { rows } = await pool.query<{ auxiliar_user_id: string | null; parent_id: string | null }>(
    `SELECT c.auxiliar_user_id, c.parent_id
       FROM cartera_entidades ce
       JOIN carteras c ON c.id = ce.cartera_id
      WHERE ce.entidad_id = $1
      ORDER BY (c.parent_id IS NOT NULL) DESC, c.created_at DESC`,
    [sanitizeUuid(contribuyenteId)],
  );
  for (const row of rows) {
    if (row.auxiliar_user_id) return row.auxiliar_user_id;
  }
  return null;
}

// ─── Seed de tareas default ───

const TAREAS_DEFAULT: TareaInput[] = [
  { nombre: 'Solicitar estados de cuenta', descripcion: 'Pedir al cliente los estados de cuenta bancarios del mes', recurrencia: 'mensual', diaMes: 5, orden: 1 },
  { nombre: 'Realizar conciliación bancaria', descripcion: 'Conciliar los movimientos bancarios contra los CFDIs', recurrencia: 'mensual', diaMes: 10, orden: 2 },
  { nombre: 'Realizar contabilización', descripcion: 'Registrar los CFDIs y movimientos en la contabilidad', recurrencia: 'mensual', diaMes: 14, orden: 3 },
  { nombre: 'Revisión fiscal preliminar', descripcion: 'Revisar declaración antes de presentación (solo supervisor/owner)', recurrencia: 'mensual', diaMes: 15, soloSupervisorCompleta: true, orden: 4 },
];

export async function seedTareasDefault(
  pool: Pool,
  contribuyenteId: string,
): Promise<number> {
  const existentes = await pool.query(
    `SELECT 1 FROM tareas_catalogo WHERE contribuyente_id = $1 AND es_default = true LIMIT 1`,
    [sanitizeUuid(contribuyenteId)],
  );
  if (existentes.rowCount && existentes.rowCount > 0) return 0;

  let count = 0;
  for (const t of TAREAS_DEFAULT) {
    await pool.query(
      `INSERT INTO tareas_catalogo
        (contribuyente_id, nombre, descripcion, recurrencia, dia_mes, solo_supervisor_completa, es_default, orden)
       VALUES ($1, $2, $3, $4, $5, $6, true, $7)`,
      [
        sanitizeUuid(contribuyenteId),
        t.nombre,
        t.descripcion ?? null,
        t.recurrencia,
        t.diaMes ?? null,
        t.soloSupervisorCompleta ?? false,
        t.orden ?? 0,
      ],
    );
    count++;
  }
  return count;
}
