import {
  BadRequestException,
  ConflictException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { Prisma } from '@prisma/client';
import type {
  AssetComponentCreate,
  AssetComponentDto,
  AssetCreate,
  AssetDetail,
  AssetDto,
  AssetOptionsResponse,
  AssetsResponse,
  AssetUpdate,
} from '@siop/shared';
import { PrismaService } from '../prisma/prisma.service';

const assetInclude = {
  category: true,
  location: { include: { parent: true } },
  _count: { select: { components: true } },
} satisfies Prisma.AssetInclude;

type AssetRow = Prisma.AssetGetPayload<{ include: typeof assetInclude }>;

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

  /** Options minimales pour le signalement — ouvert à tout rôle authentifié. */
  async options(): Promise<AssetOptionsResponse> {
    const rows = await this.prisma.asset.findMany({
      include: { location: { include: { parent: true } } },
      orderBy: { reference: 'asc' },
    });
    return {
      options: rows.map((r) => ({
        id: r.id,
        reference: r.reference,
        siteName: r.location.parent?.name ?? r.location.name,
      })),
    };
  }

  async list(): Promise<AssetsResponse> {
    const rows = await this.prisma.asset.findMany({
      include: assetInclude,
      orderBy: { reference: 'asc' },
    });
    return { assets: rows.map((r) => this.toDto(r)) };
  }

  async get(id: string): Promise<AssetDetail> {
    const row = await this.prisma.asset.findUnique({
      where: { id },
      include: { ...assetInclude, components: { include: { type: true } } },
    });
    if (!row) throw new NotFoundException('Appareil inconnu');
    return {
      ...this.toDto(row),
      components: row.components.map((c) => this.toComponentDto(c)),
    };
  }

  async create(dto: AssetCreate): Promise<AssetDetail> {
    await this.assertEquipmentCategory(dto.categoryId);
    await this.assertLocation(dto.locationId);
    if (dto.components?.length) {
      await this.assertComponentTypes(dto.components.map((c) => c.typeId));
    }
    try {
      const created = await this.prisma.asset.create({
        data: {
          reference: dto.reference,
          brand: dto.brand,
          model: dto.model,
          serialNumber: dto.serialNumber,
          commissionedAt: dto.commissionedAt ? new Date(dto.commissionedAt) : undefined,
          loadKg: dto.loadKg,
          floors: dto.floors,
          categoryId: dto.categoryId,
          locationId: dto.locationId,
          components: dto.components?.length
            ? { create: dto.components }
            : undefined,
        },
        select: { id: true },
      });
      return this.get(created.id);
    } catch (e) {
      if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
        throw new ConflictException('Cette référence est déjà utilisée');
      }
      throw e;
    }
  }

  async update(id: string, dto: AssetUpdate): Promise<AssetDetail> {
    if (dto.categoryId) await this.assertEquipmentCategory(dto.categoryId);
    if (dto.locationId) await this.assertLocation(dto.locationId);
    try {
      await this.prisma.asset.update({
        where: { id },
        data: {
          ...dto,
          commissionedAt: dto.commissionedAt ? new Date(dto.commissionedAt) : undefined,
        },
        select: { id: true },
      });
    } catch (e) {
      if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2025') {
        throw new NotFoundException('Appareil inconnu');
      }
      if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
        throw new ConflictException('Cette référence est déjà utilisée');
      }
      throw e;
    }
    return this.get(id);
  }

  async addComponent(
    assetId: string,
    dto: AssetComponentCreate,
  ): Promise<AssetComponentDto> {
    const asset = await this.prisma.asset.findUnique({ where: { id: assetId } });
    if (!asset) throw new NotFoundException('Appareil inconnu');
    await this.assertComponentTypes([dto.typeId]);
    const created = await this.prisma.assetComponent.create({
      data: { assetId, ...dto },
      include: { type: true },
    });
    return this.toComponentDto(created);
  }

  async removeComponent(assetId: string, componentId: string): Promise<void> {
    const { count } = await this.prisma.assetComponent.deleteMany({
      where: { id: componentId, assetId },
    });
    if (count === 0) throw new NotFoundException('Organe inconnu');
  }

  private async assertEquipmentCategory(categoryId: string): Promise<void> {
    const category = await this.prisma.category.findUnique({ where: { id: categoryId } });
    if (!category || category.kind !== 'EQUIPMENT' || !category.isActive) {
      throw new BadRequestException(
        'La catégorie choisie n’est pas une catégorie d’équipement active',
      );
    }
  }

  private async assertLocation(locationId: string): Promise<void> {
    const location = await this.prisma.location.findUnique({ where: { id: locationId } });
    if (!location) throw new BadRequestException('Emplacement inconnu');
  }

  private async assertComponentTypes(typeIds: string[]): Promise<void> {
    const types = await this.prisma.category.findMany({
      where: { id: { in: typeIds } },
    });
    const valid =
      types.length === new Set(typeIds).size &&
      types.every((t) => t.kind === 'COMPONENT_TYPE' && t.isActive);
    if (!valid) {
      throw new BadRequestException('Chaque organe doit avoir un type d’organe actif');
    }
  }

  private toComponentDto(c: { id: string; typeId: string; designation: string | null; type: { name: string } }): AssetComponentDto {
    return {
      id: c.id,
      typeId: c.typeId,
      typeName: c.type.name,
      designation: c.designation,
    };
  }

  private toDto(row: AssetRow): AssetDto {
    return {
      id: row.id,
      reference: row.reference,
      brand: row.brand,
      model: row.model,
      serialNumber: row.serialNumber,
      commissionedAt: row.commissionedAt?.toISOString() ?? null,
      loadKg: row.loadKg,
      floors: row.floors,
      status: row.status,
      categoryId: row.categoryId,
      categoryName: row.category.name,
      locationId: row.locationId,
      locationName: row.location.name,
      siteName: row.location.parent?.name ?? row.location.name,
      componentCount: row._count.components,
    };
  }
}
