import { Injectable } from '@nestjs/common';
import { and, eq, ilike, or, sql, asc } from 'drizzle-orm';
import { z } from 'zod';
import { CreateWarehouseDto, CreateZoneDto, CreateLocationDto, GenerateLocationsDto, PaginationQueryDto, UpdateWarehouseSchema, UpdateZoneSchema, UpdateLocationSchema } from '@wms/shared';
import { DbService } from '@/common/db/db.service';
import { warehouses, zones, locations, inventoryBalances } from '@/db/schema';
import { AuditService } from '@/common/audit/audit.service';
import { requireRequestContext } from '@/common/context/request-context';
import { NotFoundError, DomainError } from '@/common/errors/domain-error';
import { D, paginate, offsetOf, assertWarehouseAccess, serialize, qcol } from '@/common/util';
import { TenantProvisioningService } from '@/modules/platform/tenant-provisioning.service';
import { PlatformService } from '@/modules/platform/platform.service';

@Injectable()
export class WarehousesService {
  constructor(
    private readonly db: DbService,
    private readonly audit: AuditService,
    private readonly provisioning: TenantProvisioningService,
    private readonly platform: PlatformService,
  ) {}

  // ---------- warehouses ----------
  list() {
    const ctx = requireRequestContext();
    return this.db.run(async (tx) => {
      const rows = await tx.select().from(warehouses).orderBy(warehouses.code);
      return ctx.warehouseIds === 'ALL' ? rows : rows.filter((w) => (ctx.warehouseIds as string[]).includes(w.id));
    });
  }

  get(id: string) {
    assertWarehouseAccess(id);
    return this.db.run(async (tx) => {
      const [w] = await tx.select().from(warehouses).where(eq(warehouses.id, id)).limit(1);
      if (!w) throw new NotFoundError('Warehouse', id);
      const z = await tx.select().from(zones).where(eq(zones.warehouseId, id)).orderBy(zones.pickSequence);
      return { ...w, zones: z };
    });
  }

  create(dto: CreateWarehouseDto) {
    const ctx = requireRequestContext();
    return this.db.run(async (tx) => {
      const [{ count }] = await tx.select({ count: sql<number>`count(*)`.mapWith(Number) }).from(warehouses);
      await this.platform.assertWithinLimit(ctx.tenantId!, 'maxWarehouses', count, tx);
      const w = await this.provisioning.createWarehouse(ctx.tenantId!, dto.code, dto.name, dto.timezone, tx);
      const [row] = await tx.update(warehouses).set({ address: dto.address, settings: { ...(w.settings as object), ...dto.settings } }).where(eq(warehouses.id, w.id)).returning();
      await this.audit.log({ action: 'warehouse.create', entityType: 'warehouse', entityId: row.id, after: dto }, tx);
      return row;
    });
  }

  update(id: string, dto: z.infer<typeof UpdateWarehouseSchema>) {
    assertWarehouseAccess(id);
    return this.db.run(async (tx) => {
      const [before] = await tx.select().from(warehouses).where(eq(warehouses.id, id)).limit(1);
      if (!before) throw new NotFoundError('Warehouse', id);
      const [row] = await tx
        .update(warehouses)
        .set({ ...dto, settings: dto.settings ? { ...(before.settings as object), ...dto.settings } : undefined })
        .where(eq(warehouses.id, id))
        .returning();
      await this.audit.log({ action: 'warehouse.update', entityType: 'warehouse', entityId: id, before, after: row }, tx);
      return row;
    });
  }

  // ---------- zones ----------
  listZones(warehouseId: string) {
    assertWarehouseAccess(warehouseId);
    return this.db.run((tx) => tx.select().from(zones).where(eq(zones.warehouseId, warehouseId)).orderBy(zones.pickSequence, zones.code));
  }

  createZone(dto: CreateZoneDto) {
    assertWarehouseAccess(dto.warehouseId);
    const ctx = requireRequestContext();
    return this.db.run(async (tx) => {
      const [row] = await tx.insert(zones).values({ tenantId: ctx.tenantId!, ...dto }).returning();
      await this.audit.log({ action: 'zone.create', entityType: 'zone', entityId: row.id, after: dto }, tx);
      return row;
    });
  }

  updateZone(id: string, dto: z.infer<typeof UpdateZoneSchema>) {
    return this.db.run(async (tx) => {
      const [before] = await tx.select().from(zones).where(eq(zones.id, id)).limit(1);
      if (!before) throw new NotFoundError('Zone', id);
      assertWarehouseAccess(before.warehouseId);
      const [row] = await tx.update(zones).set(dto).where(eq(zones.id, id)).returning();
      await this.audit.log({ action: 'zone.update', entityType: 'zone', entityId: id, before, after: row }, tx);
      return row;
    });
  }

  // ---------- locations ----------
  listLocations(warehouseId: string, q: PaginationQueryDto & { zoneId?: string; usage?: string; status?: string; empty?: boolean }) {
    assertWarehouseAccess(warehouseId);
    return this.db.run(async (tx) => {
      const where = and(
        eq(locations.warehouseId, warehouseId),
        q.zoneId ? eq(locations.zoneId, q.zoneId) : undefined,
        q.usage ? eq(locations.usage, q.usage as never) : undefined,
        q.status ? eq(locations.status, q.status as never) : undefined,
        q.q ? or(ilike(locations.code, `%${q.q}%`), ilike(locations.barcode, `%${q.q}%`)) : undefined,
      );
      const [{ count }] = await tx.select({ count: sql<number>`count(*)`.mapWith(Number) }).from(locations).where(where);
      const rows = await tx
        .select({
          loc: locations,
          zoneCode: zones.code,
          occupancyQty: sql<string>`coalesce((select sum(b.qty_on_hand) from ${inventoryBalances} b where b.location_id = ${qcol(locations.id)}), 0)`,
          lpnCount: sql<number>`(select count(distinct b.lpn_id) from ${inventoryBalances} b where b.location_id = ${qcol(locations.id)} and b.qty_on_hand > 0)`.mapWith(Number),
        })
        .from(locations)
        .innerJoin(zones, eq(zones.id, locations.zoneId))
        .where(where)
        .orderBy(asc(locations.pickSequence), asc(locations.code))
        .limit(q.pageSize)
        .offset(offsetOf(q));
      return paginate(rows.map((r) => ({ ...r.loc, zoneCode: r.zoneCode, occupancyQty: r.occupancyQty, lpnCount: r.lpnCount })), count, q);
    });
  }

  createLocation(dto: CreateLocationDto) {
    assertWarehouseAccess(dto.warehouseId);
    const ctx = requireRequestContext();
    return this.db.run(async (tx) => {
      const [row] = await tx
        .insert(locations)
        .values({
          tenantId: ctx.tenantId!,
          ...dto,
          barcode: dto.barcode ?? dto.code,
          lengthCm: dto.lengthCm ? D(dto.lengthCm) : null,
          widthCm: dto.widthCm ? D(dto.widthCm) : null,
          heightCm: dto.heightCm ? D(dto.heightCm) : null,
          cubicCapacityM3: dto.cubicCapacityM3 ? D(dto.cubicCapacityM3) : this.deriveCube(dto),
          weightCapacityKg: dto.weightCapacityKg ? D(dto.weightCapacityKg) : null,
        })
        .returning();
      await this.audit.log({ action: 'location.create', entityType: 'location', entityId: row.id, after: dto }, tx);
      return serialize(row);
    });
  }

  updateLocation(id: string, dto: z.infer<typeof UpdateLocationSchema>) {
    return this.db.run(async (tx) => {
      const [before] = await tx.select().from(locations).where(eq(locations.id, id)).limit(1);
      if (!before) throw new NotFoundError('Location', id);
      assertWarehouseAccess(before.warehouseId);
      if (dto.status === 'INACTIVE') {
        const [{ qty }] = await tx.select({ qty: sql<string>`coalesce(sum(qty_on_hand),0)` }).from(inventoryBalances).where(eq(inventoryBalances.locationId, id));
        if (D(qty).gt(0)) throw new DomainError('INVALID_STATE', 'Location holds stock; move it before deactivating');
      }
      const [row] = await tx
        .update(locations)
        .set({
          ...dto,
          lengthCm: dto.lengthCm !== undefined ? D(dto.lengthCm) : undefined,
          widthCm: dto.widthCm !== undefined ? D(dto.widthCm) : undefined,
          heightCm: dto.heightCm !== undefined ? D(dto.heightCm) : undefined,
          cubicCapacityM3: dto.cubicCapacityM3 !== undefined ? D(dto.cubicCapacityM3) : undefined,
          weightCapacityKg: dto.weightCapacityKg !== undefined ? D(dto.weightCapacityKg) : undefined,
        })
        .where(eq(locations.id, id))
        .returning();
      await this.audit.log({ action: 'location.update', entityType: 'location', entityId: id, before, after: row }, tx);
      return serialize(row);
    });
  }

  /** Bulk create a rack grid. Skips codes that already exist. */
  generateLocations(dto: GenerateLocationsDto) {
    assertWarehouseAccess(dto.warehouseId);
    const ctx = requireRequestContext();
    return this.db.run(async (tx) => {
      const existing = new Set((await tx.select({ code: locations.code }).from(locations).where(eq(locations.warehouseId, dto.warehouseId))).map((r) => r.code));
      const values: (typeof locations.$inferInsert)[] = [];
      let seq = 0;
      for (const aisle of dto.aisles) {
        for (let bay = dto.baysFrom; bay <= dto.baysTo; bay++) {
          const bayStr = String(bay).padStart(dto.padBays, '0');
          for (const [levelIdx, level] of dto.levels.entries()) {
            for (const position of dto.positions) {
              seq += 10;
              const code = dto.pattern.replace('{aisle}', aisle).replace('{bay}', bayStr).replace('{level}', level).replace('{position}', position).replace(/-+$/, '');
              if (existing.has(code)) continue;
              const isGround = levelIdx === 0 && dto.groundLevelIsPick;
              values.push({
                tenantId: ctx.tenantId!,
                warehouseId: dto.warehouseId,
                zoneId: dto.zoneId,
                code,
                barcode: code,
                aisle,
                bay: bayStr,
                level,
                position: position || null,
                type: dto.type,
                usage: isGround ? 'PICK' : dto.usage,
                cubicCapacityM3: dto.cubicCapacityM3 ? D(dto.cubicCapacityM3) : null,
                weightCapacityKg: dto.weightCapacityKg ? D(dto.weightCapacityKg) : null,
                maxLpns: dto.maxLpns,
                pickSequence: seq,
                putawaySequence: seq,
              });
            }
          }
        }
      }
      let created = 0;
      for (let i = 0; i < values.length; i += 500) {
        const rows = await tx.insert(locations).values(values.slice(i, i + 500)).returning({ id: locations.id });
        created += rows.length;
      }
      await this.audit.log({ action: 'location.generate', entityType: 'warehouse', entityId: dto.warehouseId, after: { ...dto, created } }, tx);
      return { created, skipped: existing.size ? values.length - created : 0, requested: values.length };
    });
  }

  private deriveCube(dto: { lengthCm?: string; widthCm?: string; heightCm?: string }) {
    if (dto.lengthCm && dto.widthCm && dto.heightCm) return D(dto.lengthCm).mul(dto.widthCm).mul(dto.heightCm).div(1_000_000);
    return null;
  }
}
