import { Injectable } from '@nestjs/common';
import { and, eq, gte, lte, sql } from 'drizzle-orm';
import { z } from 'zod';
import { CreateDockAppointmentSchema, UpdateDockAppointmentSchema } from '@wms/shared';
import { DbService } from '@/common/db/db.service';
import { dockAppointments, locations, asns, loads } from '@/db/schema';
import { AuditService } from '@/common/audit/audit.service';
import { requireRequestContext } from '@/common/context/request-context';
import { DomainError, NotFoundError, ValidationError } from '@/common/errors/domain-error';
import { assertWarehouseAccess } from '@/common/util';

/** Dock scheduling: prevents double booking of a dock door. */
@Injectable()
export class DockService {
  constructor(
    private readonly db: DbService,
    private readonly audit: AuditService,
  ) {}

  list(warehouseId: string, from: string, to: string) {
    assertWarehouseAccess(warehouseId);
    return this.db.run((tx) =>
      tx
        .select({ appt: dockAppointments, dockCode: locations.code, asnNumber: asns.asnNumber, loadNumber: loads.loadNumber })
        .from(dockAppointments)
        .leftJoin(locations, eq(locations.id, dockAppointments.dockLocationId))
        .leftJoin(asns, and(eq(dockAppointments.refType, 'ASN'), eq(asns.id, dockAppointments.refId)))
        .leftJoin(loads, and(eq(dockAppointments.refType, 'LOAD'), eq(loads.id, dockAppointments.refId)))
        .where(and(eq(dockAppointments.warehouseId, warehouseId), gte(dockAppointments.startsAt, new Date(`${from}T00:00:00Z`)), lte(dockAppointments.startsAt, new Date(`${to}T23:59:59Z`))))
        .orderBy(dockAppointments.startsAt)
        .then((rows) => rows.map((r) => ({ ...r.appt, dockCode: r.dockCode, asnNumber: r.asnNumber, loadNumber: r.loadNumber }))),
    );
  }

  create(dto: z.infer<typeof CreateDockAppointmentSchema>) {
    assertWarehouseAccess(dto.warehouseId);
    const ctx = requireRequestContext();
    return this.db.run(async (tx) => {
      const startsAt = new Date(dto.startsAt);
      const endsAt = new Date(dto.endsAt);
      if (endsAt <= startsAt) throw new ValidationError('endsAt must be after startsAt');
      if (dto.dockLocationId) await this.assertFree(tx, dto.dockLocationId, startsAt, endsAt, null);
      const [row] = await tx.insert(dockAppointments).values({ tenantId: ctx.tenantId!, ...dto, startsAt, endsAt, createdById: ctx.userId }).returning();
      if (dto.refType === 'ASN' && dto.refId) await tx.update(asns).set({ status: sql`case when ${asns.status} = 'EXPECTED' then 'SCHEDULED'::asn_status else ${asns.status} end`, dockLocationId: dto.dockLocationId ?? null, expectedAt: startsAt }).where(eq(asns.id, dto.refId));
      await this.audit.log({ action: 'dock.book', entityType: 'dock_appointment', entityId: row.id, after: dto }, tx);
      return row;
    });
  }

  update(id: string, dto: z.infer<typeof UpdateDockAppointmentSchema>) {
    return this.db.run(async (tx) => {
      const [before] = await tx.select().from(dockAppointments).where(eq(dockAppointments.id, id)).limit(1);
      if (!before) throw new NotFoundError('Appointment', id);
      const startsAt = dto.startsAt ? new Date(dto.startsAt) : before.startsAt;
      const endsAt = dto.endsAt ? new Date(dto.endsAt) : before.endsAt;
      const dockId = dto.dockLocationId ?? before.dockLocationId;
      if (dockId && dto.status !== 'CANCELLED') await this.assertFree(tx, dockId, startsAt, endsAt, id);
      const [row] = await tx.update(dockAppointments).set({ ...dto, startsAt, endsAt }).where(eq(dockAppointments.id, id)).returning();
      await this.audit.log({ action: 'dock.update', entityType: 'dock_appointment', entityId: id, before, after: row }, tx);
      return row;
    });
  }

  private async assertFree(tx: Parameters<Parameters<DbService['run']>[0]>[0], dockId: string, startsAt: Date, endsAt: Date, excludeId: string | null) {
    const [clash] = await tx
      .select({ id: dockAppointments.id })
      .from(dockAppointments)
      .where(and(eq(dockAppointments.dockLocationId, dockId), sql`${dockAppointments.status} not in ('CANCELLED','COMPLETED','NO_SHOW')`, sql`${dockAppointments.startsAt} < ${endsAt.toISOString()}::timestamptz`, sql`${dockAppointments.endsAt} > ${startsAt.toISOString()}::timestamptz`, excludeId ? sql`${dockAppointments.id} <> ${excludeId}` : undefined))
      .limit(1);
    if (clash) throw new DomainError('DOCK_CLASH', 'The dock is already booked for that time window', 409, { appointmentId: clash.id });
  }
}
