import { ForbiddenException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { tieneAccesoElevado } from './acceso-elevado';
import { rubrosPermitidosUsuario } from './alcance-rubro';
import { rubroDelRequest } from './rubro-context';

/**
 * Alcance de sucursales de un usuario, ya resuelto en las tres formas en que
 * los distintos módulos atan un documento a su sucursal.
 *
 * `null` significa SIN RESTRICCIÓN y es el bypass deliberado: lo devuelve un
 * usuario con acceso elevado (superadmin / holding / reseller) y también uno
 * que no tiene ninguna asignación cargada. Sin ese segundo caso, estrenar la
 * restricción dejaría a toda empresa que todavía no asignó sucursales sin ver
 * un solo documento.
 */
export type AlcanceSucursal = {
  /** Para documentos con `sucursal_id`: presupuestos, órdenes, compras, pedidos. */
  sucursalIds: string[];
  /**
   * Para documentos fiscales (facturas, notas de crédito, remisiones), que no
   * tienen `sucursal_id` sino `dest` (punto de establecimiento) y `dpunexp`.
   *
   * Se filtra por `dest` y NO por el par (dest, dpunexp) a propósito: hay 4.019
   * facturas cargadas con un punto de expedición que no existe en
   * `empresas_puntos_expedicion`, y filtrar por el par las volvería invisibles
   * para todo el mundo. `punto_establecimiento` identifica la sucursal 1 a 1
   * (verificado: no hay dos sucursales de una misma empresa que lo compartan),
   * así que alcanza y es más robusto.
   */
  puntosEstablecimiento: string[];
  /** Para inventario: stock, lotes, ajustes, transferencias, inventario físico. */
  depositoIds: string[];
} | null;

/**
 * Resuelve a qué sucursales puede acceder el usuario, recortado además al
 * rubro que eligió en el selector (header `x-rubro-id`, ver rubro-context.ts).
 *
 * Con un rubro elegido, un superadmin —que sin rubro ve todo— ve sólo las
 * sucursales de ese rubro; un usuario restringido ve la intersección. Así el
 * selector deja de ser una comodidad de catálogo y filtra documentos,
 * reportes y dashboards en todos los módulos que pasan por acá.
 */
export async function alcanceSucursalUsuario(
  prisma: PrismaService,
  usuario_id?: string,
  empresa_id?: string,
): Promise<AlcanceSucursal> {
  const base = await alcanceSucursalBase(prisma, usuario_id, empresa_id);
  const rubroId = rubroDelRequest();
  if (!rubroId || !usuario_id || !empresa_id) return base;
  return recortarPorRubro(prisma, usuario_id, empresa_id, rubroId, base);
}

/**
 * Alcance por asignación de sucursales, sin mirar el rubro elegido.
 *
 * Ojo con el orden: primero se pregunta por acceso elevado. Un superadmin al
 * que además se le asignó una sucursal debe seguir viendo todo — es el mismo
 * bug que hubo que corregir en Tesorería.
 */
async function alcanceSucursalBase(
  prisma: PrismaService,
  usuario_id?: string,
  empresa_id?: string,
): Promise<AlcanceSucursal> {
  if (!usuario_id || !empresa_id) return null;

  if (await tieneAccesoElevado(prisma, usuario_id, empresa_id)) return null;

  const asignaciones = await prisma.asignacion_sucursal_caja_usuario.findMany({
    where: { usuario_id, sucursal: { empresa_id, active: { not: false } } },
    select: { sucursal_id: true },
    distinct: ['sucursal_id'],
  });
  if (asignaciones.length === 0) return null; // sin asignaciones = sin restricción

  const sucursalIds = asignaciones.map((a) => a.sucursal_id);

  const [sucursales, depositos] = await Promise.all([
    prisma.empresas_sucursales.findMany({
      where: { id: { in: sucursalIds } },
      select: { punto_establecimiento: true },
    }),
    prisma.depositos.findMany({
      where: { sucursal_id: { in: sucursalIds } },
      select: { id: true },
    }),
  ]);

  return {
    sucursalIds,
    puntosEstablecimiento: sucursales
      .map((s) => s.punto_establecimiento)
      .filter((p): p is string => !!p),
    depositoIds: depositos.map((d) => d.id),
  };
}

const ALCANCE_VACIO: NonNullable<AlcanceSucursal> = { sucursalIds: [], puntosEstablecimiento: [], depositoIds: [] };

/**
 * Recorta el alcance a las sucursales del rubro elegido.
 *
 * Un rubro que no es de esta empresa se ignora: es una elección guardada en el
 * navegador desde otra empresa (holding, o dos usuarios en la misma máquina) y
 * aplicarla dejaría a una empresa sin rubros sin ver un solo documento.
 *
 * Un rubro de la empresa que el usuario no tiene permitido (elección guardada de
 * antes de restringirlo) no tira 403: deja el alcance vacío y las pantallas salen
 * sin datos, igual que `sucursalesAConsultar` con una sucursal ajena.
 */
async function recortarPorRubro(
  prisma: PrismaService,
  usuario_id: string,
  empresa_id: string,
  rubroId: string,
  base: AlcanceSucursal,
): Promise<AlcanceSucursal> {
  const rubro = await prisma.rubros.findFirst({ where: { id: rubroId, empresa_id }, select: { id: true } });
  if (!rubro) return base;

  const permitidos = await rubrosPermitidosUsuario(prisma, usuario_id, empresa_id);
  if (permitidos !== null && !permitidos.includes(rubroId)) return ALCANCE_VACIO;

  const delRubro = await prisma.empresas_sucursales.findMany({
    where: {
      empresa_id,
      active: { not: false },
      sucursal_rubros: { some: { rubro_id: rubroId } },
      ...(base ? { id: { in: base.sucursalIds } } : {}),
    },
    select: { id: true, punto_establecimiento: true },
  });
  if (delRubro.length === 0) return ALCANCE_VACIO;

  const sucursalIds = delRubro.map((s) => s.id);
  const depositos = await prisma.depositos.findMany({
    where: { sucursal_id: { in: sucursalIds } },
    select: { id: true },
  });
  return {
    sucursalIds,
    puntosEstablecimiento: delRubro.map((s) => s.punto_establecimiento).filter((p): p is string => !!p),
    depositoIds: depositos.map((d) => d.id),
  };
}

/**
 * Intersecta el alcance del usuario con la sucursal que pidió por filtro.
 *
 * Devuelve la lista de sucursales a consultar, o `[]` si pidió una que no
 * tiene asignada. `[]` hace que el listado salga vacío en vez de tirar un 403:
 * un filtro guardado o un enlace viejo apuntando a otra sucursal no debería
 * romper la pantalla, y el usuario igual no ve nada que no le corresponda.
 */
export function sucursalesAConsultar(
  alcance: AlcanceSucursal,
  sucursalIdPedida?: string,
): string[] | null {
  if (!alcance) return sucursalIdPedida ? [sucursalIdPedida] : null;
  if (!sucursalIdPedida) return alcance.sucursalIds;
  return alcance.sucursalIds.includes(sucursalIdPedida) ? [sucursalIdPedida] : [];
}

/**
 * Fragmento de `where` para documentos fiscales, listo para meter en un AND.
 * Devuelve `null` cuando no hay nada que restringir.
 *
 * Se arma con AND y no con OR porque `buildFacturasWhere` ya usa `OR` para la
 * búsqueda por texto: sumar el alcance ahí lo convertiría en "de mi sucursal
 * O que coincida con el texto", que es justo lo contrario.
 */
export async function filtroFiscalPorSucursal(
  prisma: PrismaService,
  alcance: AlcanceSucursal,
  sucursalIdPedida?: string,
): Promise<{ dest: { in: string[] } } | null> {
  const ids = sucursalesAConsultar(alcance, sucursalIdPedida);
  if (ids === null) return null; // sin restricción y sin filtro pedido

  if (ids.length === 0) return { dest: { in: [] } }; // pidió una que no tiene

  const sucursales = await prisma.empresas_sucursales.findMany({
    where: { id: { in: ids } },
    select: { punto_establecimiento: true },
  });
  const puntos = sucursales
    .map((s) => s.punto_establecimiento)
    .filter((p): p is string => !!p);

  // Sucursal sin punto de establecimiento cargado: no hay forma de atarle sus
  // documentos fiscales, así que no se puede resolver a nada.
  return { dest: { in: puntos } };
}

/**
 * Valor a asignar a `where.sucursal_id` en los documentos que sí tienen esa
 * columna: presupuestos, órdenes de compra, compras y pedidos.
 *
 *   undefined     → sin restricción y sin filtro pedido: no se toca el where
 *   string        → una sucursal concreta (la pedida, ya validada)
 *   { in: [...] } → las sucursales del usuario
 *   { in: [] }    → pidió una que no tiene asignada: el listado sale vacío
 */
export function filtroSucursalOperativo(
  alcance: AlcanceSucursal,
  sucursalIdPedida?: string,
): string | { in: string[] } | undefined {
  const ids = sucursalesAConsultar(alcance, sucursalIdPedida);
  if (ids === null) return undefined;
  if (ids.length === 1) return ids[0];
  return { in: ids };
}

/**
 * Igual que `filtroSucursalOperativo` pero para inventario, donde el documento
 * cuelga del depósito y hay que filtrar por `deposito.sucursal_id`.
 */
export function filtroSucursalDeposito(
  alcance: AlcanceSucursal,
  sucursalIdPedida?: string,
): string | { in: string[] } | undefined {
  return filtroSucursalOperativo(alcance, sucursalIdPedida);
}

/**
 * Corta el acceso si el usuario no tiene acceso elevado.
 *
 * Para los libros fiscales (Libro IVA de ventas y de compras, liquidación de
 * IVA). Ahí NO se puede aplicar el alcance por sucursal: un usuario asignado a
 * una sucursal vería un libro recortado, y si declara con eso declara de menos.
 * O ve el libro completo, o no lo ve.
 */
export async function exigirAccesoElevado(
  prisma: PrismaService,
  usuario_id: string | undefined,
  empresa_id: string | undefined,
  reporte: string,
): Promise<void> {
  // Lo que hay que impedir es entregar un libro RECORTADO, no negárselo a todo el que
  // no sea superadmin. Si el alcance del usuario ya cubre la empresa entera, el libro
  // sale completo y no hay nada que proteger.
  //
  // Sin esto quedaba bloqueado para cualquier empresa que no fuera holding o reseller:
  // el criterio anterior solo miraba `is_superadmin` y el tipo de empresa, así que ni
  // el admin de una empresa de una sola sucursal podía sacar su propio Libro IVA.
  if (await alcanceCubreEmpresa(prisma, usuario_id, empresa_id)) return;

  throw new ForbiddenException(
    `${reporte} abarca toda la empresa y no se puede recortar por sucursal sin dar un libro incompleto. ` +
      'Pedíselo a un usuario con acceso a todas las sucursales.',
  );
}

/**
 * ¿El usuario alcanza a ver la empresa completa?
 *
 * Tres formas de que sí: acceso elevado (superadmin/holding/reseller), no tener
 * asignaciones -- que en `alcanceSucursalUsuario` ya significa "sin restricción" -- o
 * tener asignadas todas las sucursales activas de la empresa. El caso más común es el
 * tercero con una sola sucursal.
 */
async function alcanceCubreEmpresa(
  prisma: PrismaService,
  usuario_id: string | undefined,
  empresa_id: string | undefined,
): Promise<boolean> {
  // Sin el rubro del selector: el libro es de la empresa, no de un rubro, y un
  // superadmin con "Taller" elegido tiene que seguir pudiendo sacarlo completo.
  const alcance = await alcanceSucursalBase(prisma, usuario_id, empresa_id);
  if (alcance === null) return true; // elevado o sin restricción
  if (!empresa_id) return false;

  const sucursalesEmpresa = await prisma.empresas_sucursales.findMany({
    where: { empresa_id, active: { not: false } },
    select: { id: true },
  });
  // Sin sucursales activas no hay nada que recortar; con alguna, el usuario tiene que
  // alcanzarlas a todas.
  if (sucursalesEmpresa.length === 0) return true;

  const alcanzadas = new Set(alcance.sucursalIds);
  return sucursalesEmpresa.every((s) => alcanzadas.has(s.id));
}
