import { ConflictException, Injectable, NotFoundException, Logger } from '@nestjs/common';
import { AuditService } from 'src/audit/audit.service';
import { PrismaService } from 'src/prisma/prisma.service';
import { CreateCategoriaDto } from './dto/create-categoria.dto';
import { UpdateCategoriaDto } from './dto/update-categoria.dto';
import { whereCategoriaPorRubro } from 'src/common/utils/rubros.util';
import { rubrosFiltroUsuario } from 'src/utils/alcance-rubro';
import { rubroDelRequest } from 'src/utils/rubro-context';

@Injectable()
export class CategoriasService {
  private readonly logger = new Logger(CategoriasService.name);
  constructor(
    private readonly prisma: PrismaService,
    private readonly auditService: AuditService,
  ) {}

  private async assertPadreValido(empresa_id: string, padre_id?: string) {
    if (!padre_id) return;
    const padre = await this.prisma.categorias.findFirst({
      where: {
        id: padre_id,
        empresa_id,
        deleted_at: false,
      },
    });
    if (!padre) throw new NotFoundException('Categoría padre no encontrada');
  }

  async create(createCategoriaDto: CreateCategoriaDto, empresa_id: string, user_id?: string) {
    await this.assertPadreValido(empresa_id, createCategoriaDto.padre_id);

    const existente = await this.prisma.categorias.findFirst({
      where: {
        empresa_id,
        padre_id: createCategoriaDto.padre_id ?? null,
        descripcion: createCategoriaDto.descripcion,
        deleted_at: false,
      },
    });
    if (existente) throw new ConflictException('La categoría ya existe');

    const categoria = await this.prisma.categorias.create({
      data: {
        empresa_id,
        padre_id: createCategoriaDto.padre_id ?? null,
        descripcion: createCategoriaDto.descripcion,
        icono: createCategoriaDto.icono ?? null,
        // El DTO ya aceptaba `color` y el formulario siempre lo mandó, pero no
        // se persistía: toda categoría quedaba con el punto gris.
        color: createCategoriaDto.color ?? null,
        // Rubro de la categoría: los productos lo heredan.
        // null = compartida entre rubros. Ver docs/plan-rubros-multi-sucursal.md.
        rubro_id: createCategoriaDto.rubro_id ?? null,
        activo: true,
        deleted_at: false,
      },
    });

    this.logger.log(`Categoría creada: ${categoria.descripcion} (${categoria.id})`, 'CategoriasService');
    await this.auditService.log({
      empresa_id,
      user_id,
      action: 'CREATE',
      entity_type: 'categoria',
      entity_id: categoria.id,
      descripcion: `Categoría creada: ${categoria.descripcion}`,
      new_value: { descripcion: categoria.descripcion, padre_id: categoria.padre_id, icono: categoria.icono },
    });
    return { message: 'Categoría creada exitosamente', data: categoria };
  }

  /**
   * Las del rubro elegido en el selector (header x-rubro-id) más las
   * compartidas, recortado a los rubros que el usuario puede ver: sin esto los
   * selectores de categoría mostraban las de todos los rubros de la empresa.
   */
  private async wherePorRubro(empresa_id: string, usuario_id?: string) {
    return whereCategoriaPorRubro(await rubrosFiltroUsuario(this.prisma, usuario_id, empresa_id, rubroDelRequest()));
  }

  async findAll(page = 1, limit = 10, empresa_id: string, usuario_id?: string) {
    const take = Math.max(1, Math.min(2000, Number(limit) || 10));
    const currentPage = Math.max(1, Number(page) || 1);
    const skip = (currentPage - 1) * take;
    const where = { empresa_id, deleted_at: false, ...(await this.wherePorRubro(empresa_id, usuario_id)) };
    const [items, total] = await this.prisma.$transaction([
      this.prisma.categorias.findMany({
        where,
        select: {
          id: true,
          descripcion: true,
          padre_id: true,
          icono: true,
          padre: true,
          activo: true,
          deleted_at: true,
          created_at: true,
          updated_at: true,
          usuario: true,
          // Rubro de la categoría, para el chip y el selector del ABM.
          rubro_id: true,
          rubro: { select: { id: true, descripcion: true, color: true, icono: true } },
        },
        skip,
        take,
        orderBy: { created_at: 'desc' },
      }),
      this.prisma.categorias.count({ where }),
    ]);
    return {
      items,
      total,
      page: currentPage,
      limit: take,
      lastPage: Math.max(1, Math.ceil(total / take) || 1),
    };
  }

  async findRoots(empresa_id: string, usuario_id?: string) {
    const items = await this.prisma.categorias.findMany({
      where: {
        empresa_id,
        padre_id: null,
        deleted_at: false,
        ...(await this.wherePorRubro(empresa_id, usuario_id)),
      },
      orderBy: { created_at: 'desc' },
    });
    return { message: 'Categorías padre obtenidas exitosamente', data: items };
  }

  async findTree(empresa_id: string) {
    const items = await this.prisma.categorias.findMany({
      where: { empresa_id, deleted_at: false },
      // El rubro viaja con cada nodo: el árbol es la pantalla donde se ve de un
      // vistazo qué categoría pertenece a qué negocio.
      include: { rubro: { select: { id: true, descripcion: true, color: true, icono: true } } },
      orderBy: { created_at: 'desc' },
    });

    const byId = new Map<string, any>();
    items.forEach((c) => byId.set(c.id, { ...c, children: [] }));

    const roots: any[] = [];
    for (const c of byId.values()) {
      if (c.padre_id && byId.has(c.padre_id)) {
        byId.get(c.padre_id).children.push(c);
      } else {
        roots.push(c);
      }
    }

    return { message: 'Árbol de categorías obtenido exitosamente', data: roots };
  }

  async findChildren(padre_id: string, empresa_id: string) {
    await this.assertPadreValido(empresa_id, padre_id);
    const items = await this.prisma.categorias.findMany({
      where: {
        empresa_id,
        padre_id,
        deleted_at: false,
      },
      orderBy: { created_at: 'desc' },
    });
    return { message: 'Categorías hijas obtenidas exitosamente', data: items };
  }

  async findOne(id: string, empresa_id: string) {
    const categoria = await this.prisma.categorias.findFirst({
      where: {
        id,
        empresa_id,
        deleted_at: false,
      },
      include: {
        padre: true,
        hijos: true,
      },
    });
    if (!categoria) throw new NotFoundException('Categoría no encontrada');
    return { message: 'Categoría encontrada', data: categoria };
  }

  async update(id: string, updateCategoriaDto: UpdateCategoriaDto, empresa_id: string, user_id?: string) {
    const categoriaFind = await this.prisma.categorias.findFirst({
      where: {
        id,
        empresa_id,
        deleted_at: false,
      },
    });
    if (!categoriaFind) throw new NotFoundException('Categoría no encontrada');

    if (updateCategoriaDto.padre_id && updateCategoriaDto.padre_id === id) {
      throw new ConflictException('Una categoría no puede ser su propio padre');
    }
    await this.assertPadreValido(empresa_id, updateCategoriaDto.padre_id);

    const categoria = await this.prisma.categorias.update({
      where: { id },
      data: {
        descripcion: updateCategoriaDto.descripcion,
        padre_id: updateCategoriaDto.padre_id ?? undefined,
        icono: updateCategoriaDto.icono ?? undefined,
        color: updateCategoriaDto.color ?? undefined,
        // `undefined` no toca el rubro; `null` explícito lo limpia (vuelve a ser
        // categoría compartida). Por eso no se usa `??` acá.
        rubro_id: updateCategoriaDto.rubro_id === undefined ? undefined : updateCategoriaDto.rubro_id,
        activo: updateCategoriaDto.activo,
        updated_at: new Date(),
      },
    });
    this.logger.log(`Categoría actualizada: ${categoria.descripcion} (${id})`, 'CategoriasService');
    await this.auditService.log({
      empresa_id,
      user_id,
      action: 'UPDATE',
      entity_type: 'categoria',
      entity_id: id,
      descripcion: `Categoría actualizada: ${categoria.descripcion}`,
      old_value: { descripcion: categoriaFind.descripcion, activo: categoriaFind.activo, icono: categoriaFind.icono },
      new_value: { descripcion: categoria.descripcion, activo: categoria.activo, icono: categoria.icono },
    });
    return { message: 'Categoría actualizada exitosamente', data: categoria };
  }

  async activarDesactivar(id: string, empresa_id: string, user_id?: string) {
    const categoriaFind = await this.prisma.categorias.findFirst({
      where: {
        id,
        empresa_id,
        deleted_at: false,
      },
    });
    if (!categoriaFind) throw new NotFoundException('Categoría no encontrada');
    const categoria = await this.prisma.categorias.update({
      where: { id },
      data: {
        activo: !categoriaFind.activo,
        updated_at: new Date(),
      },
    });
    const action = categoria.activo ? 'ACTIVATE' : 'DEACTIVATE';
    this.logger.log(`Categoría (${id}) ${categoria.activo ? 'activada' : 'desactivada'}`, 'CategoriasService');
    await this.auditService.log({
      empresa_id,
      user_id,
      action,
      entity_type: 'categoria',
      entity_id: id,
      descripcion: `Categoría ${categoria.activo ? 'activada' : 'desactivada'}: ${categoriaFind.descripcion}`,
      old_value: { activo: categoriaFind.activo },
      new_value: { activo: categoria.activo },
    });
    return { message: 'Categoría actualizada exitosamente', data: categoria };
  }

  async remove(id: string, empresa_id: string, user_id?: string) {
    const categoriaFind = await this.prisma.categorias.findFirst({
      where: {
        id,
        empresa_id,
        deleted_at: false,
      },
      include: {
        hijos: {
          where: {
            deleted_at: false,
          },
          select: { id: true },
        },
      },
    });
    if (!categoriaFind) throw new NotFoundException('Categoría no encontrada');
    if (categoriaFind.hijos.length > 0) throw new ConflictException('No se puede eliminar una categoría con hijos');

    const categoria = await this.prisma.categorias.update({
      where: { id },
      data: {
        deleted_at: true,
        updated_at: new Date(),
      },
    });
    this.logger.log(`Categoría eliminada: ${categoriaFind.descripcion} (${id})`, 'CategoriasService');
    await this.auditService.log({
      empresa_id,
      user_id,
      action: 'DELETE',
      entity_type: 'categoria',
      entity_id: id,
      descripcion: `Categoría eliminada: ${categoriaFind.descripcion}`,
      old_value: {
        descripcion: categoriaFind.descripcion,
        activo: categoriaFind.activo,
        padre_id: categoriaFind.padre_id,
      },
    });
    return { message: 'Categoría eliminada exitosamente', data: categoria };
  }
}
