import type { Request, Response, NextFunction } from 'express';
import { z } from 'zod';
import * as contribuyenteService from '../services/contribuyente.service.js';
import { AppError } from '../middlewares/error.middleware.js';
import { getEntidadesVisibles } from '../utils/entidades-visibles.js';
import { adjustDespachoOverage } from '../services/payment/addon.service.js';
import { prisma } from '../config/database.js';

/**
 * Límite duro de contribuyentes mientras el despacho está en trial gratuito.
 * Una vez expira el trial (`trialEndsAt < now`) este límite deja de aplicar y
 * el plan vigente toma el control.
 */
const TRIAL_MAX_CONTRIBUYENTES = 5;

/**
 * Cuenta contribuyentes activos del tenant actual. Usado para ajustar el
 * overage de Business Control / Enterprise tras crear o desactivar un RFC,
 * y para enforce el límite del trial.
 */
async function countActiveContribuyentes(pool: import('pg').Pool): Promise<number> {
  const { rows: [{ cnt }] } = await pool.query<{ cnt: string }>(
    `SELECT COUNT(*)::text AS cnt FROM entidades_gestionadas
     WHERE active = true AND tipo = 'CONTRIBUYENTE'`,
  );
  return Number(cnt) || 0;
}

const createSchema = z.object({
  rfc: z.string().regex(/^[A-ZÑ&]{3,4}\d{6}[A-Z0-9]{3}$/i, 'RFC inválido'),
  razonSocial: z.string().min(2, 'Razón social requerida'),
  regimenFiscal: z.string().length(3).optional(),
  codigoPostal: z.string().regex(/^\d{5}$/).optional(),
  domicilio: z.record(z.unknown()).optional(),
  supervisorUserId: z.string().uuid().optional(),
});

const updateSchema = createSchema.partial();

export async function list(req: Request, res: Response, next: NextFunction) {
  try {
    const visibleIds = await getEntidadesVisibles(req.tenantPool!, req.user!.userId, req.user!.role);
    const rows = await contribuyenteService.listContribuyentes(req.tenantPool!, visibleIds);
    return res.json({ data: rows });
  } catch (err) { return next(err); }
}

export async function getById(req: Request, res: Response, next: NextFunction) {
  try {
    const row = await contribuyenteService.getContribuyenteById(req.tenantPool!, String(req.params.id));
    if (!row) return next(new AppError(404, 'Contribuyente no encontrado'));
    return res.json(row);
  } catch (err) { return next(err); }
}

export async function create(req: Request, res: Response, next: NextFunction) {
  try {
    const data = createSchema.parse(req.body);

    // Trial gate: durante el periodo de prueba (trialEndsAt > now) el despacho
    // no puede gestionar más de TRIAL_MAX_CONTRIBUYENTES RFCs activos. Cuando
    // el trial expira, deja de aplicar y el límite del plan vigente toma el control.
    const tenant = await prisma.tenant.findUnique({
      where: { id: req.user!.tenantId },
      select: { trialEndsAt: true },
    });
    const isTrialActive = tenant?.trialEndsAt ? tenant.trialEndsAt > new Date() : false;
    if (isTrialActive) {
      const activeCount = await countActiveContribuyentes(req.tenantPool!);
      if (activeCount >= TRIAL_MAX_CONTRIBUYENTES) {
        return next(new AppError(
          403,
          `Durante el periodo de prueba puedes gestionar hasta ${TRIAL_MAX_CONTRIBUYENTES} contribuyentes. Contrata un plan para agregar más.`,
        ));
      }
    }

    const row = await contribuyenteService.createContribuyente(req.tenantPool!, data);

    // Ajuste de overage despacho: si el tenant pasa de 100 a 101+ RFCs, crea
    // el addon y devuelve paymentUrl para que el frontend redirija al usuario.
    // Fail-soft: si falla el addon, el contribuyente queda creado y se loguea.
    let overage: Awaited<ReturnType<typeof adjustDespachoOverage>> | null = null;
    try {
      const activeCount = await countActiveContribuyentes(req.tenantPool!);
      overage = await adjustDespachoOverage(req.user!.tenantId, activeCount);
    } catch (err: any) {
      console.error('[Contribuyente] Overage adjust failed (non-blocking):', err.message || err);
    }

    return res.status(201).json({ ...row, overage });
  } catch (err: any) {
    if (err instanceof z.ZodError) return next(new AppError(400, err.errors[0].message));
    if (err.code === '23505') return next(new AppError(409, 'Ya existe un contribuyente con este RFC'));
    return next(err);
  }
}

export async function update(req: Request, res: Response, next: NextFunction) {
  try {
    const data = updateSchema.parse(req.body);
    const row = await contribuyenteService.updateContribuyente(req.tenantPool!, String(req.params.id), data);
    if (!row) return next(new AppError(404, 'Contribuyente no encontrado'));
    return res.json(row);
  } catch (err: any) {
    if (err instanceof z.ZodError) return next(new AppError(400, err.errors[0].message));
    return next(err);
  }
}

export async function deactivate(req: Request, res: Response, next: NextFunction) {
  try {
    const ok = await contribuyenteService.deactivateContribuyente(req.tenantPool!, String(req.params.id));
    if (!ok) return next(new AppError(404, 'Contribuyente no encontrado'));

    // Ajuste de overage despacho: si el count baja, reduce quantity del
    // addon (updatePreapprovalAmount) o cancela el preapproval si pasa al límite.
    let overage: Awaited<ReturnType<typeof adjustDespachoOverage>> | null = null;
    try {
      const activeCount = await countActiveContribuyentes(req.tenantPool!);
      overage = await adjustDespachoOverage(req.user!.tenantId, activeCount);
    } catch (err: any) {
      console.error('[Contribuyente] Overage adjust failed (non-blocking):', err.message || err);
    }

    return res.json({ message: 'Contribuyente desactivado', overage });
  } catch (err) { return next(err); }
}

export async function backfill(req: Request, res: Response, next: NextFunction) {
  try {
    const total = await contribuyenteService.backfillAllContribuyentes(req.tenantPool!);
    return res.json({ message: `${total} CFDIs asignados a contribuyentes`, total });
  } catch (err) { return next(err); }
}

export async function addClienteAcceso(req: Request, res: Response, next: NextFunction) {
  try {
    const { userId } = req.body;
    if (!userId || typeof userId !== 'string') return next(new AppError(400, 'userId requerido'));
    const entidadId = String(req.params.id);
    await req.tenantPool!.query(
      'INSERT INTO cliente_accesos (user_id, entidad_id) VALUES ($1, $2) ON CONFLICT DO NOTHING',
      [userId, entidadId],
    );
    return res.json({ message: 'Acceso otorgado' });
  } catch (err) { return next(err); }
}
