import {
  BadRequestException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import type {
  LocationCreate,
  LocationDto,
  LocationsResponse,
  LocationUpdate,
} from '@siop/shared';
import { PrismaService } from '../prisma/prisma.service';

type LocationRow = {
  id: string;
  name: string;
  parentId: string | null;
  address: string | null;
  city: string | null;
  guardianName: string | null;
  guardianPhone: string | null;
  latitude: number | null;
  longitude: number | null;
  partnerId: string | null;
  partner: { name: string } | null;
  _count: { assets: number };
};

const locationInclude = {
  partner: { select: { name: true } },
  _count: { select: { assets: true } },
} as const;

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

  /** Liste plate ; l'assetCount d'un SITE inclut les appareils de ses zones. */
  async list(): Promise<LocationsResponse> {
    const rows: LocationRow[] = await this.prisma.location.findMany({
      include: locationInclude,
      orderBy: { name: 'asc' },
    });
    const childAssets = new Map<string, number>();
    for (const row of rows) {
      if (row.parentId) {
        childAssets.set(
          row.parentId,
          (childAssets.get(row.parentId) ?? 0) + row._count.assets,
        );
      }
    }
    return {
      locations: rows.map((row) =>
        this.toDto(row, row._count.assets + (childAssets.get(row.id) ?? 0)),
      ),
    };
  }

  async create(dto: LocationCreate): Promise<LocationDto> {
    await this.assertDepth(dto.parentId);
    await this.assertClientPartner(dto.partnerId);
    const created = await this.prisma.location.create({
      data: dto,
      include: locationInclude,
    });
    return this.toDto(created, 0);
  }

  async update(id: string, dto: LocationUpdate): Promise<LocationDto> {
    const existing = await this.prisma.location.findUnique({
      where: { id },
      include: { _count: { select: { children: true, assets: true } } },
    });
    if (!existing) throw new NotFoundException('Emplacement inconnu');
    if (dto.parentId) {
      if (dto.parentId === id) {
        throw new BadRequestException('Un emplacement ne peut pas être son propre parent');
      }
      if (existing._count.children > 0) {
        throw new BadRequestException(
          'Ce site a des zones : il ne peut pas devenir une zone (hiérarchie limitée à 2 niveaux)',
        );
      }
      await this.assertDepth(dto.parentId);
    }
    await this.assertClientPartner(dto.partnerId);
    const updated = await this.prisma.location.update({
      where: { id },
      data: dto,
      include: locationInclude,
    });
    return this.toDto(updated, updated._count.assets);
  }

  /** Invariant R1 : site → zone, jamais plus profond. */
  private async assertDepth(parentId?: string): Promise<void> {
    if (!parentId) return;
    const parent = await this.prisma.location.findUnique({ where: { id: parentId } });
    if (!parent) throw new BadRequestException('Emplacement parent inconnu');
    if (parent.parentId) {
      throw new BadRequestException(
        'Hiérarchie limitée à 2 niveaux : une zone ne peut pas contenir d’emplacement',
      );
    }
  }

  /** R3 : seul un tiers Client/syndic peut gérer un site. */
  private async assertClientPartner(partnerId?: string | null): Promise<void> {
    if (!partnerId) return;
    const partner = await this.prisma.partner.findUnique({ where: { id: partnerId } });
    if (!partner) throw new BadRequestException('Tiers inconnu');
    if (partner.kind !== 'CLIENT') {
      throw new BadRequestException('Seul un tiers « Client / syndic » peut être rattaché à un site');
    }
  }

  private toDto(row: Omit<LocationRow, '_count'>, assetCount: number): LocationDto {
    return {
      id: row.id,
      name: row.name,
      parentId: row.parentId,
      address: row.address,
      city: row.city,
      guardianName: row.guardianName,
      guardianPhone: row.guardianPhone,
      latitude: row.latitude,
      longitude: row.longitude,
      partnerId: row.partnerId,
      partnerName: row.partner?.name ?? null,
      assetCount,
    };
  }
}
