import type { Request, Response, NextFunction } from 'express';
import { z } from 'zod';
import { AppError } from '../middlewares/error.middleware.js';
import * as papeleriaService from '../services/papeleria.service.js';
import { emailService } from '../services/email/email.service.js';
import { getTenantOwnerEmails, getUserEmailById } from '../utils/memberships.js';
import { getEntidadesVisibles } from '../utils/entidades-visibles.js';
import { env } from '../config/env.js';
import { prisma } from '../config/database.js';

function effectiveTenantId(req: Request): string {
  return req.viewingTenantId || req.user!.tenantId;
}

const uploadSchema = z.object({
  contribuyenteId: z.string().uuid(),
  nombre: z.string().min(1).max(255),
  descripcion: z.string().max(2000).nullable().optional(),
  anio: z.number().int().min(2000).max(2100),
  mes: z.number().int().min(1).max(12),
  requiereAprobacion: z.boolean(),
  requiereAprobacionCliente: z.boolean(),
  archivoBase64: z.string().min(1),
  archivoFilename: z.string().min(1).max(255),
  archivoMime: z.string().min(1).max(100),
});

export async function upload(req: Request, res: Response, next: NextFunction) {
  try {
    if (req.user?.role === 'cliente') {
      throw new AppError(403, 'Los clientes no pueden subir documentos de papelería');
    }
    const data = uploadSchema.parse(req.body);
    const archivo = Buffer.from(data.archivoBase64, 'base64');

    const item = await papeleriaService.uploadPapeleria(req.tenantPool!, {
      contribuyenteId: data.contribuyenteId,
      nombre: data.nombre,
      descripcion: data.descripcion ?? null,
      anio: data.anio,
      mes: data.mes,
      requiereAprobacion: data.requiereAprobacion,
      requiereAprobacionCliente: data.requiereAprobacionCliente,
      archivo,
      archivoFilename: data.archivoFilename,
      archivoMime: data.archivoMime,
      subidoPor: req.user!.userId,
    });

    if (item.requiereAprobacion) {
      notifyAprobacionRequerida(req, item).catch(err =>
        console.error('[papeleria.upload] notify aprobadores failed:', err?.message || err),
      );
    }
    if (item.requiereAprobacionCliente) {
      notifyClienteAprobacionRequerida(req, item).catch(err =>
        console.error('[papeleria.upload] notify clientes failed:', err?.message || err),
      );
    }

    res.status(201).json(item);
  } catch (error: any) {
    if (error instanceof z.ZodError) return next(new AppError(400, error.errors[0].message));
    if (error?.message?.startsWith('Formato no permitido') || error?.message?.startsWith('Archivo excede')) {
      return next(new AppError(400, error.message));
    }
    next(error);
  }
}

const listSchema = z.object({
  contribuyenteId: z.string().uuid(),
  anio: z.string().regex(/^\d{4}$/).optional(),
  mes: z.string().regex(/^\d{1,2}$/).optional(),
  estado: z.enum(['pendiente', 'aprobado', 'rechazado', 'sin_aprobacion']).optional(),
});

export async function list(req: Request, res: Response, next: NextFunction) {
  try {
    const q = listSchema.parse(req.query);
    const entidadIds = await getEntidadesVisibles(
      req.tenantPool!, req.user!.userId, req.user!.role,
    );
    if (!entidadIds.includes(q.contribuyenteId)) {
      return res.json([]);
    }
    const items = await papeleriaService.listPapeleria(req.tenantPool!, {
      contribuyenteId: q.contribuyenteId,
      anio: q.anio ? parseInt(q.anio, 10) : undefined,
      mes: q.mes ? parseInt(q.mes, 10) : undefined,
      estado: q.estado,
      entidadIds,
      userRole: req.user!.role,
    });
    res.json(items);
  } catch (error) {
    if (error instanceof z.ZodError) return next(new AppError(400, error.errors[0].message));
    next(error);
  }
}

export async function download(req: Request, res: Response, next: NextFunction) {
  try {
    const id = parseInt(String(req.params.id), 10);
    if (isNaN(id)) return next(new AppError(400, 'ID inválido'));

    const item = await papeleriaService.getById(req.tenantPool!, id);
    if (!item) return next(new AppError(404, 'Documento no encontrado'));

    const entidadIds = await getEntidadesVisibles(
      req.tenantPool!, req.user!.userId, req.user!.role,
    );
    if (!entidadIds.includes(item.contribuyenteId)) {
      return next(new AppError(403, 'No tienes acceso a este documento'));
    }
    if (req.user!.role === 'cliente' && !item.requiereAprobacionCliente) {
      return next(new AppError(403, 'No tienes acceso a este documento'));
    }

    const file = await papeleriaService.downloadArchivo(req.tenantPool!, id);
    if (!file) return next(new AppError(404, 'Documento no encontrado'));
    res.setHeader('Content-Type', file.mime);
    res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(file.filename)}"`);
    res.send(file.archivo);
  } catch (error) {
    next(error);
  }
}

export async function aprobar(req: Request, res: Response, next: NextFunction) {
  try {
    if (req.user?.role === 'cliente') {
      throw new AppError(403, 'Los clientes no pueden usar este endpoint');
    }
    const id = parseInt(String(req.params.id), 10);
    if (isNaN(id)) return next(new AppError(400, 'ID inválido'));
    const item = await papeleriaService.aprobar(
      req.tenantPool!, id, req.user!.userId, req.user!.role,
    );
    if (!item) return next(new AppError(404, 'Documento no encontrado o no requiere aprobación'));
    notifyDecisionAuxiliar(req, item).catch(err =>
      console.error('[papeleria.aprobar] notify auxiliar failed:', err?.message || err),
    );
    res.json(item);
  } catch (error: any) {
    if (error?.message?.startsWith('Solo owner')) return next(new AppError(403, error.message));
    next(error);
  }
}

const rechazarSchema = z.object({ comentario: z.string().max(2000).nullable().optional() });

export async function rechazar(req: Request, res: Response, next: NextFunction) {
  try {
    if (req.user?.role === 'cliente') {
      throw new AppError(403, 'Los clientes no pueden usar este endpoint');
    }
    const id = parseInt(String(req.params.id), 10);
    if (isNaN(id)) return next(new AppError(400, 'ID inválido'));
    const { comentario } = rechazarSchema.parse(req.body);
    const item = await papeleriaService.rechazar(
      req.tenantPool!, id, req.user!.userId, req.user!.role, comentario ?? null,
    );
    if (!item) return next(new AppError(404, 'Documento no encontrado o no requiere aprobación'));
    notifyDecisionAuxiliar(req, item).catch(err =>
      console.error('[papeleria.rechazar] notify auxiliar failed:', err?.message || err),
    );
    res.json(item);
  } catch (error: any) {
    if (error instanceof z.ZodError) return next(new AppError(400, error.errors[0].message));
    if (error?.message?.startsWith('Solo owner')) return next(new AppError(403, error.message));
    next(error);
  }
}

export async function aprobarCliente(req: Request, res: Response, next: NextFunction) {
  try {
    if (req.user?.role !== 'cliente') {
      throw new AppError(403, 'Solo clientes pueden usar este endpoint');
    }
    const id = parseInt(String(req.params.id), 10);
    if (isNaN(id)) return next(new AppError(400, 'ID inválido'));

    const entidadIds = await getEntidadesVisibles(
      req.tenantPool!, req.user!.userId, req.user!.role,
    );
    const itemCheck = await papeleriaService.getById(req.tenantPool!, id);
    if (!itemCheck || !entidadIds.includes(itemCheck.contribuyenteId) || !itemCheck.requiereAprobacionCliente) {
      return next(new AppError(404, 'Documento no encontrado o no requiere tu aprobación'));
    }

    const item = await papeleriaService.aprobarCliente(req.tenantPool!, id, req.user!.userId);
    if (!item) return next(new AppError(404, 'Documento no encontrado o no requiere tu aprobación'));
    res.json(item);
  } catch (error) {
    next(error);
  }
}

export async function rechazarCliente(req: Request, res: Response, next: NextFunction) {
  try {
    if (req.user?.role !== 'cliente') {
      throw new AppError(403, 'Solo clientes pueden usar este endpoint');
    }
    const id = parseInt(String(req.params.id), 10);
    if (isNaN(id)) return next(new AppError(400, 'ID inválido'));
    const { comentario } = rechazarSchema.parse(req.body);

    const entidadIds = await getEntidadesVisibles(
      req.tenantPool!, req.user!.userId, req.user!.role,
    );
    const itemCheck = await papeleriaService.getById(req.tenantPool!, id);
    if (!itemCheck || !entidadIds.includes(itemCheck.contribuyenteId) || !itemCheck.requiereAprobacionCliente) {
      return next(new AppError(404, 'Documento no encontrado o no requiere tu aprobación'));
    }

    const item = await papeleriaService.rechazarCliente(
      req.tenantPool!, id, req.user!.userId, comentario ?? null,
    );
    if (!item) return next(new AppError(404, 'Documento no encontrado o no requiere tu aprobación'));
    res.json(item);
  } catch (error: any) {
    if (error instanceof z.ZodError) return next(new AppError(400, error.errors[0].message));
    next(error);
  }
}

export async function eliminar(req: Request, res: Response, next: NextFunction) {
  try {
    if (req.user?.role === 'cliente') {
      throw new AppError(403, 'Los clientes no pueden eliminar documentos');
    }
    const id = parseInt(String(req.params.id), 10);
    if (isNaN(id)) return next(new AppError(400, 'ID inválido'));
    const ok = await papeleriaService.eliminar(req.tenantPool!, id);
    if (!ok) return next(new AppError(404, 'Documento no encontrado'));
    res.status(204).send();
  } catch (error) {
    next(error);
  }
}

// ─── Notificaciones ───

async function getContribuyenteInfo(req: Request, contribuyenteId: string) {
  const { rows } = await req.tenantPool!.query<{ rfc: string; nombre: string }>(
    `SELECT c.rfc, eg.nombre FROM contribuyentes c
       JOIN entidades_gestionadas eg ON eg.id = c.entidad_id
      WHERE c.entidad_id = $1`,
    [contribuyenteId],
  );
  return rows[0] ?? null;
}

/**
 * Notifica a owners y supervisores cuando una papelería requiere aprobación.
 */
async function notifyAprobacionRequerida(
  req: Request,
  item: papeleriaService.PapeleriaItem,
): Promise<void> {
  const tenantId = effectiveTenantId(req);
  const recipients = new Set<string>(await getTenantOwnerEmails(tenantId));

  const supervisores = await prisma.tenantMembership.findMany({
    where: { tenantId, active: true, rol: { nombre: { in: ['supervisor', 'cfo'] } } },
    include: { user: { select: { email: true, active: true } } },
  });
  for (const m of supervisores) {
    if (m.user.active && m.user.email) recipients.add(m.user.email);
  }

  recipients.delete(req.user!.email);
  if (recipients.size === 0) return;

  const tenant = await prisma.tenant.findUnique({
    where: { id: tenantId },
    select: { nombre: true },
  });
  const info = await getContribuyenteInfo(req, item.contribuyenteId);
  if (!info) return;

  const link = `${env.FRONTEND_URL}/documentos`;
  const meses = ['Ene','Feb','Mar','Abr','May','Jun','Jul','Ago','Sep','Oct','Nov','Dic'];
  const periodo = `${meses[item.mes - 1]} ${item.anio}`;

  for (const to of recipients) {
    try {
      await emailService.sendPapeleriaAprobacionRequerida(to, {
        contribuyenteRfc: info.rfc,
        contribuyenteNombre: info.nombre,
        despachoNombre: tenant?.nombre,
        nombreDocumento: item.nombre,
        descripcion: item.descripcion,
        periodo,
        subidoPor: req.user!.email,
        link,
      });
    } catch (err: any) {
      console.error(`[Email] papeleria-aprobacion a ${to}:`, err?.message || err);
    }
  }
}

/**
 * Notifica al uploader cuando un documento fue aprobado o rechazado por owner/supervisor.
 */
async function notifyDecisionAuxiliar(
  req: Request,
  item: papeleriaService.PapeleriaItem,
): Promise<void> {
  if (item.subidoPor === req.user!.userId) return;
  const auxiliarEmail = await getUserEmailById(item.subidoPor);
  if (!auxiliarEmail) return;

  const info = await getContribuyenteInfo(req, item.contribuyenteId);
  if (!info) return;

  const link = `${env.FRONTEND_URL}/documentos`;
  const meses = ['Ene','Feb','Mar','Abr','May','Jun','Jul','Ago','Sep','Oct','Nov','Dic'];
  const periodo = `${meses[item.mes - 1]} ${item.anio}`;

  await emailService.sendPapeleriaDecision(auxiliarEmail, {
    contribuyenteRfc: info.rfc,
    contribuyenteNombre: info.nombre,
    nombreDocumento: item.nombre,
    estado: item.estado as 'aprobado' | 'rechazado',
    revisor: req.user!.email,
    comentario: item.comentarioRechazo,
    periodo,
    link,
  });
}

/**
 * Notifica a los usuarios cliente asociados al contribuyente cuando un documento
 * requiere su aprobación.
 */
async function notifyClienteAprobacionRequerida(
  req: Request,
  item: papeleriaService.PapeleriaItem,
): Promise<void> {
  const tenantId = effectiveTenantId(req);

  // Obtener user_ids de clientes con acceso a este contribuyente
  const { rows } = await req.tenantPool!.query<{ user_id: string }>(
    `SELECT user_id FROM cliente_accesos WHERE entidad_id = $1`,
    [item.contribuyenteId],
  );
  if (rows.length === 0) return;

  const userIds = rows.map(r => r.user_id);
  const users = await prisma.user.findMany({
    where: { id: { in: userIds }, active: true },
    select: { email: true },
  });
  const recipients = users.map(u => u.email).filter(Boolean) as string[];
  if (recipients.length === 0) return;

  const tenant = await prisma.tenant.findUnique({
    where: { id: tenantId },
    select: { nombre: true },
  });
  const info = await getContribuyenteInfo(req, item.contribuyenteId);
  if (!info) return;

  const link = `${env.FRONTEND_URL}/documentos`;
  const meses = ['Ene','Feb','Mar','Abr','May','Jun','Jul','Ago','Sep','Oct','Nov','Dic'];
  const periodo = `${meses[item.mes - 1]} ${item.anio}`;

  for (const to of recipients) {
    try {
      await emailService.sendPapeleriaAprobacionClienteRequerida(to, {
        contribuyenteRfc: info.rfc,
        contribuyenteNombre: info.nombre,
        despachoNombre: tenant?.nombre,
        nombreDocumento: item.nombre,
        descripcion: item.descripcion,
        periodo,
        subidoPor: req.user!.email,
        link,
      });
    } catch (err: any) {
      console.error(`[Email] papeleria-aprobacion-cliente a ${to}:`, err?.message || err);
    }
  }
}
