import { ConflictException, Injectable, Logger, NotFoundException } from '@nestjs/common';
import { AuditService } from 'src/audit/audit.service';
import { PrismaService } from 'src/prisma/prisma.service';
import { resolverRubroDefault, resolverRubrosPermitidos } from 'src/common/utils/rubros.util';
import { tieneAccesoElevado } from 'src/utils/acceso-elevado';
import { CreateRubroDto } from './dto/create-rubro.dto';
import { UpdateRubroDto } from './dto/update-rubro.dto';

/**
 * Rubros o unidades de negocio de la empresa. Ver docs/plan-rubros-multi-sucursal.md.
 *
 * El rubro es el eje por el que se separan proveedores y catálogo cuando una
 * empresa opera sucursales de negocios distintos. Nunca se borra un rubro: hay
 * FKs colgando (sucursales, proveedores, categorías), así que la baja es lógica.
 */
@Injectable()
export class RubrosService {
  private readonly logger = new Logger(RubrosService.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly auditService: AuditService,
  ) {}

  /** El código es la clave humana del rubro: se normaliza para que "desp" y "DESP" choquen. */
  private normalizarCodigo(codigo: string) {
    return String(codigo ?? '').trim().toUpperCase();
  }

  private async obtenerDeEmpresa(id: string, empresa_id: string) {
    const rubro = await this.prisma.rubros.findFirst({ where: { id, empresa_id } });
    if (!rubro) throw new NotFoundException('Rubro no encontrado');
    return rubro;
  }

  async findAll(empresa_id: string, soloActivos = false) {
    return this.prisma.rubros.findMany({
      where: soloActivos ? { empresa_id, activo: true } : { empresa_id },
      // Viaja la descripción de la sucursal, no sólo el id: el ABM no puede
      // resolverla contra el listado de sucursales del usuario, que viene
      // recortado a las que tiene asignadas.
      include: {
        sucursal_rubros: {
          select: {
            sucursal_id: true,
            empresas_sucursales: { select: { id: true, descripcion: true } },
          },
        },
      },
      orderBy: [{ orden: 'asc' }, { descripcion: 'asc' }],
    });
  }

  async create(dto: CreateRubroDto, empresa_id: string, user_id?: string) {
    const codigo = this.normalizarCodigo(dto.codigo);

    const existente = await this.prisma.rubros.findFirst({ where: { empresa_id, codigo } });
    if (existente) throw new ConflictException(`Ya existe un rubro con el código ${codigo}`);

    const rubro = await this.prisma.rubros.create({
      data: {
        empresa_id,
        codigo,
        descripcion: dto.descripcion,
        color: dto.color ?? null,
        icono: dto.icono ?? null,
        orden: dto.orden ?? 0,
        activo: true,
      },
    });

    this.logger.log(`Rubro creado: ${rubro.descripcion} (${rubro.id})`, 'RubrosService');
    await this.auditService.log({
      empresa_id,
      user_id,
      action: 'CREATE',
      entity_type: 'rubro',
      entity_id: rubro.id,
      descripcion: `Rubro creado: ${rubro.descripcion}`,
      new_value: { codigo: rubro.codigo, descripcion: rubro.descripcion },
    });
    return { message: 'Rubro creado exitosamente', data: rubro };
  }

  async update(id: string, dto: UpdateRubroDto, empresa_id: string, user_id?: string) {
    const actual = await this.obtenerDeEmpresa(id, empresa_id);
    const codigo = dto.codigo === undefined ? undefined : this.normalizarCodigo(dto.codigo);

    if (codigo && codigo !== actual.codigo) {
      const existente = await this.prisma.rubros.findFirst({ where: { empresa_id, codigo } });
      if (existente) throw new ConflictException(`Ya existe un rubro con el código ${codigo}`);
    }

    const rubro = await this.prisma.rubros.update({
      where: { id },
      data: {
        ...(codigo !== undefined ? { codigo } : {}),
        ...(dto.descripcion !== undefined ? { descripcion: dto.descripcion } : {}),
        ...(dto.color !== undefined ? { color: dto.color } : {}),
        ...(dto.icono !== undefined ? { icono: dto.icono } : {}),
        ...(dto.orden !== undefined ? { orden: dto.orden } : {}),
        ...(dto.activo !== undefined ? { activo: dto.activo } : {}),
        updated_at: new Date(),
      },
    });

    await this.auditService.log({
      empresa_id,
      user_id,
      action: 'UPDATE',
      entity_type: 'rubro',
      entity_id: id,
      descripcion: `Rubro actualizado: ${actual.descripcion}`,
      old_value: { codigo: actual.codigo, descripcion: actual.descripcion, activo: actual.activo },
      new_value: { codigo: rubro.codigo, descripcion: rubro.descripcion, activo: rubro.activo },
    });
    return { message: 'Rubro actualizado exitosamente', data: rubro };
  }

  /**
   * Baja lógica. Un rubro dado de baja deja de ofrecerse en los selectores, pero
   * las asignaciones existentes siguen ahí: si se borrara, los proveedores
   * etiquetados quedarían sueltos y volverían a aparecer en todos lados.
   */
  async remove(id: string, empresa_id: string, user_id?: string) {
    const actual = await this.obtenerDeEmpresa(id, empresa_id);

    await this.prisma.rubros.update({ where: { id }, data: { activo: false } });

    this.logger.log(`Rubro dado de baja: ${actual.descripcion} (${id})`, 'RubrosService');
    await this.auditService.log({
      empresa_id,
      user_id,
      action: 'DELETE',
      entity_type: 'rubro',
      entity_id: id,
      descripcion: `Rubro dado de baja: ${actual.descripcion}`,
      old_value: { codigo: actual.codigo, descripcion: actual.descripcion },
    });
    return { message: 'Rubro dado de baja exitosamente' };
  }

  /** Reemplaza el set completo de sucursales del rubro. Lista vacía = ninguna. */
  async setSucursales(id: string, sucursal_ids: string[], empresa_id: string, user_id?: string) {
    const rubro = await this.obtenerDeEmpresa(id, empresa_id);
    const ids = [...new Set(sucursal_ids ?? [])];

    if (ids.length) {
      const propias = await this.prisma.empresas_sucursales.findMany({
        where: { id: { in: ids }, empresa_id },
        select: { id: true },
      });
      if (propias.length !== ids.length) {
        throw new NotFoundException('Alguna de las sucursales no pertenece a la empresa');
      }
    }

    await this.prisma.$transaction(async (tx) => {
      await tx.sucursal_rubros.deleteMany({ where: { rubro_id: id } });
      if (ids.length) {
        await tx.sucursal_rubros.createMany({
          data: ids.map((sucursal_id) => ({ rubro_id: id, sucursal_id })),
        });
      }
    });

    await this.auditService.log({
      empresa_id,
      user_id,
      action: 'UPDATE',
      entity_type: 'rubro',
      entity_id: id,
      descripcion: `Sucursales del rubro ${rubro.descripcion}: ${ids.length}`,
      new_value: { sucursal_ids: ids },
    });
    return { message: 'Sucursales del rubro actualizadas', data: { rubro_id: id, sucursal_ids: ids } };
  }

  /**
   * Contexto de rubro/sucursal con el que arranca el usuario. Lo consume
   * `/auth/me`: el frontend lo usa para el default del selector de rubro.
   *
   * La sucursal sale de `asignacion_sucursal_caja_usuario`, que es el único
   * vínculo usuario↔sucursal que existe. Hasta ahora el frontend leía un
   * `user.sucursal_id` que nunca existió en el modelo, así que todos los
   * usuarios caían por fallback en la primera sucursal de la empresa.
   *
   * Un usuario tiene una asignación **por caja**, así que la misma sucursal
   * aparece repetida: se deduplica acá.
   */
  async contextoDeUsuario(usuario_id: string, empresa_id: string) {
    const [asignaciones, rubrosEmpresa, elevado] = await Promise.all([
      this.prisma.asignacion_sucursal_caja_usuario.findMany({
        where: { usuario_id, sucursal: { empresa_id, active: { not: false } } },
        select: {
          sucursal: {
            select: {
              id: true,
              descripcion: true,
              sucursal_rubros: { select: { rubro_id: true } },
            },
          },
        },
      }),
      this.prisma.rubros.findMany({
        where: { empresa_id, activo: true },
        select: { id: true, codigo: true, descripcion: true, color: true, icono: true },
        orderBy: [{ orden: 'asc' }, { descripcion: 'asc' }],
      }),
      tieneAccesoElevado(this.prisma, usuario_id, empresa_id),
    ]);

    const porId = new Map<string, { id: string; descripcion: string; sucursal_rubros: { rubro_id: string }[] }>();
    for (const a of asignaciones) {
      if (a.sucursal && !porId.has(a.sucursal.id)) porId.set(a.sucursal.id, a.sucursal as any);
    }
    const sucursales = [...porId.values()];

    // El selector sólo ofrece lo que le corresponde al usuario. Un usuario del
    // taller no ve Regalería ni "Todos los rubros".
    const permitidos = resolverRubrosPermitidos(sucursales, elevado);
    const rubros_disponibles = permitidos === null
      ? rubrosEmpresa
      : rubrosEmpresa.filter((r) => permitidos.includes(r.id));

    return {
      sucursales_asignadas: sucursales.map((s) => ({ id: s.id, descripcion: s.descripcion })),
      sucursal_default_id: sucursales[0]?.id ?? null,
      rubros_disponibles,
      // true = puede elegir "Todos los rubros" (sin restricción por rubro).
      rubros_todos: permitidos === null,
      rubro_default_id: rubros_disponibles.length === 1 && permitidos !== null
        ? rubros_disponibles[0].id
        : resolverRubroDefault(sucursales),
    };
  }
}
