import { prisma, tenantDb } from '../../config/database.js';
import * as mpService from './mercadopago.service.js';
import { emailService } from '../email/email.service.js';
import { auditLog } from '../../utils/audit.js';
import { getTenantOwnerEmail, getTenantOwnerEmails } from '../../utils/memberships.js';
import { filterRecipientsByRole } from '../notification-preferences.service.js';
import { isDespachoPaidPlan, permiteOverage, type DespachoPricePhase } from '@horux/shared';
import { despachoPlanTieneDualidadDb, getPrecioDespachoDb } from '../plan-catalogo.service.js';
import {
  adjustDespachoOverage,
  countActiveContribuyentesForTenant,
  cancelOverageAddonForTenant,
} from './addon.service.js';

// Simple in-memory cache with TTL
const subscriptionCache = new Map<string, { data: any; expires: number }>();

export function invalidateSubscriptionCache(tenantId: string) {
  subscriptionCache.delete(`sub:${tenantId}`);
}

/**
 * Creates a subscription record in DB and a MercadoPago preapproval
 */
export async function createSubscription(params: {
  tenantId: string;
  plan: string;
  amount: number;
  payerEmail: string;
}) {
  const tenant = await prisma.tenant.findUnique({
    where: { id: params.tenantId },
  });
  if (!tenant) throw new Error('Tenant no encontrado');

  // Create MercadoPago preapproval
  const mp = await mpService.createPreapproval({
    tenantId: params.tenantId,
    reason: `Horux360 - Plan ${params.plan} - ${tenant.nombre}`,
    amount: params.amount,
    payerEmail: params.payerEmail,
  });

  // Create DB record
  const subscription = await prisma.subscription.create({
    data: {
      tenantId: params.tenantId,
      plan: params.plan as any,
      status: mp.status || 'pending',
      amount: params.amount,
      frequency: 'monthly',
      mpPreapprovalId: mp.preapprovalId,
    },
  });

  invalidateSubscriptionCache(params.tenantId);

  return {
    subscription,
    paymentUrl: mp.initPoint,
  };
}

/**
 * Gets active subscription for a tenant (cached 5 min)
 */
export async function getActiveSubscription(tenantId: string) {
  const cached = subscriptionCache.get(`sub:${tenantId}`);
  if (cached && cached.expires > Date.now()) return cached.data;

  const subscription = await prisma.subscription.findFirst({
    where: { tenantId },
    orderBy: { createdAt: 'desc' },
  });

  subscriptionCache.set(`sub:${tenantId}`, {
    data: subscription,
    expires: Date.now() + 5 * 60 * 1000,
  });

  return subscription;
}

/**
 * Updates subscription status from webhook notification
 */
export async function updateSubscriptionStatus(mpPreapprovalId: string, status: string) {
  const subscription = await prisma.subscription.findFirst({
    where: { mpPreapprovalId },
  });
  if (!subscription) return null;

  const updated = await prisma.subscription.update({
    where: { id: subscription.id },
    data: { status },
  });

  invalidateSubscriptionCache(subscription.tenantId);

  // Handle cancellation
  if (status === 'cancelled') {
    const tenant = await prisma.tenant.findUnique({
      where: { id: subscription.tenantId },
      select: { nombre: true },
    });
    const ownerEmail = await getTenantOwnerEmail(subscription.tenantId);
    if (tenant && ownerEmail) {
      emailService.sendSubscriptionCancelled(ownerEmail, {
        nombre: tenant.nombre,
        plan: subscription.plan,
      }).catch(err => console.error('[EMAIL] Subscription cancelled notification failed:', err));
    }
  }

  return updated;
}

/**
 * Records a payment from MercadoPago webhook. Idempotente por `mpPaymentId`:
 * MP puede mandar el mismo webhook múltiples veces y solo emitimos un email
 * cuando hay transición de estado (no en cada notificación duplicada).
 */
export async function recordPayment(params: {
  tenantId: string;
  subscriptionId: string;
  mpPaymentId: string;
  amount: number;
  status: string;
  paymentMethod: string;
}) {
  // Detectar duplicados antes de insertar — `mpPaymentId` no es UNIQUE en el
  // schema (puede haber colisiones si MP reusa IDs entre flujos), pero combinado
  // con `tenantId` sí es único en la práctica.
  const existing = await prisma.payment.findFirst({
    where: { tenantId: params.tenantId, mpPaymentId: params.mpPaymentId },
  });

  const previousStatus = existing?.status ?? null;
  const statusChanged = previousStatus !== params.status;

  let payment;
  if (existing) {
    payment = await prisma.payment.update({
      where: { id: existing.id },
      data: {
        amount: params.amount,
        status: params.status,
        paymentMethod: params.paymentMethod,
        ...(params.status === 'approved' && !existing.paidAt ? { paidAt: new Date() } : {}),
      },
    });
  } else {
    payment = await prisma.payment.create({
      data: {
        tenantId: params.tenantId,
        subscriptionId: params.subscriptionId,
        mpPaymentId: params.mpPaymentId,
        amount: params.amount,
        status: params.status,
        paymentMethod: params.paymentMethod,
        ...(params.status === 'approved' ? { paidAt: new Date() } : {}),
      },
    });
  }

  // Solo notificar cuando hay transición real de estado.
  if (!statusChanged) return payment;

  const tenant = await prisma.tenant.findUnique({
    where: { id: params.tenantId },
    select: { nombre: true },
  });
  const ownerEmail = await getTenantOwnerEmail(params.tenantId);

  if (tenant && ownerEmail) {
    const subscription = await prisma.subscription.findUnique({
      where: { id: params.subscriptionId },
    });

    if (params.status === 'approved') {
      emailService.sendPaymentConfirmed(ownerEmail, {
        nombre: tenant.nombre,
        amount: params.amount,
        plan: subscription?.plan || 'N/A',
        date: new Date().toLocaleDateString('es-MX'),
      }).catch(err => console.error('[EMAIL] Payment confirmed notification failed:', err));
    } else if (params.status === 'rejected' || params.status === 'cancelled') {
      // Tanto `rejected` (banco/MP rechazó) como `cancelled` (user/sistema canceló
      // antes de cobro) ameritan aviso al owner — el efecto operativo es el mismo:
      // la suscripción no avanzó.
      emailService.sendPaymentFailed(ownerEmail, {
        nombre: tenant.nombre,
        amount: params.amount,
        plan: subscription?.plan || 'N/A',
      }).catch(err => console.error('[EMAIL] Payment failed notification failed:', err));
    }
  }

  return payment;
}

/**
 * Manually marks a subscription as paid (for bank transfers)
 */
export async function markAsPaidManually(tenantId: string, amount: number) {
  const subscription = await getActiveSubscription(tenantId);
  if (!subscription) throw new Error('No hay suscripción activa');

  // Update subscription status
  await prisma.subscription.update({
    where: { id: subscription.id },
    data: { status: 'authorized' },
  });

  // Record the manual payment
  const payment = await prisma.payment.create({
    data: {
      tenantId,
      subscriptionId: subscription.id,
      mpPaymentId: `manual-${Date.now()}`,
      amount,
      status: 'approved',
      paymentMethod: 'bank_transfer',
    },
  });

  invalidateSubscriptionCache(tenantId);
  auditLog({
    tenantId,
    action: 'payment.marked_paid_manually',
    entityType: 'Payment',
    entityId: payment.id,
    metadata: { amount, subscriptionId: subscription.id },
  });
  return payment;
}

/**
 * Generates a payment link for a tenant
 */
export async function generatePaymentLink(tenantId: string) {
  const tenant = await prisma.tenant.findUnique({ where: { id: tenantId } });
  if (!tenant) throw new Error('Tenant no encontrado');
  const ownerEmail = await getTenantOwnerEmail(tenantId);
  if (!ownerEmail) throw new Error('No admin user found');

  let subscription = await getActiveSubscription(tenantId);
  const plan = (subscription?.plan || tenant.plan) as Plan;
  if (plan === 'custom' || plan === 'trial') {
    throw new Error('No se puede generar link de pago para el plan actual');
  }

  const frequency = (subscription?.frequency as Frequency) || 'annual';
  let amount = subscription?.amount ? Number(subscription.amount) : 0;
  if (!amount) {
    amount = await getPlanPrice(plan, frequency, 'firstYear');
  }

  // Los planes Business Control / Enterprise exceden el límite de cobro recurrente
  // de MercadoPago ($10k). Para esos montos usamos una Preference de pago único
  // anual; el webhook activa el período de 1 año al aprobarse.
  if (amount > mpService.MP_PREAPPROVAL_MAX_AMOUNT) {
    if (!subscription) {
      subscription = await prisma.subscription.create({
        data: {
          tenantId,
          plan: plan as any,
          status: 'pending',
          amount,
          frequency,
        },
      });
      invalidateSubscriptionCache(tenantId);
    }

    const mp = await mpService.createSubscriptionPreference({
      tenantId,
      subscriptionId: subscription.id,
      plan,
      amount,
      payerEmail: ownerEmail,
    });

    await prisma.subscription.update({
      where: { id: subscription.id },
      data: { mpPreferenceId: mp.preferenceId, status: 'pending', amount },
    });

    return { paymentUrl: mp.checkoutUrl };
  }

  const mp = await mpService.createPreapproval({
    tenantId,
    reason: `Horux360 - Plan ${plan} - ${tenant.nombre}`,
    amount,
    payerEmail: ownerEmail,
    frequency,
  });

  if (subscription) {
    await prisma.subscription.update({
      where: { id: subscription.id },
      data: { mpPreapprovalId: mp.preapprovalId, status: mp.status || 'pending' },
    });
  } else {
    await prisma.subscription.create({
      data: {
        tenantId,
        plan: plan as any,
        status: mp.status || 'pending',
        amount,
        frequency,
        mpPreapprovalId: mp.preapprovalId,
      },
    });
    invalidateSubscriptionCache(tenantId);
  }

  return { paymentUrl: mp.initPoint };
}

/**
 * Gets payment history for a tenant
 */
export async function getPaymentHistory(tenantId: string) {
  return prisma.payment.findMany({
    where: { tenantId },
    orderBy: { createdAt: 'desc' },
    take: 50,
  });
}

// ============================================================================
// Self-serve lifecycle (trial, subscribe, change, cancel)
// ============================================================================

type Plan = 'trial' | 'custom' | 'business_control' | 'business_cloud'
          | 'mi_empresa' | 'mi_empresa_plus';
type Frequency = 'monthly' | 'annual';

/**
 * Precio vigente para un (plan, frequency, phase). Lee de BD vía
 * `despacho_plan_prices` con cache 5min — admin global edita desde
 * `/configuracion/precios-suscripcion` y los nuevos precios aplican
 * inmediatamente (cache invalidation post-edit).
 *
 * - Mi Empresa / Mi Empresa+: aceptan `monthly` o `annual` (anual = 10
 *   meses, descuento ~17%).
 * - Business Control / Enterprise: solo `annual` (falla si monthly).
 * - `custom`: no tiene precio en catálogo, el admin lo fija por tenant.
 */
export async function getPlanPrice(
  plan: Plan,
  frequency: Frequency,
  phase: DespachoPricePhase = 'renewal',
): Promise<number> {
  if (plan === 'custom') {
    throw new Error('El plan custom no tiene precio en plan_prices — usa createTenant con amount explícito');
  }
  return getPrecioDespachoDb(plan, frequency, phase);
}

/**
 * Activa prueba gratuita de 30 días. Una sola vez **por RFC** y, si se pasa
 * `ownerUserId`, también una sola vez **por humano** — un mismo dueño no puede
 * obtener trials nuevos creando RFCs adicionales.
 *
 * Gates:
 *   1. Plan no puede ser custom
 *   2. Tenant no puede tener ya un `trialEndsAt` (su propia prueba en curso o consumida)
 *   3. RFC normalizado NO debe existir en `trial_usages`
 *   4. Si `ownerUserId`: ningún otro tenant donde el user es owner debe tener
 *      `trialEndsAt` (cada humano tiene derecho a 1 sola prueba)
 *   5. No hay otra suscripción activa/pendiente/trial para este tenant
 *
 * Inserta el RFC en `trial_usages` dentro de la transacción — si algo falla, rollback
 * deja el padrón sin la marca (consistente con la no-creación del trial).
 */
export async function startTrial(params: {
  tenantId: string;
  plan: Plan;
  frequency: Frequency;
  ownerUserId?: string;
}): Promise<{ subscription: any; trialEndsAt: Date }> {
  if (params.plan === 'custom') {
    throw new Error('No se puede iniciar trial en plan custom');
  }

  const tenant = await prisma.tenant.findUnique({ where: { id: params.tenantId } });
  if (!tenant) throw new Error('Tenant no encontrado');
  if (tenant.trialEndsAt) throw new Error('Este tenant ya usó su prueba gratuita');

  // Gate persistente: RFC ya consumió trial en algún tenant (actual o previo)
  const normalizedRfc = tenant.rfc.toUpperCase();
  const priorUsage = await prisma.trialUsage.findUnique({
    where: { rfc: normalizedRfc },
  });
  if (priorUsage) {
    throw new Error(
      `El RFC ${normalizedRfc} ya consumió su prueba gratuita. ` +
      `Cada RFC tiene derecho a una sola prueba de 30 días. Contrata un plan para continuar.`
    );
  }

  // Gate por owner: el mismo humano no puede usar trial dos veces creando RFCs
  // distintos. Cubre el escenario "borro tenant, creo otro, pido trial otra vez"
  // y "agrego segundo RFC bajo mi cuenta y pido trial".
  if (params.ownerUserId) {
    const ownedTenantWithTrial = await prisma.tenantMembership.findFirst({
      where: {
        userId: params.ownerUserId,
        isOwner: true,
        active: true,
        tenantId: { not: params.tenantId },
        tenant: { trialEndsAt: { not: null } },
      },
      select: { tenant: { select: { rfc: true } } },
    });
    if (ownedTenantWithTrial) {
      throw new Error(
        `Ya consumiste una prueba gratuita con otro RFC (${ownedTenantWithTrial.tenant.rfc}). ` +
        `Cada dueño tiene derecho a una sola prueba de 30 días. Para esta empresa contrata un plan directamente.`
      );
    }
  }

  const existing = await prisma.subscription.findFirst({
    where: { tenantId: params.tenantId, status: { in: ['trial', 'pending', 'authorized', 'paused'] } },
  });
  if (existing) throw new Error('Ya existe una suscripción activa o pendiente');

  const trialEndsAt = new Date();
  trialEndsAt.setDate(trialEndsAt.getDate() + 30);

  const now = new Date();
  const subscription = await prisma.$transaction(async (tx) => {
    await tx.tenant.update({
      where: { id: params.tenantId },
      data: { trialEndsAt, plan: params.plan },
    });
    // Registra el RFC en el padrón — unique constraint previene race condition
    await tx.trialUsage.create({
      data: {
        rfc: normalizedRfc,
        tenantId: params.tenantId,
      },
    });
    return tx.subscription.create({
      data: {
        tenantId: params.tenantId,
        plan: params.plan,
        status: 'trial',
        amount: 0,
        frequency: params.frequency,
        currentPeriodStart: now,
        currentPeriodEnd: trialEndsAt,
      },
    });
  });

  invalidateSubscriptionCache(params.tenantId);
  auditLog({
    tenantId: params.tenantId,
    action: 'trial.started',
    entityType: 'Subscription',
    entityId: subscription.id,
    metadata: { plan: params.plan, frequency: params.frequency, rfc: normalizedRfc, trialEndsAt: trialEndsAt.toISOString() },
  });
  console.log(`[Trial] Iniciado para tenant ${params.tenantId} (RFC ${normalizedRfc}), vence ${trialEndsAt.toISOString()}`);
  return { subscription, trialEndsAt };
}

/**
 * Crea una suscripción self-serve (el usuario eligió plan + frecuencia).
 * Lee precio de `plan_prices`, crea preapproval en MP, retorna paymentUrl.
 *
 * Falla si ya hay suscripción activa/pendiente o trial no vencido. Para cambiar
 * de plan durante una suscripción activa, usa `scheduleChange`.
 */
export async function subscribe(params: {
  tenantId: string;
  plan: Plan;
  frequency: Frequency;
  payerEmail: string;
}): Promise<{ subscription: any; paymentUrl: string }> {
  if (params.plan === 'custom') {
    throw new Error('Plan custom no es self-serve — lo activa el admin global al crear tenant');
  }

  const tenant = await prisma.tenant.findUnique({ where: { id: params.tenantId } });
  if (!tenant) throw new Error('Tenant no encontrado');

  const existing = await prisma.subscription.findFirst({
    where: {
      tenantId: params.tenantId,
      status: { in: ['authorized', 'pending', 'paused'] },
    },
  });
  if (existing) {
    throw new Error('Ya existe una suscripción activa o pendiente — usa "Cambiar plan" para modificar');
  }

  // En planes despacho con dualidad (business_control: $21K primer año, $15K
  // renovaciones) creamos el preapproval con `firstYear`. Tras el primer pago
  // aprobado, el webhook llama `updatePreapprovalAmount` para bajar al monto
  // de renewal en los siguientes cobros. El `reason` explica ambos montos al
  // usuario en la pantalla de autorización MP.
  const amount = await getPlanPrice(params.plan, params.frequency, 'firstYear');
  const hasDualidad = isDespachoPaidPlan(params.plan) && await despachoPlanTieneDualidadDb(params.plan);
  const renewalAmount = hasDualidad
    ? await getPlanPrice(params.plan, params.frequency, 'renewal')
    : amount;
  const reason = hasDualidad
    ? `${tenant.nombre} - Plan ${params.plan} - $${amount.toLocaleString('es-MX')} primer año, $${renewalAmount.toLocaleString('es-MX')} renovaciones`
    : `Horux360 - Plan ${params.plan} (${params.frequency}) - ${tenant.nombre}`;

  // Planes Business Control / Enterprise superan el límite de cobro recurrente
  // de MercadoPago ($10k). Se cobra el año completo vía Preference one-off; el
  // webhook activa el período anual tras el primer pago aprobado.
  if (amount > mpService.MP_PREAPPROVAL_MAX_AMOUNT) {
    const subscription = await prisma.subscription.create({
      data: {
        tenantId: params.tenantId,
        plan: params.plan,
        status: 'pending',
        amount,
        frequency: params.frequency,
      },
    });

    const mp = await mpService.createSubscriptionPreference({
      tenantId: params.tenantId,
      subscriptionId: subscription.id,
      plan: params.plan,
      amount,
      payerEmail: params.payerEmail,
    });

    await prisma.subscription.update({
      where: { id: subscription.id },
      data: { mpPreferenceId: mp.preferenceId },
    });

    await prisma.subscription.updateMany({
      where: { tenantId: params.tenantId, status: 'trial' },
      data: { status: 'trial_converted' },
    });

    await prisma.tenant.update({
      where: { id: params.tenantId },
      data: { plan: params.plan },
    });

    invalidateSubscriptionCache(params.tenantId);
    auditLog({
      tenantId: params.tenantId,
      action: 'subscription.created',
      entityType: 'Subscription',
      entityId: subscription.id,
      metadata: { plan: params.plan, frequency: params.frequency, amount, paymentMethod: 'preference' },
    });
    return { subscription, paymentUrl: mp.checkoutUrl };
  }

  const mp = await mpService.createPreapproval({
    tenantId: params.tenantId,
    reason,
    amount,
    payerEmail: params.payerEmail,
    frequency: params.frequency,
  });

  // Si había un trial activo, lo marca como completed (no cancelled: el trial terminó exitosamente)
  await prisma.subscription.updateMany({
    where: { tenantId: params.tenantId, status: 'trial' },
    data: { status: 'trial_converted' },
  });

  const subscription = await prisma.subscription.create({
    data: {
      tenantId: params.tenantId,
      plan: params.plan,
      status: mp.status || 'pending',
      amount,
      frequency: params.frequency,
      mpPreapprovalId: mp.preapprovalId,
    },
  });

  await prisma.tenant.update({
    where: { id: params.tenantId },
    data: { plan: params.plan },
  });

  invalidateSubscriptionCache(params.tenantId);
  auditLog({
    tenantId: params.tenantId,
    action: 'subscription.created',
    entityType: 'Subscription',
    entityId: subscription.id,
    metadata: { plan: params.plan, frequency: params.frequency, amount },
  });
  return { subscription, paymentUrl: mp.initPoint };
}

/**
 * Calcula el monto a cobrar por un upgrade prorateado.
 *
 *   proration = (newAmount - currentAmount) * (daysRemaining / periodDays)
 *
 * Redondeado a 2 decimales. Si no hay días restantes (período vencido), retorna 0
 * — el caller debe caer en scheduleChange en vez de upgrade inmediato.
 */
export function calculateProration(
  currentAmount: number,
  newAmount: number,
  periodStart: Date | null,
  periodEnd: Date | null,
): { amount: number; daysRemaining: number; periodDays: number } {
  if (!periodStart || !periodEnd) return { amount: 0, daysRemaining: 0, periodDays: 0 };
  const now = Date.now();
  const endMs = periodEnd.getTime();
  if (endMs <= now) return { amount: 0, daysRemaining: 0, periodDays: 0 };

  const msPerDay = 1000 * 60 * 60 * 24;
  const daysRemaining = Math.max(0, Math.ceil((endMs - now) / msPerDay));
  const periodDays = Math.max(1, Math.ceil((endMs - periodStart.getTime()) / msPerDay));
  const fraction = Math.min(1, daysRemaining / periodDays);
  const diff = Math.max(0, newAmount - currentAmount);
  const amount = Math.round(diff * fraction * 100) / 100;
  return { amount, daysRemaining, periodDays };
}

/**
 * Inicia un upgrade con cobro prorateado inmediato.
 *
 * Flujo:
 *   1. Valida que sea estrictamente un upgrade (precio nuevo > precio actual, misma frecuencia)
 *   2. Calcula el prorateo por días restantes del período actual
 *   3. Crea una Preference de MercadoPago para ese monto one-time
 *   4. Guarda en Subscription: upgradePreferenceId + upgradeTargetPlan + upgradeTargetAmount
 *   5. Retorna checkoutUrl — el cliente lo abre en nueva pestaña, el usuario paga
 *   6. El webhook detecta `external_reference: proration:*` y llama `applyApprovedUpgrade`
 *
 * Si falla antes de crear la preference, no hay estado que revertir. Si falla después
 * del MP call pero antes del DB update, la preference queda huérfana en MP (expirará sola).
 */
export async function initiateUpgrade(params: {
  tenantId: string;
  newPlan: Plan;
  payerEmail: string;
}): Promise<{ subscription: any; checkoutUrl: string; proratedAmount: number }> {
  if (params.newPlan === 'custom') {
    throw new Error('No se puede upgrade a plan custom — lo asigna el admin global');
  }

  const active = await prisma.subscription.findFirst({
    where: {
      tenantId: params.tenantId,
      status: { in: ['authorized', 'trial'] },
    },
    orderBy: { createdAt: 'desc' },
  });
  if (!active) throw new Error('No hay suscripción activa para upgrade');
  if (active.upgradePreferenceId) {
    throw new Error('Ya hay un upgrade en curso — cancélalo antes de iniciar otro');
  }

  const currentFrequency = (active.frequency as Frequency) || 'monthly';
  const newAmount = await getPlanPrice(params.newPlan, currentFrequency);
  const currentAmount = Number(active.amount);

  if (newAmount <= currentAmount) {
    throw new Error('El plan seleccionado no es un upgrade (precio menor o igual). Usa scheduleChange para downgrades.');
  }

  const { amount: proratedAmount, daysRemaining } = calculateProration(
    currentAmount,
    newAmount,
    active.currentPeriodStart,
    active.currentPeriodEnd,
  );
  if (proratedAmount <= 0) {
    throw new Error('No hay días restantes del período actual para prorratear — espera a que termine y contrata el nuevo plan');
  }

  const tenant = await prisma.tenant.findUnique({ where: { id: params.tenantId } });
  if (!tenant) throw new Error('Tenant no encontrado');

  const description = `Upgrade a ${params.newPlan} — prorateo ${daysRemaining} día${daysRemaining !== 1 ? 's' : ''} restante${daysRemaining !== 1 ? 's' : ''} del período actual`;

  const { preferenceId, checkoutUrl } = await mpService.createProrationPreference({
    tenantId: params.tenantId,
    subscriptionId: active.id,
    amount: proratedAmount,
    description,
    payerEmail: params.payerEmail,
  });

  const updated = await prisma.subscription.update({
    where: { id: active.id },
    data: {
      upgradePreferenceId: preferenceId,
      upgradeTargetPlan: params.newPlan,
      upgradeTargetAmount: newAmount,
    },
  });

  invalidateSubscriptionCache(params.tenantId);

  console.log(`[Upgrade] Iniciado para tenant ${params.tenantId}: ${active.plan}→${params.newPlan} (${currentFrequency}). Prorateo: $${proratedAmount}. Preference: ${preferenceId}`);

  return { subscription: updated, checkoutUrl, proratedAmount };
}

/**
 * Aplica un upgrade cuyo cobro prorateado fue aprobado por MercadoPago (llamado desde webhook).
 *
 * Acciones:
 *   1. Actualiza el monto recurrente del preapproval existente al nuevo precio
 *   2. Actualiza Subscription: plan, amount, status=authorized, limpia campos de upgrade
 *   3. Actualiza Tenant.plan
 *
 * Si el paso 1 falla (MP API down), re-lanza para que el webhook no consuma el evento —
 * MP reintentará. El paso 2 y 3 deben ser atómicos vía transacción.
 */
export async function applyApprovedUpgrade(subscriptionId: string): Promise<void> {
  const sub = await prisma.subscription.findUnique({
    where: { id: subscriptionId },
  });
  if (!sub) throw new Error(`Subscription ${subscriptionId} no encontrada`);
  if (!sub.upgradeTargetPlan || !sub.upgradeTargetAmount) {
    console.warn(`[Upgrade] Sub ${subscriptionId} sin campos upgradeTarget* — probable webhook duplicado o race, ignorando`);
    return;
  }

  const newPlan = sub.upgradeTargetPlan as Plan;
  const newAmount = Number(sub.upgradeTargetAmount);

  // Actualiza el monto del preapproval en MP (si existe). Si el nuevo monto
  // supera el límite de cobro recurrente de MP ($10k), cancelamos el preapproval
  // anterior: el plan alto se cobrará anualmente vía Preference one-off.
  if (sub.mpPreapprovalId) {
    if (newAmount > mpService.MP_PREAPPROVAL_MAX_AMOUNT) {
      await mpService.cancelPreapproval(sub.mpPreapprovalId);
      console.log(`[Upgrade] Preapproval ${sub.mpPreapprovalId} cancelado porque el nuevo monto $${newAmount} supera el límite de MP`);
    } else {
      try {
        await mpService.updatePreapprovalAmount(sub.mpPreapprovalId, newAmount);
      } catch (error: any) {
        console.error(`[Upgrade] Error actualizando preapproval ${sub.mpPreapprovalId}:`, error.message);
        throw error; // Re-lanza para que MP reintente el webhook
      }
    }
  }

  await prisma.$transaction([
    prisma.subscription.update({
      where: { id: sub.id },
      data: {
        plan: newPlan,
        amount: newAmount,
        status: 'authorized',
        upgradePreferenceId: null,
        upgradeTargetPlan: null,
        upgradeTargetAmount: null,
      },
    }),
    prisma.tenant.update({
      where: { id: sub.tenantId },
      data: { plan: newPlan },
    }),
  ]);

  invalidateSubscriptionCache(sub.tenantId);
  auditLog({
    tenantId: sub.tenantId,
    action: 'subscription.plan_changed',
    entityType: 'Subscription',
    entityId: sub.id,
    metadata: {
      kind: 'upgrade_immediate',
      fromPlan: sub.plan,
      toPlan: newPlan,
      frequency: sub.frequency,
      newAmount,
    },
  });
  console.log(`[Upgrade] Aplicado exitosamente para tenant ${sub.tenantId}: ${sub.plan}→${newPlan} ($${newAmount}/${sub.frequency})`);

  // Reajusta el overage por contribuyente extra al nuevo plan: si pasa de
  // un plan sin overage (mi_empresa, etc.) a uno con overage (business_*),
  // crea el addon. Si pasa al revés, lo cancela. Fail-soft.
  await reconcileOverageAfterPlanChange(sub.tenantId, sub.plan, newPlan);
}

/**
 * Después de cualquier cambio de plan (upgrade, scheduled change aplicado,
 * cancelación), ajusta el add-on de overage según corresponda al nuevo plan.
 * Fail-soft: cualquier error se logea sin propagar.
 */
async function reconcileOverageAfterPlanChange(
  tenantId: string,
  fromPlan: string,
  toPlan: string,
): Promise<void> {
  try {
    if (permiteOverage(toPlan)) {
      const count = await countActiveContribuyentesForTenant(tenantId);
      const result = await adjustDespachoOverage(tenantId, count);
      if (result.action !== 'none' && result.action !== 'skipped') {
        console.log(`[Overage] Reconcile ${fromPlan}→${toPlan} (tenant ${tenantId}): ${result.action} (count=${result.overageCount})`);
      }
    } else {
      // Plan nuevo NO permite overage → cancelar addon si existía.
      const r = await cancelOverageAddonForTenant(tenantId);
      if (r.cancelled) {
        console.log(`[Overage] Cancelado tras cambio a plan ${toPlan} sin overage (tenant ${tenantId})`);
      }
    }
  } catch (err: any) {
    console.error(`[Overage] Reconcile ${fromPlan}→${toPlan} (tenant ${tenantId}) fallo:`, err.message || err);
  }
}

/**
 * Aborta un upgrade en curso (el usuario cambió de opinión antes de pagar la preference).
 * Simplemente limpia los campos — MP dejará expirar la preference sola.
 */
export async function cancelPendingUpgrade(tenantId: string): Promise<void> {
  const sub = await prisma.subscription.findFirst({
    where: { tenantId, upgradePreferenceId: { not: null } },
  });
  if (!sub) throw new Error('No hay upgrade en curso para este tenant');

  await prisma.subscription.update({
    where: { id: sub.id },
    data: {
      upgradePreferenceId: null,
      upgradeTargetPlan: null,
      upgradeTargetAmount: null,
    },
  });
  invalidateSubscriptionCache(tenantId);
  console.log(`[Upgrade] Cancelado pendiente para tenant ${tenantId}`);
}

/**
 * Programa un cambio de plan o frecuencia al próximo período.
 * Se aplica por el cron `applyPendingChanges` cuando `pendingEffectiveAt` llega.
 *
 * Usado para downgrades y cambios de frecuencia. Upgrades con misma frecuencia
 * deben ir por `initiateUpgrade` (cobro prorateado inmediato).
 */
export async function scheduleChange(params: {
  tenantId: string;
  newPlan: Plan;
  newFrequency: Frequency;
}): Promise<{ subscription: any; effectiveAt: Date }> {
  if (params.newPlan === 'custom') {
    throw new Error('No se puede cambiar a plan custom — lo asigna el admin global');
  }

  const active = await prisma.subscription.findFirst({
    where: {
      tenantId: params.tenantId,
      status: { in: ['authorized', 'trial'] },
    },
    orderBy: { createdAt: 'desc' },
  });
  if (!active) throw new Error('No hay suscripción activa para cambiar');

  if (active.plan === params.newPlan && active.frequency === params.newFrequency) {
    throw new Error('El plan y frecuencia son iguales a los actuales');
  }

  // Valida que el nuevo plan/frecuencia tenga precio
  await getPlanPrice(params.newPlan, params.newFrequency);

  // Si no hay currentPeriodEnd (raro — trial en curso u otro caso),
  // programa el cambio para mañana como salvaguarda
  const effectiveAt = active.currentPeriodEnd || new Date(Date.now() + 24 * 60 * 60 * 1000);

  const updated = await prisma.subscription.update({
    where: { id: active.id },
    data: {
      pendingPlan: params.newPlan,
      pendingFrequency: params.newFrequency,
      pendingEffectiveAt: effectiveAt,
    },
  });

  invalidateSubscriptionCache(params.tenantId);
  auditLog({
    tenantId: params.tenantId,
    action: 'subscription.plan_changed',
    entityType: 'Subscription',
    entityId: updated.id,
    metadata: {
      kind: 'scheduled',
      fromPlan: active.plan,
      toPlan: params.newPlan,
      fromFrequency: active.frequency,
      toFrequency: params.newFrequency,
      effectiveAt: effectiveAt.toISOString(),
    },
  });
  return { subscription: updated, effectiveAt };
}

/**
 * Reactiva una suscripción cancelada que aún está dentro de su período pagado.
 *
 * MP preapproval cancelado es terminal — no se puede revivir. Esta función crea
 * un preapproval nuevo con los mismos parámetros (plan/amount/frequency) y
 * `start_date = currentPeriodEnd` para que el primer cobro caiga al final del
 * período ya pagado (evita doble cobro).
 *
 * Resultado: subscription con status=pending + nuevo mpPreapprovalId. El usuario
 * debe abrir paymentUrl y autorizar en MP. Al autorizar, webhook → authorized.
 *
 * Validaciones:
 *   - Debe existir una subscription status=cancelled con currentPeriodEnd en el futuro
 *   - Si el período ya venció, redirige al flujo normal de subscribe (picker)
 */
export async function reactivateSubscription(params: {
  tenantId: string;
  payerEmail: string;
}): Promise<{ subscription: any; paymentUrl: string }> {
  const cancelled = await prisma.subscription.findFirst({
    where: {
      tenantId: params.tenantId,
      status: 'cancelled',
    },
    orderBy: { createdAt: 'desc' },
  });
  if (!cancelled) {
    throw new Error('No hay suscripción cancelada para reactivar');
  }

  const now = new Date();
  if (!cancelled.currentPeriodEnd || cancelled.currentPeriodEnd.getTime() <= now.getTime()) {
    throw new Error('El período pagado ya venció — contrata un nuevo plan desde el selector');
  }

  if (cancelled.plan === 'custom') {
    throw new Error('Reactivación de plan custom requiere coordinación con el admin global');
  }

  const tenant = await prisma.tenant.findUnique({ where: { id: params.tenantId } });
  if (!tenant) throw new Error('Tenant no encontrado');

  const amount = Number(cancelled.amount);
  const frequency = (cancelled.frequency as Frequency) || 'monthly';

  const mp = await mpService.createPreapproval({
    tenantId: params.tenantId,
    reason: `Horux360 - Reactivación Plan ${cancelled.plan} (${frequency}) - ${tenant.nombre}`,
    amount,
    payerEmail: params.payerEmail,
    frequency,
    startDate: cancelled.currentPeriodEnd, // Primer cobro al final del período actual
  });

  const updated = await prisma.subscription.update({
    where: { id: cancelled.id },
    data: {
      status: 'pending',
      mpPreapprovalId: mp.preapprovalId,
      // Limpia cualquier upgrade/change pendiente que hubiera antes de cancelar
      pendingPlan: null,
      pendingFrequency: null,
      pendingEffectiveAt: null,
      upgradePreferenceId: null,
      upgradeTargetPlan: null,
      upgradeTargetAmount: null,
    },
  });

  // Asegura que tenant.plan refleje el plan reactivado
  await prisma.tenant.update({
    where: { id: params.tenantId },
    data: { plan: cancelled.plan },
  });

  invalidateSubscriptionCache(params.tenantId);
  auditLog({
    tenantId: params.tenantId,
    action: 'subscription.reactivated',
    entityType: 'Subscription',
    entityId: updated.id,
    metadata: { plan: cancelled.plan, frequency, nextChargeAt: cancelled.currentPeriodEnd!.toISOString() },
  });

  console.log(`[Reactivate] Tenant ${params.tenantId}: ${cancelled.plan} (${frequency}), próximo cobro: ${cancelled.currentPeriodEnd.toISOString()}`);

  return { subscription: updated, paymentUrl: mp.initPoint };
}

/**
 * Cancela la suscripción activa. El acceso continúa hasta `currentPeriodEnd`
 * (el middleware `plan-limits` sigue respetando `status in (authorized, cancelled)`
 * con periodo vigente — no requiere cambio).
 *
 * También cancela el preapproval en MercadoPago para que no se siga cobrando.
 */
export async function cancelSubscription(tenantId: string): Promise<{ subscription: any }> {
  const active = await prisma.subscription.findFirst({
    where: {
      tenantId,
      status: { in: ['authorized', 'trial', 'pending', 'paused'] },
    },
    orderBy: { createdAt: 'desc' },
  });
  if (!active) throw new Error('No hay suscripción activa para cancelar');

  // Cancela el addon de overage primero (antes de marcar la sub como cancelled
  // para que el lookup de la sub aún la encuentre activa). Fail-soft.
  try {
    const r = await cancelOverageAddonForTenant(tenantId);
    if (r.cancelled) console.log(`[Overage] Cancelado por cancelación de suscripción (tenant ${tenantId})`);
  } catch (err: any) {
    console.error(`[Overage] Error cancelando addon en cancelSubscription (tenant ${tenantId}):`, err.message || err);
  }

  if (active.mpPreapprovalId) {
    await mpService.cancelPreapproval(active.mpPreapprovalId);
  }

  const updated = await prisma.subscription.update({
    where: { id: active.id },
    data: {
      status: 'cancelled',
      pendingPlan: null,
      pendingFrequency: null,
      pendingEffectiveAt: null,
    },
  });

  invalidateSubscriptionCache(tenantId);
  auditLog({
    tenantId,
    action: 'subscription.cancelled',
    entityType: 'Subscription',
    entityId: updated.id,
    metadata: { plan: active.plan, currentPeriodEnd: active.currentPeriodEnd?.toISOString() },
  });

  // Email notificación (non-blocking)
  const tenant = await prisma.tenant.findUnique({
    where: { id: tenantId },
    select: { nombre: true },
  });
  const ownerEmail = await getTenantOwnerEmail(tenantId);
  if (tenant && ownerEmail) {
    emailService.sendSubscriptionCancelled(ownerEmail, {
      nombre: tenant.nombre,
      plan: active.plan,
    }).catch(err => console.error('[EMAIL] Cancellation email failed:', err));
  }

  return { subscription: updated };
}

// ============================================================================
// Cron helpers: apply pending changes + expire trials
// ============================================================================

/**
 * Aplica cambios de plan programados cuyo `pendingEffectiveAt` ya pasó.
 * Llamado por cron diario.
 *
 * Para cada cambio pendiente:
 *   1. Cancela el preapproval viejo en MP
 *   2. Crea nuevo preapproval con el nuevo plan/frecuencia/monto
 *   3. Actualiza la Subscription a `pending` (esperando que el usuario autorice el nuevo preapproval)
 *
 * Nota: si el usuario no autoriza el nuevo preapproval rápidamente, el middleware
 * `plan-limits` lo trata como pending — aún con acceso según período previo.
 */
export async function applyPendingChanges(): Promise<{ applied: number; errors: number }> {
  const now = new Date();
  const pending = await prisma.subscription.findMany({
    where: {
      pendingEffectiveAt: { lte: now, not: null },
      pendingPlan: { not: null },
      status: { in: ['authorized', 'trial'] },
    },
    include: { tenant: true },
  });

  let applied = 0;
  let errors = 0;

  for (const sub of pending) {
    try {
      const newPlan = sub.pendingPlan as Plan;
      const newFrequency = (sub.pendingFrequency || 'monthly') as Frequency;
      const newAmount = await getPlanPrice(newPlan, newFrequency);

      const adminEmail = await getTenantOwnerEmail(sub.tenantId);
      if (!adminEmail) {
        console.error(`[Pending] Sub ${sub.id} sin admin user — omito`);
        errors++;
        continue;
      }

      // Cancelar preapproval viejo
      if (sub.mpPreapprovalId) {
        await mpService.cancelPreapproval(sub.mpPreapprovalId);
      }

      // Crear preapproval nuevo
      const mp = await mpService.createPreapproval({
        tenantId: sub.tenantId,
        reason: `Horux360 - Plan ${newPlan} (${newFrequency}) - ${sub.tenant.nombre}`,
        amount: newAmount,
        payerEmail: adminEmail,
        frequency: newFrequency,
      });

      await prisma.$transaction([
        prisma.subscription.update({
          where: { id: sub.id },
          data: {
            plan: newPlan,
            frequency: newFrequency,
            amount: newAmount,
            status: 'pending',
            mpPreapprovalId: mp.preapprovalId,
            pendingPlan: null,
            pendingFrequency: null,
            pendingEffectiveAt: null,
            currentPeriodStart: now,
            // currentPeriodEnd se actualizará al recibir el webhook de authorization
          },
        }),
        prisma.tenant.update({
          where: { id: sub.tenantId },
          data: { plan: newPlan },
        }),
      ]);

      invalidateSubscriptionCache(sub.tenantId);

      // Reajusta el overage según el nuevo plan (fail-soft).
      await reconcileOverageAfterPlanChange(sub.tenantId, sub.plan, newPlan);

      applied++;
      console.log(`[Pending] Aplicado cambio para tenant ${sub.tenantId}: ${sub.plan}→${newPlan} (${newFrequency})`);
    } catch (error: any) {
      console.error(`[Pending] Error en sub ${sub.id}:`, error.message);
      errors++;
    }
  }

  return { applied, errors };
}

/**
 * Cron diario de avisos pre-vencimiento. Itera suscripciones cuyo `currentPeriodEnd`
 * está dentro de los próximos 7 días (o el día mismo del vencimiento). Por cada una
 * envía un email al owner con el bucket apropiado (7d, 3d, 1d, 0d) y guarda el bucket
 * en `lastReminderDay` para no duplicar.
 *
 * Idempotencia:
 *  - Si el bucket actual es menor o igual al guardado, ya se notificó este bucket → skip.
 *  - Si el bucket actual es MAYOR que el guardado, el período rolló (renovación) — se
 *    actualiza `lastReminderDay` al nuevo bucket pero NO se envía email (el período
 *    nuevo está lejos, no hay nada que avisar). Próximas corridas avisarán al bajar.
 *
 * Buckets (días restantes): 7 → 3 → 1 → 0 (post-vencimiento, último aviso de cortesía).
 * Trial usa template `trialReminder`/`trialExpired`; suscripciones de pago usan
 * `subscriptionExpiring`. `cancelled` dentro de período también recibe aviso (es la
 * señal final antes de pagar de menos).
 */
export async function sendExpiryReminders(): Promise<{ sent: number; resetOnly: number; skipped: number; errors: number }> {
  const now = new Date();
  const sevenDaysFromNow = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
  const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);

  // Solo avisamos a tenants que estén en alguno de estos estados — pending y
  // paused no aplican (no han llegado al primer cobro o están suspendidas por MP).
  // trial_expired y trial post-expiry caen en el bucket 0 si están dentro del
  // último día — ahí mandamos el aviso final de "tu prueba terminó".
  const candidates = await prisma.subscription.findMany({
    where: {
      OR: [
        // Activos por vencer
        { status: { in: ['authorized', 'trial', 'cancelled'] }, currentPeriodEnd: { lte: sevenDaysFromNow, gte: oneDayAgo } },
        // Trial recién vencido (último aviso 0d)
        { status: 'trial_expired', currentPeriodEnd: { gte: oneDayAgo } },
      ],
    },
    include: { tenant: { select: { nombre: true, rfc: true, databaseName: true } } },
  });

  let sent = 0;
  let resetOnly = 0;
  let skipped = 0;
  let errors = 0;

  for (const sub of candidates) {
    if (!sub.currentPeriodEnd) continue;

    // Calcula el bucket actual de días restantes (al cero más cercano hacia abajo).
    const msUntil = sub.currentPeriodEnd.getTime() - now.getTime();
    const daysUntil = Math.ceil(msUntil / (24 * 60 * 60 * 1000));
    let bucket: number | null = null;
    if (daysUntil <= 0) bucket = 0;
    else if (daysUntil <= 1) bucket = 1;
    else if (daysUntil <= 3) bucket = 3;
    else if (daysUntil <= 7) bucket = 7;
    if (bucket === null) {
      skipped++;
      continue;
    }

    const lastBucket = sub.lastReminderDay;

    // Período renovado (ej. lastBucket=0 y bucket=7) — actualiza el tracker pero no envía.
    if (lastBucket !== null && bucket > lastBucket) {
      await prisma.subscription.update({
        where: { id: sub.id },
        data: { lastReminderDay: bucket, lastReminderSentAt: now },
      });
      resetOnly++;
      continue;
    }

    // Mismo o menor — ya se notificó este bucket o más cercano.
    if (lastBucket !== null && bucket >= lastBucket) {
      skipped++;
      continue;
    }

    // Hay algo que avisar.
    try {
      // Para suscripciones de pago, respeta preferencia 'subscription_expiring' del rol owner.
      // Para trials siempre avisa al owner (no depende de preferencias de notificación informativa).
      const isTrialFlow = sub.status === 'trial' || sub.status === 'trial_expired';
      let emailsToNotify: string[] = [];

      if (isTrialFlow) {
        const ownerEmail = await getTenantOwnerEmail(sub.tenantId);
        if (ownerEmail) emailsToNotify = [ownerEmail];
      } else {
        const pool = await tenantDb.getPool(sub.tenantId, sub.tenant.databaseName);
        const ownerEmails = await getTenantOwnerEmails(sub.tenantId);
        const recipientsWithRole = ownerEmails.map(email => ({ email, role: 'owner' as const }));
        emailsToNotify = await filterRecipientsByRole(pool, 'subscription_expiring', recipientsWithRole);
      }

      if (emailsToNotify.length === 0) {
        skipped++;
        continue;
      }

      for (const ownerEmail of emailsToNotify) {
        if (isTrialFlow) {
          if (bucket === 0) {
            await emailService.sendTrialExpired(ownerEmail, {
              nombre: sub.tenant.nombre,
              despachoNombre: sub.tenant.nombre,
            });
          } else {
            await emailService.sendTrialReminder(ownerEmail, {
              nombre: sub.tenant.nombre,
              despachoNombre: sub.tenant.nombre,
              diasRestantes: Math.max(0, daysUntil),
              wizardCompleto: true,
            });
          }
        } else {
          await emailService.sendSubscriptionExpiring(ownerEmail, {
            nombre: sub.tenant.nombre,
            plan: sub.plan,
            expiresAt: sub.currentPeriodEnd.toLocaleDateString('es-MX', { dateStyle: 'long' }),
          });
        }
      }

      await prisma.subscription.update({
        where: { id: sub.id },
        data: { lastReminderDay: bucket, lastReminderSentAt: now },
      });
      sent++;
    } catch (err: any) {
      console.error(`[ExpiryReminder] Error en sub ${sub.id}:`, err.message || err);
      errors++;
    }
  }

  return { sent, resetOnly, skipped, errors };
}

/**
 * Transiciona trials vencidos (`Tenant.trialEndsAt` ya pasó) a `pending`.
 * El usuario debe agregar método de pago para continuar — el middleware `plan-limits`
 * empezará a restringir features.
 */
export async function expireTrials(): Promise<{ expired: number }> {
  const now = new Date();

  const expiredTrials = await prisma.subscription.findMany({
    where: { status: 'trial', currentPeriodEnd: { lt: now } },
  });

  let expired = 0;
  for (const sub of expiredTrials) {
    await prisma.subscription.update({
      where: { id: sub.id },
      data: { status: 'trial_expired' },
    });
    invalidateSubscriptionCache(sub.tenantId);
    expired++;
    console.log(`[Trial] Expiró trial de tenant ${sub.tenantId} (plan ${sub.plan})`);
  }

  return { expired };
}
