import {
  BadRequestException,
  ConflictException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { Prisma } from '@prisma/client';
import type {
  PurchaseOrderCreate,
  PurchaseOrderDto,
  PurchaseOrdersResponse,
  PurchaseOrderStatus,
  PurchaseOrderTransition,
} from '@siop/shared';
import type { AuthenticatedUser } from '../auth/current-user.decorator';
import { PrismaService } from '../prisma/prisma.service';

const poInclude = {
  supplier: true,
  lines: { include: { part: true } },
} satisfies Prisma.PurchaseOrderInclude;

type Row = Prisma.PurchaseOrderGetPayload<{ include: typeof poInclude }>;

/** Machine à états du BC : la réception est le seul chemin vers le stock. */
const TRANSITIONS: Record<PurchaseOrderStatus, PurchaseOrderStatus[]> = {
  DRAFT: ['SENT', 'CANCELLED'],
  SENT: ['RECEIVED', 'CANCELLED'],
  RECEIVED: [],
  CANCELLED: [],
};

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

  async list(): Promise<PurchaseOrdersResponse> {
    const rows = await this.prisma.purchaseOrder.findMany({
      include: poInclude,
      orderBy: { createdAt: 'desc' },
    });
    return { purchaseOrders: rows.map((r) => this.toDto(r)) };
  }

  async get(id: string): Promise<PurchaseOrderDto> {
    const row = await this.prisma.purchaseOrder.findUnique({
      where: { id },
      include: poInclude,
    });
    if (!row) throw new NotFoundException('BC inconnu');
    return this.toDto(row);
  }

  async create(dto: PurchaseOrderCreate, user: AuthenticatedUser): Promise<PurchaseOrderDto> {
    const supplier = await this.prisma.partner.findUnique({
      where: { id: dto.supplierId },
    });
    if (!supplier || supplier.kind !== 'SUPPLIER' || !supplier.isActive) {
      throw new BadRequestException('Fournisseur inconnu ou inactif');
    }
    const parts = await this.prisma.part.findMany({
      where: { id: { in: dto.lines.map((l) => l.partId) } },
    });
    if (parts.length !== new Set(dto.lines.map((l) => l.partId)).size) {
      throw new BadRequestException('Pièce inconnue dans les lignes');
    }
    for (let essai = 0; ; essai++) {
      try {
        const created = await this.prisma.purchaseOrder.create({
          data: {
            reference: await this.nextReference(),
            supplierId: dto.supplierId,
            createdById: user.userId,
            lines: { create: dto.lines },
          },
          select: { id: true },
        });
        return this.get(created.id);
      } catch (e) {
        if (
          e instanceof Prisma.PrismaClientKnownRequestError &&
          e.code === 'P2002' &&
          essai < 3
        ) {
          continue;
        }
        throw e;
      }
    }
  }

  async transition(
    id: string,
    dto: PurchaseOrderTransition,
    user: AuthenticatedUser,
  ): Promise<PurchaseOrderDto> {
    const row = await this.prisma.purchaseOrder.findUnique({
      where: { id },
      include: poInclude,
    });
    if (!row) throw new NotFoundException('BC inconnu');
    if (!TRANSITIONS[row.status].includes(dto.to)) {
      throw new ConflictException(`Transition interdite : ${row.status} → ${dto.to}`);
    }
    if (dto.to === 'RECEIVED') {
      // LA règle : la réception crée les entrées de stock et FIGE les PU
      await this.prisma.$transaction(async (tx) => {
        for (const line of row.lines) {
          await tx.stockMovement.create({
            data: {
              partId: line.partId,
              kind: 'RECEIPT',
              quantity: line.quantity,
              unitPrice: line.unitPrice,
              purchaseOrderId: row.id,
              byId: user.userId,
            },
          });
          await tx.part.update({
            where: { id: line.partId },
            data: { lastUnitPrice: line.unitPrice },
          });
        }
        await tx.purchaseOrder.update({
          where: { id },
          data: { status: 'RECEIVED', receivedAt: new Date() },
        });
      });
    } else {
      await this.prisma.purchaseOrder.update({
        where: { id },
        data: {
          status: dto.to,
          sentAt: dto.to === 'SENT' ? new Date() : undefined,
          cancelledAt: dto.to === 'CANCELLED' ? new Date() : undefined,
        },
      });
    }
    return this.get(id);
  }

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

  private toDto(row: Row): PurchaseOrderDto {
    const lines = row.lines.map((l) => ({
      id: l.id,
      partId: l.partId,
      partReference: l.part.reference,
      designation: l.part.designation,
      quantity: l.quantity,
      unitPrice: Number(l.unitPrice),
    }));
    return {
      id: row.id,
      reference: row.reference,
      status: row.status,
      supplierId: row.supplierId,
      supplierName: row.supplier.name,
      lines,
      total: lines.reduce((s, l) => s + l.quantity * l.unitPrice, 0),
      sentAt: row.sentAt?.toISOString() ?? null,
      receivedAt: row.receivedAt?.toISOString() ?? null,
      createdAt: row.createdAt.toISOString(),
    };
  }
}
