import {
  BadRequestException,
  ConflictException,
  ForbiddenException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import {
  AssignmentStatus,
  OrderSource,
  OrderStatus,
  Prisma,
  ProductionStage,
  StageTaskStatus,
  WorkerCompMode,
  WorkerLedgerEntryType,
} from '@prisma/client';
import type {
  AssignmentCreateInput,
  AssignmentUpdateInput,
  CompletionCreateInput,
  OrderStatusUpdateInput,
  StageUpdateInput,
} from '@ali-ismail/contracts';
import { PrismaService } from '../prisma/prisma.service';
import { AuditService } from '../audit/audit.service';
import { BusinessEventsService } from '../notifications/events.service';
import { WorkerHistoryService } from '../workers/worker-history.service';
import { DeliveryPostingService } from '../accounting/delivery-posting.service';
import { WorkerPostingService } from '../accounting/worker-posting.service';

/** Stage -> seeded worker-type code. */
const STAGE_WORKER_TYPE: Record<ProductionStage, string> = {
  CUTTING: 'CUTTING',
  STITCHING: 'STITCHING',
  FINISHING: 'FINISHING',
  DESIGN_WORK: 'DESIGN',
  QUALITY_CONTROL: 'QC',
};

/** Only these stages create piece-rate earnings (doc 21 PRD-4). */
const PAID_STAGES: ProductionStage[] = [
  ProductionStage.CUTTING,
  ProductionStage.STITCHING,
  ProductionStage.FINISHING,
  ProductionStage.DESIGN_WORK,
];

const dec = (v: string | number | Prisma.Decimal) => new Prisma.Decimal(v as never);

@Injectable()
export class ProductionService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly audit: AuditService,
    private readonly businessEvents: BusinessEventsService,
    private readonly history: WorkerHistoryService,
    private readonly deliveryPosting: DeliveryPostingService,
    private readonly workerPosting: WorkerPostingService,
  ) {}

  private assignmentInclude = {
    worker: { select: { id: true, name: true, workerNumber: true, isActive: true, compensationMode: true } },
    garmentLine: { select: { id: true, itemName: true, qty: true } },
    designLine: { select: { id: true, itemName: true, qty: true } },
    completions: { orderBy: { createdAt: 'asc' as const } },
  };

  // -------------------------------------------------- assignment create

  async createAssignment(
    organizationId: string,
    dto: AssignmentCreateInput,
    actor: { id: string; permissions: string[]; isSuperAdmin: boolean },
  ) {
    const order = await this.prisma.order.findFirst({
      where: { id: dto.orderId, organizationId },
      include: { garmentLines: true, designLines: true, customer: { select: { name: true } } },
    });
    if (!order) throw new NotFoundException('Order not found');
    if (order.status !== OrderStatus.CONFIRMED && order.status !== OrderStatus.IN_PRODUCTION) {
      throw new ConflictException(`Cannot assign production work to an order in status ${order.status}`);
    }

    const stage = dto.stage as ProductionStage;
    const isPaid = PAID_STAGES.includes(stage);

    // ----- target line validation -----
    let garmentLine = null;
    let designLine = null;
    if (stage === ProductionStage.DESIGN_WORK) {
      if (!dto.designLineId) throw new BadRequestException('DESIGN_WORK assignments require designLineId');
      if (dto.garmentLineId) throw new BadRequestException('DESIGN_WORK assignments must not target a garment line');
      designLine = order.designLines.find((l) => l.id === dto.designLineId);
      if (!designLine) throw new BadRequestException('designLineId does not belong to this order');
    } else if (stage === ProductionStage.QUALITY_CONTROL) {
      if (dto.garmentLineId || dto.designLineId) {
        throw new BadRequestException('QUALITY_CONTROL assignments target the production task, not a line');
      }
    } else {
      if (!dto.garmentLineId) throw new BadRequestException(`${stage} assignments require garmentLineId`);
      if (dto.designLineId) throw new BadRequestException(`${stage} assignments must not target a design line`);
      garmentLine = order.garmentLines.find((l) => l.id === dto.garmentLineId);
      if (!garmentLine) throw new BadRequestException('garmentLineId does not belong to this order');
    }

    // ----- worker eligibility -----
    const worker = await this.prisma.worker.findFirst({
      where: { id: dto.workerId, organizationId },
      include: { typeLinks: { include: { workerType: true } } },
    });
    if (!worker) throw new NotFoundException('Worker not found');
    if (!worker.isActive || worker.archivedAt) throw new BadRequestException('Worker is not active');
    const requiredTypeCode = STAGE_WORKER_TYPE[stage];
    const typeLink = worker.typeLinks.find((l) => l.workerType.code === requiredTypeCode && l.workerType.isActive);
    if (!typeLink) {
      throw new BadRequestException(`Worker does not support the ${stage} stage (requires worker type ${requiredTypeCode})`);
    }

    const qtyAssigned = dec(dto.qtyAssigned as never);
    if (qtyAssigned.lessThanOrEqualTo(0)) throw new BadRequestException('qtyAssigned must be positive');

    // ----- rate snapshot (paid stages only) -----
    let rateAmount: Prisma.Decimal | null = null;
    let rateSource: 'WORKER_TYPE_RATE' | 'MANUAL_OVERRIDE' | 'NONE' = 'NONE';
    let rateItemId: string | null = null;
    let rateOverrideReason: string | null = null;
    if (isPaid) {
      const lineRateItemId = (garmentLine ?? designLine)?.rateItemId ?? null;
      if (dto.rateOverride) {
        const canOverride = actor.isSuperAdmin || actor.permissions.includes('workers.adjust');
        if (!canOverride) {
          throw new ForbiddenException('Manual rate override requires the workers.adjust permission');
        }
        rateAmount = dec(dto.rateOverride.amount as never);
        rateSource = 'MANUAL_OVERRIDE';
        rateOverrideReason = dto.rateOverride.reason;
        rateItemId = lineRateItemId;
      } else {
        if (!lineRateItemId) {
          throw new ConflictException(
            'No rate item linked to the target line; configure the rate or use an authorized manual override',
          );
        }
        const wr = await this.prisma.rateItemWorkerRate.findFirst({
          where: { organizationId, rateItemId: lineRateItemId, workerTypeId: typeLink.workerTypeId },
        });
        if (!wr) {
          throw new ConflictException(
            `No worker rate configured for this rate item and worker type (${requiredTypeCode}); configure it in the Rate List or use an authorized manual override`,
          );
        }
        rateAmount = wr.amount;
        rateSource = 'WORKER_TYPE_RATE';
        rateItemId = lineRateItemId;
      }
    }

    const created = await this.prisma.$transaction(async (tx) => {
      // Lock the order row first (consistent lock ordering: order -> line) and
      // re-validate its status inside the transaction so a concurrent READY /
      // cancel transition cannot race a stale pre-check.
      await tx.$queryRaw`SELECT id FROM orders WHERE id = ${order.id} FOR UPDATE`;
      const fresh = await tx.order.findFirstOrThrow({
        where: { id: order.id, organizationId },
        select: { status: true },
      });
      if (fresh.status !== OrderStatus.CONFIRMED && fresh.status !== OrderStatus.IN_PRODUCTION) {
        throw new ConflictException(`Cannot assign production work to an order in status ${fresh.status}`);
      }

      // Serialize capacity checks per line+stage: lock the target line row.
      if (garmentLine) {
        await tx.$queryRaw`SELECT id FROM order_garment_lines WHERE id = ${garmentLine.id} FOR UPDATE`;
      } else if (designLine) {
        await tx.$queryRaw`SELECT id FROM order_design_lines WHERE id = ${designLine.id} FOR UPDATE`;
      }

      // Per line + per stage capacity (stage capacities are independent).
      if (garmentLine || designLine) {
        const lineQty = (garmentLine ?? designLine)!.qty;
        const agg = await tx.workAssignment.aggregate({
          where: {
            organizationId,
            stage,
            status: { not: AssignmentStatus.CANCELLED },
            ...(garmentLine ? { garmentLineId: garmentLine.id } : { designLineId: designLine!.id }),
          },
          _sum: { qtyAssigned: true },
        });
        const already = agg._sum.qtyAssigned ?? dec(0);
        if (already.add(qtyAssigned).greaterThan(lineQty)) {
          throw new ConflictException(
            `Over-assignment: ${stage} already has ${already} of ${lineQty} assigned for this line; cannot add ${qtyAssigned}`,
          );
        }
      }

      const assignment = await tx.workAssignment.create({
        data: {
          organizationId,
          orderId: order.id,
          stage,
          garmentLineId: garmentLine?.id ?? null,
          designLineId: designLine?.id ?? null,
          workerId: worker.id,
          workerTypeId: typeLink.workerTypeId,
          qtyAssigned,
          rateItemId,
          rateAmount,
          rateSource,
          rateOverrideReason,
          status: AssignmentStatus.ASSIGNED,
          assignedDate: dto.assignedDate ? new Date(dto.assignedDate) : new Date(),
          dueDate: dto.dueDate ? new Date(dto.dueDate) : null,
          notes: dto.notes ?? null,
          createdById: actor.id,
        },
        include: this.assignmentInclude,
      });

      // First assignment moves a CONFIRMED order into production (doc 12:36).
      if (fresh.status === OrderStatus.CONFIRMED) {
        await tx.order.update({ where: { id: order.id }, data: { status: OrderStatus.IN_PRODUCTION } });
      }
      await this.upsertStageState(tx, organizationId, order.id, stage, StageTaskStatus.ASSIGNED, actor.id);
      return assignment;
    });

    await this.audit.write({
      organizationId,
      actorUserId: actor.id,
      action: 'production.assignment_create',
      entityType: 'work_assignment',
      entityId: created.id,
      after: {
        orderId: order.id,
        stage,
        workerId: worker.id,
        qtyAssigned: String(qtyAssigned),
        rateAmount: rateAmount ? String(rateAmount) : null,
        rateSource,
      },
      reason: rateOverrideReason,
    });
    return created;
  }

  // -------------------------------------------------- assignment update / cancel / start / rework

  async updateAssignment(organizationId: string, id: string, dto: AssignmentUpdateInput, actorUserId: string) {
    const updated = await this.prisma.$transaction(async (tx) => {
      await tx.$queryRaw`SELECT id FROM work_assignments WHERE id = ${id} FOR UPDATE`;
      const a = await tx.workAssignment.findFirst({ where: { id, organizationId } });
      if (!a) throw new NotFoundException('Assignment not found');
      if (a.status === AssignmentStatus.CANCELLED || a.status === AssignmentStatus.COMPLETED) {
        throw new ConflictException(`Assignment cannot be edited in status ${a.status}`);
      }
      let qtyAssigned = a.qtyAssigned;
      if (dto.qtyAssigned != null) {
        qtyAssigned = dec(dto.qtyAssigned as never);
        if (qtyAssigned.lessThan(a.qtyCompleted)) {
          throw new ConflictException(`qtyAssigned cannot be below already-completed quantity ${a.qtyCompleted}`);
        }
        // capacity re-check against sibling assignments
        if (a.garmentLineId || a.designLineId) {
          if (a.garmentLineId) {
            await tx.$queryRaw`SELECT id FROM order_garment_lines WHERE id = ${a.garmentLineId} FOR UPDATE`;
          } else {
            await tx.$queryRaw`SELECT id FROM order_design_lines WHERE id = ${a.designLineId} FOR UPDATE`;
          }
          const line = a.garmentLineId
            ? await tx.orderGarmentLine.findUnique({ where: { id: a.garmentLineId } })
            : await tx.orderDesignLine.findUnique({ where: { id: a.designLineId! } });
          if (line) {
            const agg = await tx.workAssignment.aggregate({
              where: {
                organizationId,
                stage: a.stage,
                status: { not: AssignmentStatus.CANCELLED },
                id: { not: a.id },
                ...(a.garmentLineId ? { garmentLineId: a.garmentLineId } : { designLineId: a.designLineId }),
              },
              _sum: { qtyAssigned: true },
            });
            const others = agg._sum.qtyAssigned ?? dec(0);
            if (others.add(qtyAssigned).greaterThan(line.qty)) {
              throw new ConflictException(
                `Over-assignment: other ${a.stage} assignments total ${others} of ${line.qty}; cannot set this one to ${qtyAssigned}`,
              );
            }
          }
        }
      }
      return tx.workAssignment.update({
        where: { id: a.id },
        data: {
          qtyAssigned,
          ...(dto.dueDate !== undefined ? { dueDate: dto.dueDate ? new Date(dto.dueDate) : null } : {}),
          ...(dto.notes !== undefined ? { notes: dto.notes ?? null } : {}),
        },
        include: this.assignmentInclude,
      });
    });
    await this.audit.write({
      organizationId,
      actorUserId,
      action: 'production.assignment_update',
      entityType: 'work_assignment',
      entityId: id,
      after: { qtyAssigned: String(updated.qtyAssigned), dueDate: updated.dueDate, notes: updated.notes },
      reason: dto.reason ?? null,
    });
    return updated;
  }

  async cancelAssignment(organizationId: string, id: string, reason: string, actorUserId: string) {
    const updated = await this.prisma.$transaction(async (tx) => {
      await tx.$queryRaw`SELECT id FROM work_assignments WHERE id = ${id} FOR UPDATE`;
      const a = await tx.workAssignment.findFirst({ where: { id, organizationId } });
      if (!a) throw new NotFoundException('Assignment not found');
      if (a.status === AssignmentStatus.CANCELLED) return a;
      if (!a.qtyCompleted.equals(0)) {
        throw new ConflictException(
          'Assignment has posted completions/earnings; it cannot be cancelled. Use ledger reversal for corrections.',
        );
      }
      return tx.workAssignment.update({
        where: { id: a.id },
        data: { status: AssignmentStatus.CANCELLED, cancelledAt: new Date(), cancelReason: reason },
        include: this.assignmentInclude,
      });
    });
    await this.audit.write({
      organizationId,
      actorUserId,
      action: 'production.assignment_cancel',
      entityType: 'work_assignment',
      entityId: id,
      reason,
    });
    return updated;
  }

  async startAssignment(organizationId: string, id: string, actorUserId: string) {
    const a = await this.prisma.workAssignment.findFirst({ where: { id, organizationId } });
    if (!a) throw new NotFoundException('Assignment not found');
    if (a.status !== AssignmentStatus.ASSIGNED && a.status !== AssignmentStatus.REWORK_REQUIRED) {
      throw new ConflictException(`Cannot start work from status ${a.status}`);
    }
    // Guarded update: a concurrent completion cannot be overwritten because
    // the status predicate is re-checked atomically.
    const applied = await this.prisma.workAssignment.updateMany({
      where: {
        id: a.id,
        organizationId,
        status: { in: [AssignmentStatus.ASSIGNED, AssignmentStatus.REWORK_REQUIRED] },
      },
      data: { status: AssignmentStatus.IN_PROGRESS },
    });
    if (applied.count === 0) throw new ConflictException('Assignment status changed concurrently; refresh and retry');
    const updated = await this.prisma.workAssignment.findFirstOrThrow({
      where: { id: a.id, organizationId },
      include: this.assignmentInclude,
    });
    await this.prisma.$transaction((tx) =>
      this.upsertStageState(tx, organizationId, a.orderId, a.stage, StageTaskStatus.IN_PROGRESS, actorUserId),
    );
    await this.audit.write({
      organizationId,
      actorUserId,
      action: 'production.assignment_start',
      entityType: 'work_assignment',
      entityId: id,
      before: { status: a.status },
      after: { status: updated.status },
    });
    return updated;
  }

  async markRework(organizationId: string, id: string, reason: string, actorUserId: string) {
    const a = await this.prisma.workAssignment.findFirst({ where: { id, organizationId } });
    if (!a) throw new NotFoundException('Assignment not found');
    if (a.status === AssignmentStatus.CANCELLED) throw new ConflictException('Cancelled assignments cannot be reworked');
    // History (completions, earnings) is preserved; only status + reason change.
    // Rework compensation is NOT defined in the approved payroll docs, so no
    // automatic extra earnings are created for rework.
    const applied = await this.prisma.workAssignment.updateMany({
      where: { id: a.id, organizationId, status: { not: AssignmentStatus.CANCELLED } },
      data: { status: AssignmentStatus.REWORK_REQUIRED, reworkReason: reason },
    });
    if (applied.count === 0) throw new ConflictException('Assignment status changed concurrently; refresh and retry');
    const updated = await this.prisma.workAssignment.findFirstOrThrow({
      where: { id: a.id, organizationId },
      include: this.assignmentInclude,
    });
    await this.prisma.$transaction((tx) =>
      this.upsertStageState(tx, organizationId, a.orderId, a.stage, StageTaskStatus.REWORK_REQUIRED, actorUserId, reason),
    );
    await this.audit.write({
      organizationId,
      actorUserId,
      action: 'production.assignment_rework',
      entityType: 'work_assignment',
      entityId: id,
      before: { status: a.status },
      after: { status: AssignmentStatus.REWORK_REQUIRED },
      reason,
    });
    return updated;
  }

  // -------------------------------------------------- completion (partial, append-only, idempotent)

  async complete(organizationId: string, assignmentId: string, dto: CompletionCreateInput, actorUserId: string) {
    // Idempotency pre-check (fast path).
    if (dto.clientRequestId) {
      const existing = await this.prisma.workCompletion.findFirst({
        where: { organizationId, idempotencyKey: dto.clientRequestId },
        include: { earning: true, assignment: { include: this.assignmentInclude } },
      });
      if (existing) return { completion: existing, assignment: existing.assignment, duplicate: true };
    }

    try {
      const result = await this.prisma.$transaction(async (tx) => {
        // Lock assignment: remaining-quantity math must be serialized.
        await tx.$queryRaw`SELECT id FROM work_assignments WHERE id = ${assignmentId} FOR UPDATE`;
        const a = await tx.workAssignment.findFirst({
          where: { id: assignmentId, organizationId },
          include: { worker: true },
        });
        if (!a) throw new NotFoundException('Assignment not found');
        if (a.status === AssignmentStatus.CANCELLED) throw new ConflictException('Assignment is cancelled');
        if (a.status === AssignmentStatus.COMPLETED) {
          throw new ConflictException('Assignment is already fully completed');
        }

        const qty = dec(dto.qty as never);
        if (qty.lessThanOrEqualTo(0)) throw new BadRequestException('qty must be positive');
        const remaining = a.qtyAssigned.sub(a.qtyCompleted);
        if (qty.greaterThan(remaining)) {
          throw new ConflictException(`Over-completion: only ${remaining} of ${a.qtyAssigned} remaining`);
        }

        const completedAt = dto.completedAt ? new Date(dto.completedAt) : new Date();
        const isPaid = PAID_STAGES.includes(a.stage);
        // Phase 5.1 FINAL: piece earnings post only when the worker's
        // EFFECTIVE mode on the completion date is PER_PIECE (mode/status
        // history-resolved; mode changes never rewrite old earnings).
        // SALARY_PLUS_PIECE is retired: never piece-compensated.
        const effectiveMode = await this.history.modeOn(
          organizationId,
          a.workerId,
          completedAt,
          tx,
          a.worker,
        );
        const pieceCompensated = effectiveMode === WorkerCompMode.PER_PIECE;

        // Append-only ledger: create the completion first, then write the
        // earning ONCE with its final sourceId (previously the earning row
        // was created with sourceId NULL and updated afterwards, which both
        // mutated a ledger row and bypassed the (org, sourceType, sourceId)
        // uniqueness guard against duplicate earnings per completion).
        let completion = await tx.workCompletion.create({
          data: {
            organizationId,
            assignmentId: a.id,
            workerId: a.workerId,
            stage: a.stage,
            qty,
            rateAmount: a.rateAmount,
            completedAt,
            idempotencyKey: dto.clientRequestId ?? null,
            earningId: null,
            notes: dto.notes ?? null,
            createdById: actorUserId,
          },
        });

        let earningId: string | null = null;
        if (isPaid && a.rateAmount && pieceCompensated) {
          const amount = qty.mul(a.rateAmount).toDecimalPlaces(2);
          const earning = await tx.workerLedgerEntry.create({
            data: {
              organizationId,
              workerId: a.workerId,
              entryType: WorkerLedgerEntryType.PIECE_RATE_EARNING,
              amount,
              effectiveDate: completedAt,
              sourceType: 'work_completion',
              sourceId: completion.id,
              description: `${a.stage} x ${qty} @ ${a.rateAmount}`,
              createdById: actorUserId,
            },
          });
          earningId = earning.id;
          completion = await tx.workCompletion.update({ where: { id: completion.id }, data: { earningId } });
          // Phase 7 row 14: piece earning → Dr stage COS / Cr Worker Payable.
          await this.workerPosting.postLedgerEntry(tx, {
            organizationId,
            entry: earning,
            actorUserId,
            stage: a.stage,
            orderId: a.orderId,
          });
        }

        const newCompleted = a.qtyCompleted.add(qty);
        const fullyDone = newCompleted.greaterThanOrEqualTo(a.qtyAssigned);
        const assignment = await tx.workAssignment.update({
          where: { id: a.id },
          data: {
            qtyCompleted: newCompleted,
            status: fullyDone ? AssignmentStatus.COMPLETED : AssignmentStatus.PARTIALLY_COMPLETED,
          },
          include: this.assignmentInclude,
        });

        // Stage aggregate: completed only when every non-cancelled assignment
        // for this order+stage is complete.
        const open = await tx.workAssignment.count({
          where: {
            organizationId,
            orderId: a.orderId,
            stage: a.stage,
            status: { notIn: [AssignmentStatus.COMPLETED, AssignmentStatus.CANCELLED] },
          },
        });
        await this.upsertStageState(
          tx,
          organizationId,
          a.orderId,
          a.stage,
          open === 0 ? StageTaskStatus.COMPLETED : StageTaskStatus.PARTIALLY_COMPLETED,
          actorUserId,
        );
        return { completion, assignment, duplicate: false };
      });

      await this.audit.write({
        organizationId,
        actorUserId,
        action: 'production.completion',
        entityType: 'work_completion',
        entityId: result.completion.id,
        after: {
          assignmentId,
          qty: String(result.completion.qty),
          earningId: result.completion.earningId,
          rateAmount: result.completion.rateAmount ? String(result.completion.rateAmount) : null,
        },
      });
      return result;
    } catch (e) {
      // Unique (org, idempotency_key) race: return the original result.
      if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002' && dto.clientRequestId) {
        const existing = await this.prisma.workCompletion.findFirst({
          where: { organizationId, idempotencyKey: dto.clientRequestId },
          include: { earning: true, assignment: { include: this.assignmentInclude } },
        });
        if (existing) return { completion: existing, assignment: existing.assignment, duplicate: true };
      }
      throw e;
    }
  }

  // -------------------------------------------------- stage + order status

  private async upsertStageState(
    tx: Prisma.TransactionClient,
    organizationId: string,
    orderId: string,
    stage: ProductionStage,
    status: StageTaskStatus,
    actorUserId: string,
    reason?: string,
  ) {
    await tx.orderStageState.upsert({
      where: { orderId_stage: { orderId, stage } },
      update: { status, updatedById: actorUserId, ...(reason ? { reason } : {}) },
      create: { organizationId, orderId, stage, status, updatedById: actorUserId, reason: reason ?? null },
    });
  }

  async updateStage(organizationId: string, orderId: string, dto: StageUpdateInput, actorUserId: string) {
    const order = await this.prisma.order.findFirst({ where: { id: orderId, organizationId } });
    if (!order) throw new NotFoundException('Order not found');
    const before = await this.prisma.orderStageState.findUnique({
      where: { orderId_stage: { orderId, stage: dto.stage as ProductionStage } },
    });
    // Explicit domain action; rollbacks (moving away from COMPLETED) need a reason.
    if (before?.status === StageTaskStatus.COMPLETED && dto.status !== 'COMPLETED' && !dto.reason) {
      throw new BadRequestException('Rolling back a completed stage requires a reason');
    }
    await this.prisma.$transaction((tx) =>
      this.upsertStageState(
        tx,
        organizationId,
        orderId,
        dto.stage as ProductionStage,
        dto.status as StageTaskStatus,
        actorUserId,
        dto.reason,
      ),
    );
    await this.audit.write({
      organizationId,
      actorUserId,
      action: 'production.stage_update',
      entityType: 'order_stage_state',
      entityId: orderId,
      before: { stage: dto.stage, status: before?.status ?? null },
      after: { stage: dto.stage, status: dto.status },
      reason: dto.reason ?? null,
    });
    return this.prisma.orderStageState.findMany({ where: { orderId, organizationId } });
  }

  async updateOrderStatus(
    organizationId: string,
    orderId: string,
    dto: OrderStatusUpdateInput,
    actor: { id: string; permissions: string[]; isSuperAdmin: boolean },
  ) {
    const exists = await this.prisma.order.findFirst({ where: { id: orderId, organizationId }, select: { id: true } });
    if (!exists) throw new NotFoundException('Order not found');
    const target = dto.status as OrderStatus;
    if (target === OrderStatus.DELIVERED) {
      const canDeliver = actor.isSuperAdmin || actor.permissions.includes('delivery.perform');
      if (!canDeliver) throw new ForbiddenException('Delivery requires the delivery.perform permission');
    }

    // Lock the order row and re-check everything inside the transaction so a
    // concurrent assignment creation cannot race the READY guard.
    const { updated, order } = await this.prisma.$transaction(async (tx) => {
      await tx.$queryRaw`SELECT id FROM orders WHERE id = ${orderId} FOR UPDATE`;
      const order = await tx.order.findFirstOrThrow({ where: { id: orderId, organizationId } });

      const valid: Record<string, OrderStatus[]> = {
        [OrderStatus.CONFIRMED]: [OrderStatus.IN_PRODUCTION],
        [OrderStatus.IN_PRODUCTION]: [OrderStatus.READY],
        [OrderStatus.READY]: [OrderStatus.DELIVERED],
      };
      if (!valid[order.status]?.includes(target)) {
        throw new ConflictException(`Invalid order status transition ${order.status} -> ${target}`);
      }
      if (target === OrderStatus.READY) {
        // READY requires every non-cancelled assignment complete and QC not pending/in progress.
        const open = await tx.workAssignment.count({
          where: {
            organizationId,
            orderId,
            status: { notIn: [AssignmentStatus.COMPLETED, AssignmentStatus.CANCELLED] },
          },
        });
        if (open > 0) throw new ConflictException(`Order has ${open} incomplete assignment(s); cannot mark READY`);
        const qc = await tx.orderStageState.findUnique({
          where: { orderId_stage: { orderId, stage: ProductionStage.QUALITY_CONTROL } },
        });
        const qcOk: StageTaskStatus[] = [StageTaskStatus.COMPLETED, StageTaskStatus.NOT_REQUIRED];
        if (qc && !qcOk.includes(qc.status)) {
          throw new ConflictException(`Quality Control stage is ${qc.status}; cannot mark READY`);
        }
      }

      const deliveredNow = target === OrderStatus.DELIVERED;
      const updated = await tx.order.update({
        where: { id: orderId },
        data: { status: target, ...(deliveredNow ? { actualDeliveryDate: new Date() } : {}) },
      });

      // Phase 8: Emit business events inside the same transaction (outbox pattern).
      // Only for MODERN orders — never for legacy/imported/backfill paths.
      const isModern = order.source === OrderSource.MODERN;

      if (target === OrderStatus.READY && isModern) {
        // ORDER_READY: customer-safe payload only.
        const customer = await tx.customer.findFirst({
          where: { id: order.customerId, organizationId },
          select: { id: true, name: true },
        });
        await this.businessEvents.emit(tx, {
          organizationId,
          eventType: 'ORDER_READY',
          entityType: 'Order',
          entityId: orderId,
          dedupeKey: `ORDER_READY:${orderId}`,
          payload: {
            orderId,
            orderNumber: order.orderNumber,
            customerId: order.customerId,
            customerName: customer?.name ?? '',
            readyDate: new Date().toISOString().slice(0, 10),
          },
        });
      }

      if (deliveredNow) {
        // Phase 7: revenue recognition at delivery (invoice + AR + advance application).
        const postingResult = await this.deliveryPosting.postDelivery(tx, {
          organizationId,
          orderId,
          actorUserId: actor.id,
        });

        // Phase 8: INVOICE_ISSUED + DELIVERY_CONFIRMED only when postDelivery returns an invoice.
        if (postingResult && isModern) {
          const { invoiceId, invoiceNumber } = postingResult;
          const customer = await tx.customer.findFirst({
            where: { id: order.customerId, organizationId },
            select: { id: true, name: true },
          });
          const deliveryDateStr = new Date().toISOString().slice(0, 10);

          await this.businessEvents.emit(tx, {
            organizationId,
            eventType: 'INVOICE_ISSUED',
            entityType: 'CustomerInvoice',
            entityId: invoiceId,
            dedupeKey: `INVOICE_ISSUED:${invoiceId}`,
            payload: {
              invoiceId,
              invoiceNumber,
              orderId,
              orderNumber: order.orderNumber,
              customerId: order.customerId,
              customerName: customer?.name ?? '',
              invoiceDate: deliveryDateStr,
              netAmount: String(order.grandTotal),
              currency: 'PKR',
            },
          });

          await this.businessEvents.emit(tx, {
            organizationId,
            eventType: 'DELIVERY_CONFIRMED',
            entityType: 'Order',
            entityId: orderId,
            dedupeKey: `DELIVERY_CONFIRMED:${orderId}`,
            payload: {
              orderId,
              orderNumber: order.orderNumber,
              customerId: order.customerId,
              customerName: customer?.name ?? '',
              deliveryDate: deliveryDateStr,
              invoiceId,
              invoiceNumber,
            },
          });
        }
      }

      return { updated, order };
    }, { timeout: 60000, maxWait: 15000 });
    await this.audit.write({
      organizationId,
      actorUserId: actor.id,
      action: 'order.status_update',
      entityType: 'order',
      entityId: orderId,
      before: { status: order.status },
      after: { status: target },
      reason: dto.reason ?? null,
    });
    return updated;
  }

  // -------------------------------------------------- reads

  async orderProduction(organizationId: string, orderId: string) {
    const order = await this.prisma.order.findFirst({
      where: { id: orderId, organizationId },
      include: {
        customer: { select: { id: true, name: true, customerNumber: true } },
        garmentLines: true,
        designLines: true,
        measurementVersion: { select: { id: true, versionNo: true, type: true } },
      },
    });
    if (!order) throw new NotFoundException('Order not found');
    const [assignments, stageStates] = await Promise.all([
      this.prisma.workAssignment.findMany({
        where: { organizationId, orderId },
        include: this.assignmentInclude,
        orderBy: { createdAt: 'asc' },
      }),
      this.prisma.orderStageState.findMany({ where: { organizationId, orderId } }),
    ]);
    // Per line+stage capacity summary (no N+1: computed from loaded rows).
    const capacity: Record<string, { assigned: string; completed: string }> = {};
    for (const a of assignments) {
      if (a.status === AssignmentStatus.CANCELLED) continue;
      const key = `${a.garmentLineId ?? a.designLineId ?? 'order'}:${a.stage}`;
      const cur = capacity[key] ?? { assigned: '0', completed: '0' };
      capacity[key] = {
        assigned: dec(cur.assigned).add(a.qtyAssigned).toString(),
        completed: dec(cur.completed).add(a.qtyCompleted).toString(),
      };
    }
    return {
      order: {
        id: order.id,
        orderNumber: order.orderNumber,
        orderType: order.orderType,
        status: order.status,
        customer: order.customer,
        measurementVersion: order.measurementVersion,
        promisedDeliveryDate: order.promisedDeliveryDate,
        urgentFlag: order.urgentFlag,
        kamizQty: order.kamizQty,
        kurtaQty: order.kurtaQty,
        shalwarQty: order.shalwarQty,
        pajamaQty: order.pajamaQty,
        bshShirtQty: order.bshShirtQty,
        opShirtQty: order.opShirtQty,
        safShirtQty: order.safShirtQty,
        pantQty: order.pantQty,
        notes: order.notes,
      },
      garmentLines: order.garmentLines.map((l) => ({ id: l.id, itemName: l.itemName, qty: l.qty, notes: l.notes })),
      designLines: order.designLines.map((l) => ({ id: l.id, itemName: l.itemName, qty: l.qty, notes: l.notes })),
      assignments,
      stageStates,
      capacity,
    };
  }

  /** Worker slip DTO — read-only, zero business side effects, no financials. */
  async slipData(organizationId: string, orderId: string, filter: { workerId?: string; stage?: string }) {
    const order = await this.prisma.order.findFirst({
      where: { id: orderId, organizationId },
      include: {
        customer: { select: { name: true, customerNumber: true } },
        measurementVersion: { include: { ks: true, cp: true } },
        organization: { select: { name: true } },
      },
    });
    if (!order) throw new NotFoundException('Order not found');
    const assignments = await this.prisma.workAssignment.findMany({
      where: {
        organizationId,
        orderId,
        status: { not: AssignmentStatus.CANCELLED },
        ...(filter.workerId ? { workerId: filter.workerId } : {}),
        ...(filter.stage ? { stage: filter.stage as ProductionStage } : {}),
      },
      include: {
        worker: { select: { id: true, name: true, workerNumber: true } },
        garmentLine: { select: { itemName: true, qty: true, notes: true } },
        designLine: { select: { itemName: true, qty: true, notes: true } },
        rateItem: { select: { primaryImageId: true, name: true } },
      },
      orderBy: [{ workerId: 'asc' }, { stage: 'asc' }],
    });
    return {
      organizationName: order.organization.name,
      order: {
        id: order.id,
        orderNumber: order.orderNumber,
        orderType: order.orderType,
        promisedDeliveryDate: order.promisedDeliveryDate,
        trialDate: order.trialDate,
        urgentFlag: order.urgentFlag,
        notes: order.notes,
      },
      customer: order.customer,
      measurement: order.measurementVersion
        ? {
            versionNo: order.measurementVersion.versionNo,
            type: order.measurementVersion.type,
            ks: order.measurementVersion.ks,
            cp: order.measurementVersion.cp,
          }
        : null,
      assignments: assignments.map((a) => ({
        id: a.id,
        stage: a.stage,
        worker: a.worker,
        line: a.garmentLine ?? a.designLine,
        qtyAssigned: a.qtyAssigned,
        dueDate: a.dueDate,
        notes: a.notes,
        designImageId: a.rateItem?.primaryImageId ?? null,
        rateItemName: a.rateItem?.name ?? null,
      })),
    };
  }

  async workerAssignments(organizationId: string, workerId: string, query: { page: number; pageSize: number }) {
    const where = { organizationId, workerId };
    const [total, items] = await Promise.all([
      this.prisma.workAssignment.count({ where }),
      this.prisma.workAssignment.findMany({
        where,
        include: {
          order: { select: { id: true, orderNumber: true, orderType: true, status: true } },
          garmentLine: { select: { itemName: true } },
          designLine: { select: { itemName: true } },
          completions: { orderBy: { createdAt: 'asc' } },
        },
        orderBy: { createdAt: 'desc' },
        skip: (query.page - 1) * query.pageSize,
        take: query.pageSize,
      }),
    ]);
    return { total, page: query.page, pageSize: query.pageSize, items };
  }

  /** Remote worker search for assignment (org + active + stage eligibility + q). */
  async searchWorkers(organizationId: string, query: { q?: string; stage?: string; page: number; pageSize: number }) {
    const where: Prisma.WorkerWhereInput = {
      organizationId,
      isActive: true,
      archivedAt: null,
      ...(query.stage
        ? {
            typeLinks: {
              some: { workerType: { code: STAGE_WORKER_TYPE[query.stage as ProductionStage], isActive: true } },
            },
          }
        : {}),
      ...(query.q
        ? {
            OR: [
              { name: { contains: query.q } },
              { workerNumber: { contains: query.q } },
              { mobile: { contains: query.q } },
            ],
          }
        : {}),
    };
    const [total, items] = await Promise.all([
      this.prisma.worker.count({ where }),
      this.prisma.worker.findMany({
        where,
        select: {
          id: true,
          name: true,
          workerNumber: true,
          compensationMode: true,
          typeLinks: { select: { workerType: { select: { code: true, name: true } } } },
        },
        orderBy: { name: 'asc' },
        skip: (query.page - 1) * query.pageSize,
        take: query.pageSize,
      }),
    ]);
    return { total, page: query.page, pageSize: query.pageSize, items };
  }
}
