import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common';
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
import { z } from 'zod';
import {
  CreateOrderSchema, UpdateOrderSchema, OrderListQuerySchema, HoldOrderSchema, CancelOrderSchema, AllocateOrdersSchema, CreateWaveSchema, WaveQuerySchema,
  StartPackingSchema, PackCartonSchema, CompletePackingSchema, CreateLoadSchema, UpdateLoadSchema, LoadShipmentsSchema, DispatchLoadSchema, RecordPodSchema, ShipOrderSchema,
  CreateReturnSchema, DispositionReturnSchema, PaginationQuery, Uuid,
} from '@wms/shared';
import { RequirePermissions, ApiKeyScopes } from '@/common/auth/decorators';
import { zodBody, zodQuery } from '@/common/validation/zod.pipe';
import { DbService } from '@/common/db/db.service';
import { OrdersService } from './orders.service';
import { AllocationService } from './allocation.service';
import { WavesService } from './waves.service';
import { PackingService } from './packing.service';
import { PickingService } from './picking.service';
import { ShippingService } from './shipping.service';
import { ReturnsService } from './returns.service';
import { ReplenishmentService } from './replenishment.service';

const OrderQuery = OrderListQuerySchema.merge(PaginationQuery);
const WaveListQuery = WaveQuerySchema.merge(PaginationQuery);
const LoadQuery = PaginationQuery.extend({ warehouseId: Uuid.optional(), status: z.string().optional() });
const ReturnQuery = PaginationQuery.extend({ warehouseId: Uuid.optional(), storerId: Uuid.optional(), status: z.string().optional() });

@ApiTags('outbound')
@ApiBearerAuth()
@Controller('outbound')
export class OutboundController {
  constructor(
    private readonly db: DbService,
    private readonly orders: OrdersService,
    private readonly allocation: AllocationService,
    private readonly waves: WavesService,
    private readonly packing: PackingService,
    private readonly picking: PickingService,
    private readonly shipping: ShippingService,
    private readonly returns: ReturnsService,
    private readonly replenishment: ReplenishmentService,
  ) {}

  // ---- orders ----
  @Get('orders') @RequirePermissions('outbound.read') @ApiKeyScopes('orders:read') listOrders(@Query(zodQuery(OrderQuery)) q: z.infer<typeof OrderQuery>) { return this.orders.list(q); }
  @Get('orders/:id') @RequirePermissions('outbound.read') @ApiKeyScopes('orders:read') getOrder(@Param('id') id: string) { return this.orders.get(id); }
  @Post('orders') @RequirePermissions('outbound.manage') createOrder(@Body(zodBody(CreateOrderSchema)) dto: z.infer<typeof CreateOrderSchema>) { return this.orders.create(dto); }
  @Patch('orders/:id') @RequirePermissions('outbound.manage') updateOrder(@Param('id') id: string, @Body(zodBody(UpdateOrderSchema)) dto: z.infer<typeof UpdateOrderSchema>) { return this.orders.update(id, dto); }
  @Post('orders/:id/hold') @RequirePermissions('outbound.manage') hold(@Param('id') id: string, @Body(zodBody(HoldOrderSchema)) dto: z.infer<typeof HoldOrderSchema>) { return this.orders.hold(id, dto.reason); }
  @Post('orders/:id/release-hold') @RequirePermissions('outbound.manage') releaseHold(@Param('id') id: string) { return this.orders.releaseHold(id); }
  @Post('orders/:id/cancel') @RequirePermissions('outbound.manage') cancelOrder(@Param('id') id: string, @Body(zodBody(CancelOrderSchema)) dto: z.infer<typeof CancelOrderSchema>) { return this.orders.cancel(id, dto.reason, dto.reasonCodeId); }
  @Post('orders/:id/ship') @RequirePermissions('outbound.ship') shipOrder(@Param('id') id: string, @Body(zodBody(ShipOrderSchema)) dto: z.infer<typeof ShipOrderSchema>) { return this.shipping.shipOrder(id, dto); }

  @Post('orders/allocate') @RequirePermissions('outbound.allocate')
  allocate(@Body(zodBody(AllocateOrdersSchema)) dto: z.infer<typeof AllocateOrdersSchema>) {
    return this.db.run(async (tx) => {
      const out = [];
      for (const id of dto.orderIds) {
        // Released/picking orders may be re-allocated after a short pick; new reservations get pick tasks immediately.
        const res = await this.allocation.allocateOrder(tx, id, dto.allowPartial, { statuses: ['OPEN', 'PARTIALLY_ALLOCATED', 'ALLOCATED', 'RELEASED', 'PICKING', 'PICKED'] });
        const newIds = res.flatMap((r) => r.allocationIds);
        if (newIds.length) await this.picking.createPickTasksForAllocations(tx, id, newIds);
        await this.orders.clearShortIfCovered(tx, id);
        const status = await this.orders.refreshStatus(tx, id);
        out.push({ orderId: id, status, lines: res.map((r) => ({ lineId: r.lineId, requested: r.requested.toString(), allocated: r.allocated.toString(), short: r.short.toString() })) });
      }
      return out;
    });
  }

  @Post('orders/:id/accept-short') @RequirePermissions('outbound.manage')
  acceptShort(@Param('id') id: string, @Body() body: { notes?: string }) { return this.orders.acceptShort(id, body?.notes); }

  @Post('orders/:id/deallocate') @RequirePermissions('outbound.allocate')
  deallocate(@Param('id') id: string) {
    return this.db.run(async (tx) => {
      await this.allocation.deallocateOrder(tx, id);
      return this.orders.getInTx(tx, id);
    });
  }

  @Get('atp') @RequirePermissions('outbound.read')
  atp(@Query('warehouseId') warehouseId: string, @Query('storerId') storerId: string, @Query('skuId') skuId: string) {
    return this.db.run(async (tx) => {
      const r = await this.allocation.atp(tx, warehouseId, storerId, skuId);
      return { onHand: r.onHand.toString(), allocated: r.allocated.toString(), available: r.available.toString() };
    });
  }

  // ---- waves ----
  @Get('waves') @RequirePermissions('outbound.read') listWaves(@Query(zodQuery(WaveListQuery)) q: z.infer<typeof WaveListQuery>) { return this.waves.list(q); }
  @Get('waves/:id') @RequirePermissions('outbound.read') getWave(@Param('id') id: string) { return this.waves.get(id); }
  @Post('waves') @RequirePermissions('outbound.allocate') createWave(@Body(zodBody(CreateWaveSchema)) dto: z.infer<typeof CreateWaveSchema>) { return this.waves.create(dto); }
  @Post('waves/:id/allocate') @RequirePermissions('outbound.allocate') allocateWave(@Param('id') id: string, @Query('allowPartial') allowPartial?: string) { return this.waves.allocate(id, allowPartial !== 'false'); }
  @Post('waves/:id/release') @RequirePermissions('outbound.allocate') releaseWave(@Param('id') id: string) { return this.waves.release(id); }
  @Post('waves/:id/pick-list') @RequirePermissions('outbound.read') pickList(@Param('id') id: string) { return this.waves.pickList(id); }
  @Post('waves/:id/cancel') @RequirePermissions('outbound.allocate') cancelWave(@Param('id') id: string) { return this.waves.cancel(id); }

  // ---- packing ----
  @Post('packing/start') @RequirePermissions('outbound.pack') startPacking(@Body(zodBody(StartPackingSchema)) dto: z.infer<typeof StartPackingSchema>) { return this.packing.start(dto); }
  @Post('packing/carton') @RequirePermissions('outbound.pack') packCarton(@Body(zodBody(PackCartonSchema)) dto: z.infer<typeof PackCartonSchema>) { return this.packing.packCarton(dto); }
  @Delete('packing/carton/:id') @RequirePermissions('outbound.pack') removeCarton(@Param('id') id: string) { return this.packing.removeCarton(id); }
  @Post('packing/complete') @RequirePermissions('outbound.pack') completePacking(@Body(zodBody(CompletePackingSchema)) dto: z.infer<typeof CompletePackingSchema>) { return this.packing.complete(dto); }
  @Get('shipments/:id') @RequirePermissions('outbound.read') getShipment(@Param('id') id: string) { return this.packing.getShipment(id); }

  // ---- loads ----
  @Get('loads') @RequirePermissions('outbound.read') listLoads(@Query(zodQuery(LoadQuery)) q: z.infer<typeof LoadQuery>) { return this.shipping.list(q); }
  @Get('loads/:id') @RequirePermissions('outbound.read') getLoad(@Param('id') id: string) { return this.shipping.get(id); }
  @Post('loads') @RequirePermissions('outbound.ship') createLoad(@Body(zodBody(CreateLoadSchema)) dto: z.infer<typeof CreateLoadSchema>) { return this.shipping.create(dto); }
  @Patch('loads/:id') @RequirePermissions('outbound.ship') updateLoad(@Param('id') id: string, @Body(zodBody(UpdateLoadSchema)) dto: z.infer<typeof UpdateLoadSchema>) { return this.shipping.update(id, dto); }
  @Post('loads/:id/shipments') @RequirePermissions('outbound.ship') attach(@Param('id') id: string, @Body(zodBody(LoadShipmentsSchema)) dto: z.infer<typeof LoadShipmentsSchema>) { return this.shipping.attach(id, dto); }
  @Delete('loads/:id/shipments/:shipmentId') @RequirePermissions('outbound.ship') detach(@Param('id') id: string, @Param('shipmentId') shipmentId: string) { return this.shipping.detach(id, shipmentId); }
  @Post('loads/:id/shipments/:shipmentId/loaded') @RequirePermissions('outbound.ship') loaded(@Param('id') id: string, @Param('shipmentId') shipmentId: string) { return this.shipping.confirmLoaded(id, shipmentId); }
  @Post('loads/:id/dispatch') @RequirePermissions('outbound.ship') dispatch(@Param('id') id: string, @Body(zodBody(DispatchLoadSchema)) dto: z.infer<typeof DispatchLoadSchema>) { return this.shipping.dispatch(id, dto); }
  @Post('loads/:id/pod') @RequirePermissions('outbound.ship') pod(@Param('id') id: string, @Body(zodBody(RecordPodSchema)) dto: z.infer<typeof RecordPodSchema>) { return this.shipping.recordPod(id, dto); }

  // ---- returns ----
  @Get('returns') @RequirePermissions('outbound.read') listReturns(@Query(zodQuery(ReturnQuery)) q: z.infer<typeof ReturnQuery>) { return this.returns.list(q); }
  @Get('returns/:id') @RequirePermissions('outbound.read') getReturn(@Param('id') id: string) { return this.returns.get(id); }
  @Post('returns') @RequirePermissions('returns.manage') createReturn(@Body(zodBody(CreateReturnSchema)) dto: z.infer<typeof CreateReturnSchema>) { return this.returns.create(dto); }
  @Post('returns/:id/authorise') @RequirePermissions('returns.manage') authorise(@Param('id') id: string) { return this.returns.authorise(id); }
  @Post('returns/:id/disposition') @RequirePermissions('returns.manage') disposition(@Param('id') id: string, @Body(zodBody(DispositionReturnSchema)) dto: z.infer<typeof DispositionReturnSchema>) { return this.returns.disposition(id, dto); }
  @Post('returns/:id/cancel') @RequirePermissions('returns.manage') cancelReturn(@Param('id') id: string) { return this.returns.cancel(id); }

  // ---- replenishment ----
  @Post('replenishment/run') @RequirePermissions('tasks.manage') runReplenishment(@Query('warehouseId') warehouseId: string) { return this.replenishment.run(warehouseId); }
}
