import {
  BadRequestException,
  ConflictException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { Prisma } from '@prisma/client';
import type {
  PartCreate,
  PartDetail,
  PartDto,
  PartsResponse,
  PartUpdate,
  StockMovementCreate,
  StockMovementDto,
} from '@siop/shared';
import type { AuthenticatedUser } from '../auth/current-user.decorator';
import { PrismaService } from '../prisma/prisma.service';

const partInclude = { supplier: true } satisfies Prisma.PartInclude;
type PartRow = Prisma.PartGetPayload<{ include: typeof partInclude }>;

const mvtInclude = {
  workOrder: { select: { reference: true } },
  purchaseOrder: { select: { reference: true } },
  by: { select: { displayName: true } },
} satisfies Prisma.StockMovementInclude;
type MvtRow = Prisma.StockMovementGetPayload<{ include: typeof mvtInclude }>;

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

  /** Le stock EST la somme des mouvements — calculé, jamais stocké. */
  private async stocks(partIds?: string[]): Promise<Map<string, number>> {
    const grouped = await this.prisma.stockMovement.groupBy({
      by: ['partId'],
      where: partIds ? { partId: { in: partIds } } : undefined,
      _sum: { quantity: true },
    });
    return new Map(grouped.map((g) => [g.partId, g._sum.quantity ?? 0]));
  }

  async list(): Promise<PartsResponse> {
    const rows = await this.prisma.part.findMany({
      include: partInclude,
      orderBy: { reference: 'asc' },
    });
    const stocks = await this.stocks();
    return { parts: rows.map((r) => this.toDto(r, stocks.get(r.id) ?? 0)) };
  }

  async get(id: string): Promise<PartDetail> {
    const row = await this.prisma.part.findUnique({ where: { id }, include: partInclude });
    if (!row) throw new NotFoundException('Pièce inconnue');
    const [stocks, movements] = await Promise.all([
      this.stocks([id]),
      this.prisma.stockMovement.findMany({
        where: { partId: id },
        include: mvtInclude,
        orderBy: { createdAt: 'desc' },
        take: 50,
      }),
    ]);
    return {
      ...this.toDto(row, stocks.get(id) ?? 0),
      movements: movements.map((m) => this.toMovementDto(m)),
    };
  }

  async create(dto: PartCreate): Promise<PartDetail> {
    if (dto.supplierId) await this.assertSupplier(dto.supplierId);
    for (let essai = 0; ; essai++) {
      try {
        const created = await this.prisma.part.create({
          data: {
            reference: await this.nextReference(),
            designation: dto.designation,
            threshold: dto.threshold ?? 0,
            supplierId: dto.supplierId,
            compatible: dto.compatible,
            lastUnitPrice: dto.initialPrice,
          },
          select: { id: true },
        });
        return this.get(created.id);
      } catch (e) {
        if (
          e instanceof Prisma.PrismaClientKnownRequestError &&
          e.code === 'P2002' &&
          essai < 3
        ) {
          continue;
        }
        throw e;
      }
    }
  }

  async update(id: string, dto: PartUpdate): Promise<PartDetail> {
    if (dto.supplierId) await this.assertSupplier(dto.supplierId);
    try {
      await this.prisma.part.update({ where: { id }, data: dto, select: { id: true } });
    } catch (e) {
      if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2025') {
        throw new NotFoundException('Pièce inconnue');
      }
      throw e;
    }
    return this.get(id);
  }

  /** Entrée manuelle ou ajustement — TOUJOURS un mouvement tracé. */
  async addMovement(
    id: string,
    dto: StockMovementCreate,
    user: AuthenticatedUser,
  ): Promise<PartDetail> {
    const part = await this.prisma.part.findUnique({ where: { id } });
    if (!part) throw new NotFoundException('Pièce inconnue');
    if (dto.kind === 'ENTRY' && dto.quantity <= 0) {
      throw new BadRequestException('Une entrée manuelle est positive');
    }
    if (dto.kind === 'ADJUSTMENT' && !dto.reason?.trim()) {
      throw new BadRequestException('Un ajustement d’inventaire exige un motif');
    }
    await this.prisma.$transaction(async (tx) => {
      const somme = await tx.stockMovement.aggregate({
        where: { partId: id },
        _sum: { quantity: true },
      });
      const stock = somme._sum.quantity ?? 0;
      if (stock + dto.quantity < 0) {
        throw new ConflictException(
          `Refusé : le stock deviendrait négatif (${stock} ${dto.quantity > 0 ? '+' : ''}${dto.quantity})`,
        );
      }
      await tx.stockMovement.create({
        data: {
          partId: id,
          kind: dto.kind,
          quantity: dto.quantity,
          reason: dto.reason,
          byId: user.userId,
        },
      });
    });
    return this.get(id);
  }

  /** Consommation d'OT — stock suffisant exigé, PRIX FIGÉ au moment T.
   *  Appelé par WorkOrdersService dans le périmètre d'un OT vérifié. */
  async consume(
    partId: string,
    quantity: number,
    workOrderId: string,
    user: AuthenticatedUser,
  ): Promise<{ designation: string; unitPrice: number }> {
    const part = await this.prisma.part.findUnique({ where: { id: partId } });
    if (!part || !part.isActive) throw new BadRequestException('Pièce inconnue ou désactivée');
    if (part.lastUnitPrice === null) {
      throw new ConflictException(
        'Aucun prix connu pour cette pièce — réceptionnez un BC ou renseignez un prix initial',
      );
    }
    await this.prisma.$transaction(async (tx) => {
      const somme = await tx.stockMovement.aggregate({
        where: { partId },
        _sum: { quantity: true },
      });
      const stock = somme._sum.quantity ?? 0;
      if (stock < quantity) {
        throw new ConflictException(`Stock insuffisant : ${stock} en stock, ${quantity} demandé`);
      }
      await tx.stockMovement.create({
        data: {
          partId,
          kind: 'CONSUMPTION',
          quantity: -quantity,
          unitPrice: part.lastUnitPrice,
          workOrderId,
          byId: user.userId,
        },
      });
    });
    return { designation: part.designation, unitPrice: Number(part.lastUnitPrice) };
  }

  private async assertSupplier(supplierId: string): Promise<void> {
    const supplier = await this.prisma.partner.findUnique({ where: { id: supplierId } });
    if (!supplier || supplier.kind !== 'SUPPLIER') {
      throw new BadRequestException('Fournisseur inconnu');
    }
  }

  private async nextReference(): Promise<string> {
    const [{ nextval }] = await this.prisma.$queryRaw<[{ nextval: bigint }]>`
      SELECT nextval('part_ref_seq')`;
    return `P-${String(nextval).padStart(4, '0')}`;
  }

  private toDto(row: PartRow, stock: number): PartDto {
    return {
      id: row.id,
      reference: row.reference,
      designation: row.designation,
      threshold: row.threshold,
      lastUnitPrice: row.lastUnitPrice === null ? null : Number(row.lastUnitPrice),
      compatible: row.compatible,
      supplierId: row.supplierId,
      supplierName: row.supplier?.name ?? null,
      isActive: row.isActive,
      stock,
      belowThreshold: stock < row.threshold,
    };
  }

  private toMovementDto(m: MvtRow): StockMovementDto {
    return {
      id: m.id,
      kind: m.kind,
      quantity: m.quantity,
      unitPrice: m.unitPrice === null ? null : Number(m.unitPrice),
      reason: m.reason,
      workOrderReference: m.workOrder?.reference ?? null,
      purchaseOrderReference: m.purchaseOrder?.reference ?? null,
      byName: m.by?.displayName ?? null,
      createdAt: m.createdAt.toISOString(),
    };
  }
}
