import { Injectable } from '@nestjs/common';
import { and, eq, ilike, or, sql } from 'drizzle-orm';
import { z } from 'zod';
import { CreateStorerDto, PaginationQueryDto, UpdateStorerSchema, CreateSupplierSchema, UpdateSupplierSchema, CreateCustomerSchema, UpdateCustomerSchema, CreateCarrierSchema, UpdateCarrierSchema } from '@wms/shared';
import { DbService, Tx } from '@/common/db/db.service';
import { storers, storerWarehouses, suppliers, customers, carriers } from '@/db/schema';
import { AuditService } from '@/common/audit/audit.service';
import { requireRequestContext } from '@/common/context/request-context';
import { NotFoundError } from '@/common/errors/domain-error';
import { paginate, offsetOf, resolveStorerScope } from '@/common/util';
import { PlatformService } from '@/modules/platform/platform.service';

/** Storers (3PL clients), suppliers, customers (consignees) and carriers. */
@Injectable()
export class PartnersService {
  constructor(
    private readonly db: DbService,
    private readonly audit: AuditService,
    private readonly platform: PlatformService,
  ) {}

  // ---------- storers ----------
  listStorers(q: PaginationQueryDto) {
    const scope = resolveStorerScope();
    return this.db.run(async (tx) => {
      const where = and(scope ? eq(storers.id, scope) : undefined, q.q ? or(ilike(storers.name, `%${q.q}%`), ilike(storers.code, `%${q.q}%`)) : undefined);
      const [{ count }] = await tx.select({ count: sql<number>`count(*)`.mapWith(Number) }).from(storers).where(where);
      const rows = await tx.select().from(storers).where(where).orderBy(storers.code).limit(q.pageSize).offset(offsetOf(q));
      return paginate(rows, count, q);
    });
  }

  getStorer(id: string) {
    resolveStorerScope(id);
    return this.db.run((tx) => this.getStorerInTx(tx, id));
  }

  async getStorerInTx(tx: Tx, id: string) {
    const [s] = await tx.select().from(storers).where(eq(storers.id, id)).limit(1);
    if (!s) throw new NotFoundError('Storer', id);
    const wh = await tx.select({ warehouseId: storerWarehouses.warehouseId }).from(storerWarehouses).where(eq(storerWarehouses.storerId, id));
    return { ...s, warehouseIds: wh.map((w) => w.warehouseId) };
  }

  createStorer(dto: CreateStorerDto) {
    const ctx = requireRequestContext();
    return this.db.run(async (tx) => {
      const [{ count }] = await tx.select({ count: sql<number>`count(*)`.mapWith(Number) }).from(storers).where(eq(storers.status, 'ACTIVE'));
      await this.platform.assertWithinLimit(ctx.tenantId!, 'maxStorers', count, tx);
      const { warehouseIds, ...rest } = dto;
      const [row] = await tx.insert(storers).values({ tenantId: ctx.tenantId!, ...rest }).returning();
      if (warehouseIds.length) await tx.insert(storerWarehouses).values(warehouseIds.map((warehouseId) => ({ storerId: row.id, warehouseId })));
      await this.audit.log({ action: 'storer.create', entityType: 'storer', entityId: row.id, after: dto }, tx);
      return this.getStorerInTx(tx, row.id);
    });
  }

  updateStorer(id: string, dto: z.infer<typeof UpdateStorerSchema>) {
    return this.db.run(async (tx) => {
      const [before] = await tx.select().from(storers).where(eq(storers.id, id)).limit(1);
      if (!before) throw new NotFoundError('Storer', id);
      const { warehouseIds, ...rest } = dto;
      const [row] = await tx.update(storers).set({ ...rest, settings: rest.settings ? { ...(before.settings as object), ...rest.settings } : undefined }).where(eq(storers.id, id)).returning();
      if (warehouseIds) {
        await tx.delete(storerWarehouses).where(eq(storerWarehouses.storerId, id));
        if (warehouseIds.length) await tx.insert(storerWarehouses).values(warehouseIds.map((warehouseId) => ({ storerId: id, warehouseId })));
      }
      await this.audit.log({ action: 'storer.update', entityType: 'storer', entityId: id, before, after: row }, tx);
      return this.getStorerInTx(tx, id);
    });
  }

  // ---------- suppliers ----------
  listSuppliers(q: PaginationQueryDto & { storerId?: string }) {
    const scope = resolveStorerScope(q.storerId);
    return this.db.run(async (tx) => {
      const where = and(scope ? or(eq(suppliers.storerId, scope), sql`${suppliers.storerId} is null`) : undefined, q.q ? or(ilike(suppliers.name, `%${q.q}%`), ilike(suppliers.code, `%${q.q}%`)) : undefined);
      const [{ count }] = await tx.select({ count: sql<number>`count(*)`.mapWith(Number) }).from(suppliers).where(where);
      const rows = await tx.select().from(suppliers).where(where).orderBy(suppliers.code).limit(q.pageSize).offset(offsetOf(q));
      return paginate(rows, count, q);
    });
  }

  createSupplier(dto: z.infer<typeof CreateSupplierSchema>) {
    const ctx = requireRequestContext();
    return this.db.run(async (tx) => {
      const [row] = await tx.insert(suppliers).values({ tenantId: ctx.tenantId!, ...dto, storerId: resolveStorerScope(dto.storerId) ?? null }).returning();
      await this.audit.log({ action: 'supplier.create', entityType: 'supplier', entityId: row.id, after: dto }, tx);
      return row;
    });
  }

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

  // ---------- customers ----------
  listCustomers(q: PaginationQueryDto & { storerId?: string }) {
    const scope = resolveStorerScope(q.storerId);
    return this.db.run(async (tx) => {
      const where = and(scope ? eq(customers.storerId, scope) : undefined, q.q ? or(ilike(customers.name, `%${q.q}%`), ilike(customers.code, `%${q.q}%`)) : undefined);
      const [{ count }] = await tx.select({ count: sql<number>`count(*)`.mapWith(Number) }).from(customers).where(where);
      const rows = await tx.select().from(customers).where(where).orderBy(customers.code).limit(q.pageSize).offset(offsetOf(q));
      return paginate(rows, count, q);
    });
  }

  createCustomer(dto: z.infer<typeof CreateCustomerSchema>) {
    const ctx = requireRequestContext();
    const storerId = resolveStorerScope(dto.storerId)!;
    return this.db.run(async (tx) => {
      const [row] = await tx.insert(customers).values({ tenantId: ctx.tenantId!, ...dto, storerId }).returning();
      await this.audit.log({ action: 'customer.create', entityType: 'customer', entityId: row.id, after: dto }, tx);
      return row;
    });
  }

  updateCustomer(id: string, dto: z.infer<typeof UpdateCustomerSchema>) {
    return this.db.run(async (tx) => {
      const [before] = await tx.select().from(customers).where(eq(customers.id, id)).limit(1);
      if (!before) throw new NotFoundError('Customer', id);
      resolveStorerScope(before.storerId);
      const [row] = await tx.update(customers).set(dto).where(eq(customers.id, id)).returning();
      await this.audit.log({ action: 'customer.update', entityType: 'customer', entityId: id, before, after: row }, tx);
      return row;
    });
  }

  // ---------- carriers ----------
  listCarriers() {
    return this.db.run((tx) => tx.select().from(carriers).orderBy(carriers.code));
  }

  createCarrier(dto: z.infer<typeof CreateCarrierSchema>) {
    const ctx = requireRequestContext();
    return this.db.run(async (tx) => {
      const [row] = await tx.insert(carriers).values({ tenantId: ctx.tenantId!, ...dto }).returning();
      await this.audit.log({ action: 'carrier.create', entityType: 'carrier', entityId: row.id, after: dto }, tx);
      return row;
    });
  }

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