import { Injectable, Logger } from '@nestjs/common';
import { and, asc, desc, eq, inArray, or, sql, isNull } from 'drizzle-orm';
import { TaskQueryDto, NextTaskDto, ConfirmTaskDto, TaskExceptionDto, PaginationQueryDto, TaskType, CreateMoveTaskSchema, AssignTasksSchema, ReprioritiseTasksSchema, CancelTasksSchema } from '@wms/shared';
import { z } from 'zod';
import { DbService, Tx } from '@/common/db/db.service';
import { tasks, locations, skus, lpns, users, zones } 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, ValidationError } from '@/common/errors/domain-error';
import { D, paginate, offsetOf, assertWarehouseAccess, serialize, warehouseScopeFilter } from '@/common/util';
import { LookupService } from '@/modules/master-data/lookup.service';
import { LedgerService } from '@/modules/inventory/ledger.service';
import { InventoryService } from '@/modules/inventory/inventory.service';

export type TaskRow = typeof tasks.$inferSelect;
export interface TaskConfirmResult {
  task: TaskRow;
  completed: boolean;
  message?: string;
  next?: unknown;
}
export type TaskHandler = (tx: Tx, task: TaskRow, dto: ConfirmTaskDto) => Promise<TaskConfirmResult>;

export interface CreateTaskInput {
  warehouseId: string;
  type: TaskType;
  priority?: number;
  sequence?: number;
  groupKey?: string;
  zoneId?: string | null;
  storerId?: string | null;
  skuId?: string | null;
  qty?: InstanceType<typeof import('decimal.js').default> | string | null;
  packLevel?: 'EACH' | 'INNER' | 'CASE' | 'PALLET' | null;
  fromLocationId?: string | null;
  toLocationId?: string | null;
  lpnId?: string | null;
  toLpnId?: string | null;
  lotNumber?: string | null;
  expiryDate?: string | null;
  refType?: string;
  refId?: string;
  data?: Record<string, unknown>;
  assignedToId?: string | null;
}

/**
 * Unified work queue. Modules register a handler per task type; the RF client
 * only ever calls next / start / confirm / exception regardless of task type.
 */
@Injectable()
export class TasksService {
  private readonly logger = new Logger(TasksService.name);
  private readonly handlers = new Map<TaskType, TaskHandler>();

  constructor(
    private readonly db: DbService,
    private readonly numbering: NumberingService,
    private readonly audit: AuditService,
    private readonly events: EventBus,
    private readonly lookup: LookupService,
    private readonly ledger: LedgerService,
    private readonly inventory: InventoryService,
  ) {
    this.registerHandler('MOVE', (tx, task, dto) => this.handleMove(tx, task, dto));
  }

  registerHandler(type: TaskType, handler: TaskHandler) {
    this.handlers.set(type, handler);
  }

  async create(tx: Tx, input: CreateTaskInput): Promise<TaskRow> {
    const ctx = requireRequestContext();
    const taskNumber = await this.numbering.next('TASK', tx, input.warehouseId);
    let zoneId = input.zoneId ?? null;
    if (!zoneId && input.fromLocationId) zoneId = (await this.lookup.location(tx, input.fromLocationId)).zoneId;
    const [row] = await tx
      .insert(tasks)
      .values({
        tenantId: ctx.tenantId!,
        warehouseId: input.warehouseId,
        taskNumber,
        type: input.type,
        priority: input.priority ?? 5,
        sequence: input.sequence ?? 0,
        groupKey: input.groupKey ?? null,
        zoneId,
        storerId: input.storerId ?? null,
        skuId: input.skuId ?? null,
        qty: input.qty != null ? D(input.qty) : null,
        packLevel: input.packLevel ?? null,
        fromLocationId: input.fromLocationId ?? null,
        toLocationId: input.toLocationId ?? null,
        lpnId: input.lpnId ?? null,
        toLpnId: input.toLpnId ?? null,
        lotNumber: input.lotNumber ?? null,
        expiryDate: input.expiryDate ?? null,
        refType: input.refType ?? null,
        refId: input.refId ?? null,
        data: input.data ?? {},
        assignedToId: input.assignedToId ?? null,
        assignedAt: input.assignedToId ? new Date() : null,
        status: input.assignedToId ? 'ASSIGNED' : 'PENDING',
        createdById: ctx.userId,
      })
      .returning();
    await this.events.publish('task.created', { taskId: row.id, type: row.type, taskNumber: row.taskNumber }, tx, { warehouseId: input.warehouseId });
    return row;
  }

  // ---------------- queries ----------------
  list(q: TaskQueryDto & PaginationQueryDto) {
    return this.db.run(async (tx) => {
      const statuses = q.statuses ? q.statuses.split(',') : q.status ? [q.status] : undefined;
      const where = and(
        q.warehouseId ? eq(tasks.warehouseId, q.warehouseId) : undefined,
        q.type ? eq(tasks.type, q.type) : undefined,
        statuses ? inArray(tasks.status, statuses as never) : undefined,
        q.assignedToId ? eq(tasks.assignedToId, q.assignedToId) : undefined,
        q.groupKey ? eq(tasks.groupKey, q.groupKey) : undefined,
        q.storerId ? eq(tasks.storerId, q.storerId) : undefined,
        q.zoneId ? eq(tasks.zoneId, q.zoneId) : undefined,
        q.q ? or(sql`${tasks.taskNumber} ilike ${'%' + q.q + '%'}`, sql`${tasks.groupKey} ilike ${'%' + q.q + '%'}`) : undefined,
      );
      const [{ count }] = await tx.select({ count: sql<number>`count(*)`.mapWith(Number) }).from(tasks).where(where);
      const rows = await tx.select().from(tasks).where(where).orderBy(asc(tasks.status), asc(tasks.priority), asc(tasks.sequence), desc(tasks.createdAt)).limit(q.pageSize).offset(offsetOf(q));
      return paginate(await this.enrich(tx, rows), count, q);
    });
  }

  get(id: string) {
    return this.db.run(async (tx) => {
      const [t] = await tx.select().from(tasks).where(eq(tasks.id, id)).limit(1);
      if (!t) throw new NotFoundError('Task', id);
      return (await this.enrich(tx, [t]))[0];
    });
  }

  /** Queue summary for the supervisor dashboard. */
  summary(warehouseId: string) {
    assertWarehouseAccess(warehouseId);
    return this.db.run((tx) =>
      tx
        .select({ type: tasks.type, status: tasks.status, count: sql<number>`count(*)`.mapWith(Number) })
        .from(tasks)
        .where(and(eq(tasks.warehouseId, warehouseId), inArray(tasks.status, ['PENDING', 'ASSIGNED', 'IN_PROGRESS', 'EXCEPTION'])))
        .groupBy(tasks.type, tasks.status),
    );
  }

  // ---------------- RF flow ----------------
  /** Atomically claims the next task. Tasks already assigned to the caller come first. */
  next(dto: NextTaskDto) {
    assertWarehouseAccess(dto.warehouseId);
    const ctx = requireRequestContext();
    return this.db.run(async (tx) => {
      const types = dto.types?.length ? dto.types : (['RECEIVE', 'PUTAWAY', 'PICK', 'REPLENISH', 'MOVE', 'COUNT'] as TaskType[]);
      // 1. my own in-flight work
      const [mine] = await tx
        .select()
        .from(tasks)
        .where(and(eq(tasks.warehouseId, dto.warehouseId), eq(tasks.assignedToId, ctx.userId!), inArray(tasks.status, ['ASSIGNED', 'IN_PROGRESS']), inArray(tasks.type, types), dto.groupKey ? eq(tasks.groupKey, dto.groupKey) : undefined))
        .orderBy(asc(tasks.priority), asc(tasks.sequence), asc(tasks.createdAt))
        .limit(1);
      if (mine) return { task: (await this.enrich(tx, [mine]))[0], reclaimed: true };

      // 2. claim a pending task; SKIP LOCKED makes concurrent RF users safe
      const candidate = await tx
        .select({ id: tasks.id })
        .from(tasks)
        .where(and(eq(tasks.warehouseId, dto.warehouseId), eq(tasks.status, 'PENDING'), isNull(tasks.assignedToId), inArray(tasks.type, types), dto.zoneId ? eq(tasks.zoneId, dto.zoneId) : undefined, dto.groupKey ? eq(tasks.groupKey, dto.groupKey) : undefined))
        .orderBy(asc(tasks.priority), asc(tasks.sequence), asc(tasks.createdAt))
        .limit(1)
        .for('update', { skipLocked: true });
      if (!candidate.length) return { task: null, reclaimed: false };
      const [row] = await tx.update(tasks).set({ status: 'ASSIGNED', assignedToId: ctx.userId, assignedAt: new Date() }).where(eq(tasks.id, candidate[0].id)).returning();
      return { task: (await this.enrich(tx, [row]))[0], reclaimed: false };
    });
  }

  start(id: string) {
    const ctx = requireRequestContext();
    return this.db.run(async (tx) => {
      const t = await this.lockOwned(tx, id, ctx.userId!);
      if (t.status === 'IN_PROGRESS') return (await this.enrich(tx, [t]))[0];
      if (t.status !== 'ASSIGNED' && t.status !== 'PENDING') throw new DomainError(ErrorCodes.INVALID_STATE, `Task is ${t.status}`);
      const [row] = await tx.update(tasks).set({ status: 'IN_PROGRESS', startedAt: new Date(), assignedToId: ctx.userId, assignedAt: t.assignedAt ?? new Date() }).where(eq(tasks.id, id)).returning();
      return (await this.enrich(tx, [row]))[0];
    });
  }

  confirm(id: string, dto: ConfirmTaskDto) {
    const ctx = requireRequestContext();
    return this.db.run(async (tx) => {
      const t = await this.lockOwned(tx, id, ctx.userId!);
      if (dto.idempotencyKey) {
        const existing = await this.ledger.findByIdempotencyKey(tx, dto.idempotencyKey);
        if (existing) return serialize({ task: (await this.enrich(tx, [t]))[0], completed: t.status === 'COMPLETED', idempotent: true, message: 'Already confirmed' });
      }
      if (!['ASSIGNED', 'IN_PROGRESS', 'PENDING'].includes(t.status)) throw new DomainError(ErrorCodes.INVALID_STATE, `Task is ${t.status}`);
      const handler = this.handlers.get(t.type);
      if (!handler) throw new ValidationError(`Task type ${t.type} is not confirmed through this endpoint`);
      if (t.status !== 'IN_PROGRESS') await tx.update(tasks).set({ status: 'IN_PROGRESS', startedAt: t.startedAt ?? new Date(), assignedToId: ctx.userId }).where(eq(tasks.id, id));
      const result = await handler(tx, { ...t, status: 'IN_PROGRESS' }, dto);
      if (result.completed) {
        await this.events.publish('task.completed', { taskId: id, type: t.type, taskNumber: t.taskNumber, groupKey: t.groupKey, refType: t.refType, refId: t.refId }, tx, { warehouseId: t.warehouseId, storerId: t.storerId ?? undefined });
      }
      return serialize({ ...result, task: (await this.enrich(tx, [result.task]))[0] });
    });
  }

  exception(id: string, dto: TaskExceptionDto) {
    const ctx = requireRequestContext();
    return this.db.run(async (tx) => {
      const t = await this.lockOwned(tx, id, ctx.userId!);
      if (dto.code === 'SKIP') {
        // release back to the queue for someone else
        const [row] = await tx.update(tasks).set({ status: 'PENDING', assignedToId: null, assignedAt: null, startedAt: null, exceptionNotes: dto.notes }).where(eq(tasks.id, id)).returning();
        return (await this.enrich(tx, [row]))[0];
      }
      const [row] = await tx.update(tasks).set({ status: 'EXCEPTION', exceptionCode: dto.code, exceptionNotes: dto.notes }).where(eq(tasks.id, id)).returning();
      await this.audit.log({ action: 'task.exception', entityType: 'task', entityId: id, after: dto }, tx);
      await this.events.publish('task.exception', { taskId: id, type: t.type, code: dto.code, taskNumber: t.taskNumber, refType: t.refType, refId: t.refId }, tx, { warehouseId: t.warehouseId });
      return (await this.enrich(tx, [row]))[0];
    });
  }

  // ---------------- supervisor ----------------
  assign(dto: z.infer<typeof AssignTasksSchema>) {
    return this.db.run(async (tx) => {
      if (dto.userId) {
        const [u] = await tx.select({ id: users.id }).from(users).where(eq(users.id, dto.userId)).limit(1);
        if (!u) throw new NotFoundError('User', dto.userId);
      }
      const rows = await tx
        .update(tasks)
        .set({ assignedToId: dto.userId, assignedAt: dto.userId ? new Date() : null, status: dto.userId ? 'ASSIGNED' : 'PENDING' })
        .where(and(inArray(tasks.id, dto.taskIds), inArray(tasks.status, ['PENDING', 'ASSIGNED', 'EXCEPTION']), warehouseScopeFilter(tasks.warehouseId)))
        .returning();
      await this.audit.log({ action: 'task.assign', entityType: 'task', after: dto }, tx);
      return { updated: rows.length };
    });
  }

  reprioritise(dto: z.infer<typeof ReprioritiseTasksSchema>) {
    return this.db.run(async (tx) => {
      const rows = await tx.update(tasks).set({ priority: dto.priority }).where(and(inArray(tasks.id, dto.taskIds), inArray(tasks.status, ['PENDING', 'ASSIGNED']), warehouseScopeFilter(tasks.warehouseId))).returning();
      return { updated: rows.length };
    });
  }

  cancel(dto: z.infer<typeof CancelTasksSchema>) {
    return this.db.run(async (tx) => {
      const rows = await tx.update(tasks).set({ status: 'CANCELLED', exceptionNotes: dto.reason }).where(and(inArray(tasks.id, dto.taskIds), inArray(tasks.status, ['PENDING', 'ASSIGNED', 'IN_PROGRESS', 'EXCEPTION']), warehouseScopeFilter(tasks.warehouseId))).returning();
      await this.audit.log({ action: 'task.cancel', entityType: 'task', after: dto }, tx);
      return { updated: rows.length };
    });
  }

  /** Ad-hoc move task created by a supervisor (e.g. consolidation, relocation). */
  createMoveTask(dto: z.infer<typeof CreateMoveTaskSchema>) {
    assertWarehouseAccess(dto.warehouseId);
    return this.db.run(async (tx) => {
      const lpn = dto.lpnNumber ? await this.lookup.lpnByNumber(tx, dto.lpnNumber) : null;
      if (dto.lpnNumber && !lpn) throw new NotFoundError('LPN', dto.lpnNumber);
      if (!lpn && !dto.skuId) throw new ValidationError('lpnNumber or skuId is required');
      const row = await this.create(tx, { warehouseId: dto.warehouseId, type: 'MOVE', priority: dto.priority, fromLocationId: dto.fromLocationId, toLocationId: dto.toLocationId, lpnId: lpn?.id ?? null, skuId: dto.skuId ?? null, qty: dto.qty ?? null, data: { notes: dto.notes } });
      return (await this.enrich(tx, [row]))[0];
    });
  }

  // ---------------- generic MOVE handler ----------------
  private async handleMove(tx: Tx, task: TaskRow, dto: ConfirmTaskDto): Promise<TaskConfirmResult> {
    const ctx = requireRequestContext();
    const toLoc = dto.toLocationCode ? await this.lookup.locationByCode(tx, task.warehouseId, dto.toLocationCode) : task.toLocationId ? await this.lookup.location(tx, task.toLocationId) : null;
    if (!toLoc) throw new ValidationError('Destination location required');
    if (task.lpnId) {
      const lpn = (await tx.select().from(lpns).where(eq(lpns.id, task.lpnId)).limit(1))[0];
      if (dto.lpnNumber && dto.lpnNumber !== lpn.lpnNumber) throw new DomainError(ErrorCodes.WRONG_LPN, `Expected LPN ${lpn.lpnNumber}`);
      await this.inventory.move({ warehouseId: task.warehouseId, mode: 'LPN', lpnNumber: lpn.lpnNumber, toLocationId: toLoc.id, idempotencyKey: dto.idempotencyKey, notes: `Task ${task.taskNumber}` }, tx);
    } else {
      if (!task.skuId || !task.fromLocationId) throw new ValidationError('Task lacks SKU or source');
      const sku = await this.lookup.sku(tx, task.skuId);
      const qty = dto.qty ? (await this.lookup.toBaseQty(tx, sku.id, dto.qty, dto.packLevel)).baseQty : D(task.qty ?? 0);
      await this.ledger.post(tx, { type: 'MOVE', qty, warehouseId: task.warehouseId, storerId: sku.storerId, skuId: sku.id, from: { locationId: task.fromLocationId, lpnId: null, status: 'AVAILABLE' }, to: { locationId: toLoc.id, lpnId: null, status: 'AVAILABLE' }, lotNumber: task.lotNumber ?? dto.lotNumber, expiryDate: task.expiryDate ?? dto.expiryDate ?? null, taskId: task.id, refType: 'TASK', refId: task.id, idempotencyKey: dto.idempotencyKey });
    }
    const [row] = await tx.update(tasks).set({ status: 'COMPLETED', completedAt: new Date(), completedById: ctx.userId, toLocationId: toLoc.id, qtyDone: task.qty ?? D(0) }).where(eq(tasks.id, task.id)).returning();
    return { task: row, completed: true };
  }

  // ---------------- helpers ----------------
  private async lockOwned(tx: Tx, id: string, userId: string): Promise<TaskRow> {
    const [t] = await tx.select().from(tasks).where(eq(tasks.id, id)).limit(1).for('update');
    if (!t) throw new NotFoundError('Task', id);
    if (t.assignedToId && t.assignedToId !== userId) {
      const ctx = requireRequestContext();
      if (!ctx.permissions.includes('*') && !ctx.permissions.includes('tasks.manage') && !ctx.permissions.includes('tasks.*')) {
        throw new DomainError(ErrorCodes.TASK_NOT_OWNED, 'Task is assigned to another user', 409);
      }
    }
    return t;
  }

  async enrich(tx: Tx, rows: TaskRow[]) {
    if (!rows.length) return [];
    const locIds = Array.from(new Set(rows.flatMap((r) => [r.fromLocationId, r.toLocationId]).filter(Boolean))) as string[];
    const skuIds = Array.from(new Set(rows.map((r) => r.skuId).filter(Boolean))) as string[];
    const lpnIds = Array.from(new Set(rows.flatMap((r) => [r.lpnId, r.toLpnId]).filter(Boolean))) as string[];
    const userIds = Array.from(new Set(rows.map((r) => r.assignedToId).filter(Boolean))) as string[];
    const zoneIds = Array.from(new Set(rows.map((r) => r.zoneId).filter(Boolean))) as string[];
    const [locs, skuRows, lpnRows, userRows, zoneRows] = await Promise.all([
      locIds.length ? tx.select({ id: locations.id, code: locations.code }).from(locations).where(inArray(locations.id, locIds)) : [],
      skuIds.length ? tx.select({ id: skus.id, code: skus.code, description: skus.description, baseUom: skus.baseUom, serialControlled: skus.serialControlled, lotControlled: skus.lotControlled, expiryControlled: skus.expiryControlled }).from(skus).where(inArray(skus.id, skuIds)) : [],
      lpnIds.length ? tx.select({ id: lpns.id, lpnNumber: lpns.lpnNumber }).from(lpns).where(inArray(lpns.id, lpnIds)) : [],
      userIds.length ? tx.select({ id: users.id, name: users.name }).from(users).where(inArray(users.id, userIds)) : [],
      zoneIds.length ? tx.select({ id: zones.id, code: zones.code }).from(zones).where(inArray(zones.id, zoneIds)) : [],
    ]);
    const lm = new Map(locs.map((l) => [l.id, l.code]));
    const sm = new Map(skuRows.map((s) => [s.id, s]));
    const pm = new Map(lpnRows.map((l) => [l.id, l.lpnNumber]));
    const um = new Map(userRows.map((u) => [u.id, u.name]));
    const zm = new Map(zoneRows.map((z) => [z.id, z.code]));
    return rows.map((r) =>
      serialize({
        ...r,
        fromLocationCode: r.fromLocationId ? lm.get(r.fromLocationId) ?? null : null,
        toLocationCode: r.toLocationId ? lm.get(r.toLocationId) ?? null : null,
        sku: r.skuId ? sm.get(r.skuId) ?? null : null,
        lpnNumber: r.lpnId ? pm.get(r.lpnId) ?? null : null,
        toLpnNumber: r.toLpnId ? pm.get(r.toLpnId) ?? null : null,
        assignedToName: r.assignedToId ? um.get(r.assignedToId) ?? null : null,
        zoneCode: r.zoneId ? zm.get(r.zoneId) ?? null : null,
      }),
    );
  }
}
