import { Category as PrismaCategory, Prisma } from '@prisma/client';
import {
  BadRequestException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreateCategoryDto } from './dto/create-category.dto';
import { UpdateCategoryDto } from './dto/update-category.dto';
import { CategoryNodeDto } from './dto/category-node.dto';

@Injectable()
export class CategoriesService {
  constructor(private readonly prisma: PrismaService) {}

  async findAll(): Promise<CategoryNodeDto[]> {
    const all = await this.prisma.category.findMany({
      orderBy: [{ id: 'asc' }],
    });
    return this.buildTree(all);
  }

  async findFlat() {
    return this.prisma.category.findMany({
      orderBy: [{ id: 'asc' }],
    });
  }

  async findOne(id: bigint) {
    const c = await this.prisma.category.findUnique({ where: { id } });
    if (!c) throw new NotFoundException(`Category ${id} not found`);
    return c;
  }

  async create(dto: CreateCategoryDto) {
    if (dto.parentCategoryId !== undefined && dto.parentCategoryId !== null) {
      // Validate the parent exists to produce a clean 404 instead of FK error.
      await this.findOne(BigInt(dto.parentCategoryId));
    }
    return this.prisma.category.create({
      data: {
        categoryName: dto.categoryName,
        categoryIcon: dto.categoryIcon ?? null,
        parentCategoryId:
          dto.parentCategoryId === undefined || dto.parentCategoryId === null
            ? null
            : BigInt(dto.parentCategoryId),
      },
    });
  }

  async update(id: bigint, dto: UpdateCategoryDto) {
    await this.findOne(id);
    if (dto.parentCategoryId !== undefined && dto.parentCategoryId !== null) {
      // Prevent self-parenting & cycles.
      if (BigInt(dto.parentCategoryId) === id) {
        throw new BadRequestException('A category cannot be its own parent');
      }
      await this.findOne(BigInt(dto.parentCategoryId));
    }
    const data: Prisma.CategoryUpdateInput = {};
    if (dto.categoryName !== undefined) data.categoryName = dto.categoryName;
    if (dto.categoryIcon !== undefined) data.categoryIcon = dto.categoryIcon;
    if (dto.parentCategoryId !== undefined) {
      data.parent = dto.parentCategoryId === null
        ? { disconnect: true }
        : { connect: { id: BigInt(dto.parentCategoryId) } };
    }
    return this.prisma.category.update({ where: { id }, data });
  }

  async remove(id: bigint) {
    await this.findOne(id);
    const childCount = await this.prisma.category.count({
      where: { parentCategoryId: id },
    });
    if (childCount > 0) {
      throw new BadRequestException(
        'Category has children and cannot be deleted',
      );
    }
    try {
      return await this.prisma.category.delete({ where: { id } });
    } catch (err) {
      if (this.isForeignKeyViolation(err)) {
        throw new BadRequestException(
          'Category is referenced by goods or positions and cannot be deleted',
        );
      }
      throw err;
    }
  }

  private isForeignKeyViolation(err: unknown): boolean {
    if (err instanceof Prisma.PrismaClientKnownRequestError) {
      return err.code === 'P2003';
    }
    if (err instanceof Prisma.PrismaClientUnknownRequestError) {
      const msg = err.message ?? '';
      return (
        msg.includes('foreign key constraint') ||
        msg.includes('RESTRICT') ||
        msg.includes('violates')
      );
    }
    return false;
  }

  /**
   * Builds a tree in-memory from a flat list. Top-level nodes have
   * `parentCategoryId = null`.
   */
  private buildTree(
    rows: PrismaCategory[],
  ): CategoryNodeDto[] {
    const byId = new Map<bigint, CategoryNodeDto>();
    for (const row of rows) {
      byId.set(row.id, CategoryNodeDto.from(row, []));
    }
    const roots: CategoryNodeDto[] = [];
    for (const row of rows) {
      const node = byId.get(row.id)!;
      if (row.parentCategoryId === null) {
        roots.push(node);
      } else {
        const parent = byId.get(row.parentCategoryId);
        if (parent) {
          parent.children.push(node);
        } else {
          // Orphan (parent row missing) — surface as a root.
          roots.push(node);
        }
      }
    }
    return roots;
  }
}
