import { Injectable } from '@nestjs/common';
import { and, desc, eq, inArray, sql } from 'drizzle-orm';
import { CreateReturnDto, DispositionReturnSchema, PaginationQueryDto, InventoryStatus } from '@wms/shared';
import { z } from 'zod';
import { DbService, Tx } from '@/common/db/db.service';
import { returnOrders, returnOrderLines, asns, asnLines, receiptLines, skus, storers, outboundOrders } from '@/db/schema';
import { AuditService } from '@/common/audit/audit.service';
import { NumberingService } from '@/common/numbering/numbering.service';
import { EventBus } from '@/common/events/event-bus.service';
import { requireRequestContext } from '@/common/context/request-context';
import { DomainError, NotFoundError, ErrorCodes } from '@/common/errors/domain-error';
import { D, paginate, offsetOf, assertWarehouseAccess, resolveStorerScope, serialize } from '@/common/util';
import { LookupService } from '@/modules/master-data/lookup.service';
import { LedgerService } from '@/modules/inventory/ledger.service';
import { PutawayService } from '@/modules/inventory/putaway.service';
import { TasksService } from '@/modules/tasks/tasks.service';
import { AsnService } from '@/modules/inbound/asn.service';
import { OrdersService } from './orders.service';

/**
 * Returns (RMA). Authorising a return creates an ASN of type RETURN so the
 * goods are received with the normal RF receiving flow (lot, serial, condition
 * capture). After receipt, each line is dispositioned: restock (putaway task),
 * quarantine, scrap (adjust out), or return to vendor (outbound order).
 */
@Injectable()
export class ReturnsService {
  constructor(
    private readonly db: DbService,
    private readonly lookup: LookupService,
    private readonly ledger: LedgerService,
    private readonly putaway: PutawayService,
    private readonly tasksService: TasksService,
    private readonly asnService: AsnService,
    private readonly orders: OrdersService,
    private readonly numbering: NumberingService,
    private readonly audit: AuditService,
    private readonly events: EventBus,
  ) {}

  list(q: PaginationQueryDto & { warehouseId?: string; storerId?: string; status?: string }) {
    const storerId = resolveStorerScope(q.storerId);
    return this.db.run(async (tx) => {
      const where = and(q.warehouseId ? eq(returnOrders.warehouseId, q.warehouseId) : undefined, storerId ? eq(returnOrders.storerId, storerId) : undefined, q.status ? eq(returnOrders.status, q.status as never) : undefined);
      const [{ count }] = await tx.select({ count: sql<number>`count(*)`.mapWith(Number) }).from(returnOrders).where(where);
      const rows = await tx.select({ r: returnOrders, storerName: storers.name, asnNumber: asns.asnNumber, originalOrderNumber: outboundOrders.orderNumber }).from(returnOrders).innerJoin(storers, eq(storers.id, returnOrders.storerId)).leftJoin(asns, eq(asns.id, returnOrders.asnId)).leftJoin(outboundOrders, eq(outboundOrders.id, returnOrders.originalOrderId)).where(where).orderBy(desc(returnOrders.createdAt)).limit(q.pageSize).offset(offsetOf(q));
      return paginate(rows.map((x) => ({ ...x.r, storerName: x.storerName, asnNumber: x.asnNumber, originalOrderNumber: x.originalOrderNumber })), count, q);
    });
  }

  get(id: string) {
    return this.db.run((tx) => this.getInTx(tx, id));
  }

  async getInTx(tx: Tx, id: string) {
    const [r] = await tx.select().from(returnOrders).where(eq(returnOrders.id, id)).limit(1);
    if (!r) throw new NotFoundError('Return', id);
    resolveStorerScope(r.storerId);
    const lines = await tx.select({ l: returnOrderLines, skuCode: skus.code, description: skus.description }).from(returnOrderLines).innerJoin(skus, eq(skus.id, returnOrderLines.skuId)).where(eq(returnOrderLines.returnId, id)).orderBy(returnOrderLines.lineNo);
    const asn = r.asnId ? await this.asnService.getInTx(tx, r.asnId) : null;
    return serialize({ ...r, lines: lines.map((x) => ({ ...x.l, skuCode: x.skuCode, description: x.description })), asn });
  }

  create(dto: CreateReturnDto) {
    assertWarehouseAccess(dto.warehouseId);
    const ctx = requireRequestContext();
    const storerId = resolveStorerScope(dto.storerId)!;
    return this.db.run(async (tx) => {
      const rmaNumber = await this.numbering.next('RMA', tx, dto.warehouseId);
      const [r] = await tx.insert(returnOrders).values({ tenantId: ctx.tenantId!, warehouseId: dto.warehouseId, storerId, rmaNumber, originalOrderId: dto.originalOrderId ?? null, reasonCodeId: dto.reasonCodeId ?? null, customerRef: dto.customerRef ?? null, notes: dto.notes ?? null, createdById: ctx.userId! }).returning();
      let lineNo = 0;
      for (const l of dto.lines) {
        lineNo++;
        const sku = l.skuId ? await this.lookup.sku(tx, l.skuId) : await this.lookup.skuByCode(tx, storerId, l.skuCode!);
        if (sku.storerId !== storerId) throw new DomainError(ErrorCodes.WRONG_SKU, `SKU ${sku.code} does not belong to the storer`);
        await tx.insert(returnOrderLines).values({ tenantId: ctx.tenantId!, returnId: r.id, lineNo, skuId: sku.id, expectedQty: D(l.expectedQty), serials: l.serials, reasonCodeId: l.reasonCodeId ?? null, notes: l.notes ?? null });
      }
      await this.audit.log({ action: 'return.create', entityType: 'return', entityId: r.id, after: dto }, tx);
      if (dto.authoriseNow) await this.authoriseInTx(tx, r.id);
      return this.getInTx(tx, r.id);
    });
  }

  authorise(id: string) {
    return this.db.run(async (tx) => {
      await this.authoriseInTx(tx, id);
      return this.getInTx(tx, id);
    });
  }

  private async authoriseInTx(tx: Tx, id: string) {
    const [r] = await tx.select().from(returnOrders).where(eq(returnOrders.id, id)).limit(1).for('update');
    if (!r) throw new NotFoundError('Return', id);
    if (r.status !== 'REQUESTED') throw new DomainError(ErrorCodes.INVALID_STATE, `Return is ${r.status}`);
    const lines = await tx.select().from(returnOrderLines).where(eq(returnOrderLines.returnId, id)).orderBy(returnOrderLines.lineNo);
    const asn = await this.asnService.create(
      { warehouseId: r.warehouseId, storerId: r.storerId, externalRef: r.rmaNumber, type: 'RETURN', priority: 5, documents: [], attributes: { returnId: r.id }, notes: `Return ${r.rmaNumber}${r.customerRef ? ` / ${r.customerRef}` : ''}`, lines: lines.map((l) => ({ lineNo: l.lineNo, skuId: l.skuId, expectedQty: l.expectedQty.toString(), packLevel: 'EACH' as const })) },
      'SYSTEM',
      tx,
    );
    await tx.update(returnOrders).set({ status: 'AUTHORISED', asnId: (asn as { id: string }).id }).where(eq(returnOrders.id, id));
    await this.audit.log({ action: 'return.authorise', entityType: 'return', entityId: id, after: { asnId: (asn as { id: string }).id } }, tx);
  }

  /** Sync received quantities from the RETURN ASN (called after ASN close via event, or on demand). */
  async syncReceived(tx: Tx, returnId: string) {
    const [r] = await tx.select().from(returnOrders).where(eq(returnOrders.id, returnId)).limit(1).for('update');
    if (!r?.asnId) return;
    const lines = await tx.select().from(returnOrderLines).where(eq(returnOrderLines.returnId, returnId));
    const asnLineRows = await tx.select().from(asnLines).where(eq(asnLines.asnId, r.asnId));
    for (const l of lines) {
      const al = asnLineRows.find((a) => a.lineNo === l.lineNo);
      if (al) await tx.update(returnOrderLines).set({ receivedQty: D(al.receivedQty).plus(al.damagedQty).plus(al.rejectedQty) }).where(eq(returnOrderLines.id, l.id));
    }
    const [asn] = await tx.select({ status: asns.status }).from(asns).where(eq(asns.id, r.asnId)).limit(1);
    const status = asn.status === 'CLOSED' ? 'RECEIVED' : asn.status === 'RECEIVING' ? 'RECEIVING' : r.status;
    if (status !== r.status) await tx.update(returnOrders).set({ status }).where(eq(returnOrders.id, returnId));
    if (status === 'RECEIVED') await this.events.publish('return.received', { returnId, rmaNumber: r.rmaNumber }, tx, { warehouseId: r.warehouseId, storerId: r.storerId });
  }

  disposition(id: string, dto: z.infer<typeof DispositionReturnSchema>) {
    return this.db.run(async (tx) => {
      const [r] = await tx.select().from(returnOrders).where(eq(returnOrders.id, id)).limit(1).for('update');
      if (!r) throw new NotFoundError('Return', id);
      await this.syncReceived(tx, id);
      if (!['RECEIVED', 'DISPOSITIONED', 'RECEIVING'].includes(r.status) && r.status !== 'AUTHORISED') throw new DomainError(ErrorCodes.INVALID_STATE, `Return is ${r.status}`);
      const lines = await tx.select().from(returnOrderLines).where(and(eq(returnOrderLines.returnId, id), inArray(returnOrderLines.id, dto.lines.map((l) => l.lineId))));
      if (lines.length !== dto.lines.length) throw new NotFoundError('Return line');
      const receipts = r.asnId ? await tx.select().from(receiptLines).where(and(eq(receiptLines.asnId, r.asnId), sql`${receiptLines.reversedAt} is null`)) : [];
      for (const d of dto.lines) {
        const line = lines.find((l) => l.id === d.lineId)!;
        const sku = await this.lookup.sku(tx, line.skuId);
        const stock = receipts.filter((x) => x.skuId === line.skuId);
        for (const rc of stock) {
          const status: InventoryStatus = rc.condition === 'GOOD' ? 'AVAILABLE' : rc.condition === 'DAMAGED' ? 'DAMAGED' : 'QUARANTINE';
          const bal = await this.ledger.getBalance(tx, { warehouseId: r.warehouseId, storerId: r.storerId, skuId: sku.id, locationId: rc.locationId, lpnId: rc.lpnId, lotNumber: rc.lotNumber, expiryDate: rc.expiryDate, status }, true);
          if (!bal || D(bal.qtyOnHand).lte(0)) continue;
          const qty = D(bal.qtyOnHand);
          switch (d.disposition) {
            case 'RESTOCK': {
              if (status !== 'AVAILABLE') {
                await this.ledger.post(tx, { type: 'STATUS_CHANGE', qty, warehouseId: r.warehouseId, storerId: r.storerId, skuId: sku.id, from: { locationId: rc.locationId, lpnId: rc.lpnId, status }, to: { locationId: rc.locationId, lpnId: rc.lpnId, status: 'AVAILABLE' }, lotNumber: rc.lotNumber, expiryDate: rc.expiryDate, refType: 'RMA', refId: r.id, firstReceivedAt: bal.firstReceivedAt });
              }
              const suggestion = await this.putaway.suggest(tx, { warehouseId: r.warehouseId, storerId: r.storerId, sku, qty, packLevel: 'EACH', lotNumber: rc.lotNumber, lpnId: rc.lpnId, condition: 'GOOD', asnType: 'RETURN' });
              await this.tasksService.create(tx, { warehouseId: r.warehouseId, type: 'PUTAWAY', priority: 6, groupKey: r.id, storerId: r.storerId, skuId: sku.id, qty, packLevel: 'EACH', fromLocationId: rc.locationId, toLocationId: suggestion?.locationId ?? null, lpnId: rc.lpnId, lotNumber: rc.lotNumber, expiryDate: rc.expiryDate, refType: 'RMA', refId: r.id, data: { rmaNumber: r.rmaNumber, inventoryStatus: 'AVAILABLE', condition: 'GOOD', suggestion } });
              break;
            }
            case 'QUARANTINE':
              if (status !== 'QUARANTINE') await this.ledger.post(tx, { type: 'STATUS_CHANGE', qty, warehouseId: r.warehouseId, storerId: r.storerId, skuId: sku.id, from: { locationId: rc.locationId, lpnId: rc.lpnId, status }, to: { locationId: rc.locationId, lpnId: rc.lpnId, status: 'QUARANTINE' }, lotNumber: rc.lotNumber, expiryDate: rc.expiryDate, refType: 'RMA', refId: r.id, firstReceivedAt: bal.firstReceivedAt });
              break;
            case 'SCRAP':
              await this.ledger.post(tx, { type: 'ADJUST_OUT', qty, warehouseId: r.warehouseId, storerId: r.storerId, skuId: sku.id, from: { locationId: rc.locationId, lpnId: rc.lpnId, status }, lotNumber: rc.lotNumber, expiryDate: rc.expiryDate, serials: rc.serials, reasonCodeId: line.reasonCodeId ?? r.reasonCodeId, refType: 'RMA', refId: r.id, notes: `Scrapped on return ${r.rmaNumber}` });
              break;
            case 'RETURN_TO_VENDOR': {
              if (status !== 'AVAILABLE') await this.ledger.post(tx, { type: 'STATUS_CHANGE', qty, warehouseId: r.warehouseId, storerId: r.storerId, skuId: sku.id, from: { locationId: rc.locationId, lpnId: rc.lpnId, status }, to: { locationId: rc.locationId, lpnId: rc.lpnId, status: 'AVAILABLE' }, lotNumber: rc.lotNumber, expiryDate: rc.expiryDate, refType: 'RMA', refId: r.id, firstReceivedAt: bal.firstReceivedAt });
              // stock stays in the returns area; an RTV outbound order is created for it (allocation reads RESERVE/PICK only, so move to storage first or pick manually)
              await this.orders.create({ warehouseId: r.warehouseId, storerId: r.storerId, externalRef: `RTV-${r.rmaNumber}-${line.lineNo}`, type: 'RETURN_TO_VENDOR', priority: 5, shipTo: { name: 'Vendor return' }, attributes: { returnId: r.id }, lines: [{ skuId: sku.id, orderedQty: qty.toString(), packLevel: 'EACH', requestedSerials: [] }] }, 'SYSTEM', tx);
              break;
            }
            case 'REPAIR':
              await this.ledger.post(tx, { type: 'STATUS_CHANGE', qty, warehouseId: r.warehouseId, storerId: r.storerId, skuId: sku.id, from: { locationId: rc.locationId, lpnId: rc.lpnId, status }, to: { locationId: rc.locationId, lpnId: rc.lpnId, status: 'HOLD' }, lotNumber: rc.lotNumber, expiryDate: rc.expiryDate, refType: 'RMA', refId: r.id, firstReceivedAt: bal.firstReceivedAt });
              break;
          }
        }
        await tx.update(returnOrderLines).set({ disposition: d.disposition, notes: d.notes ?? line.notes }).where(eq(returnOrderLines.id, line.id));
      }
      const all = await tx.select().from(returnOrderLines).where(eq(returnOrderLines.returnId, id));
      const done = all.every((l) => l.disposition);
      await tx.update(returnOrders).set({ status: done ? 'CLOSED' : 'DISPOSITIONED' }).where(eq(returnOrders.id, id));
      await this.audit.log({ action: 'return.disposition', entityType: 'return', entityId: id, after: dto }, tx);
      await this.events.publish('return.dispositioned', { returnId: id, rmaNumber: r.rmaNumber, lines: dto.lines }, tx, { warehouseId: r.warehouseId, storerId: r.storerId });
      return this.getInTx(tx, id);
    });
  }

  cancel(id: string) {
    return this.db.run(async (tx) => {
      const [r] = await tx.select().from(returnOrders).where(eq(returnOrders.id, id)).limit(1).for('update');
      if (!r) throw new NotFoundError('Return', id);
      if (!['REQUESTED', 'AUTHORISED'].includes(r.status)) throw new DomainError(ErrorCodes.INVALID_STATE, `Return is ${r.status}`);
      if (r.asnId) await this.asnService.cancel(r.asnId, `Return ${r.rmaNumber} cancelled`, tx);
      const [row] = await tx.update(returnOrders).set({ status: 'REJECTED' }).where(eq(returnOrders.id, id)).returning();
      await this.audit.log({ action: 'return.cancel', entityType: 'return', entityId: id }, tx);
      return serialize(row);
    });
  }
}
