import { Injectable } from '@nestjs/common';
import { and, desc, eq, isNull, lte, or, gte, sql } from 'drizzle-orm';
import { CreateContractDto, UpdateContractSchema, CreateTariffDto, UpdateTariffSchema } from '@wms/shared';
import { z } from 'zod';
import { DbService, Tx } from '@/common/db/db.service';
import { billingContracts, tariffs, storers } from '@/db/schema';
import { AuditService } from '@/common/audit/audit.service';
import { NumberingService } from '@/common/numbering/numbering.service';
import { requireRequestContext } from '@/common/context/request-context';
import { DomainError, NotFoundError, ValidationError, ErrorCodes } from '@/common/errors/domain-error';
import { D, resolveStorerScope, serialize, qcol } from '@/common/util';

export type ContractRow = typeof billingContracts.$inferSelect;
export type TariffRow = typeof tariffs.$inferSelect;

@Injectable()
export class ContractsService {
  constructor(
    private readonly db: DbService,
    private readonly audit: AuditService,
    private readonly numbering: NumberingService,
  ) {}

  list(storerId?: string) {
    const scope = resolveStorerScope(storerId);
    return this.db.run((tx) =>
      tx
        .select({ c: billingContracts, storerName: storers.name, storerCode: storers.code, tariffCount: sql<number>`(select count(*) from ${tariffs} t where t.contract_id = ${qcol(billingContracts.id)})`.mapWith(Number) })
        .from(billingContracts)
        .innerJoin(storers, eq(storers.id, billingContracts.storerId))
        .where(scope ? eq(billingContracts.storerId, scope) : undefined)
        .orderBy(desc(billingContracts.validFrom))
        .then((rows) => serialize(rows.map((r) => ({ ...r.c, storerName: r.storerName, storerCode: r.storerCode, tariffCount: r.tariffCount })))),
    );
  }

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

  async getInTx(tx: Tx, id: string) {
    const [c] = await tx.select().from(billingContracts).where(eq(billingContracts.id, id)).limit(1);
    if (!c) throw new NotFoundError('Contract', id);
    resolveStorerScope(c.storerId);
    const t = await tx.select().from(tariffs).where(eq(tariffs.contractId, id)).orderBy(tariffs.group, tariffs.sortOrder, tariffs.code);
    return serialize({ ...c, tariffs: t });
  }

  create(dto: CreateContractDto) {
    const ctx = requireRequestContext();
    return this.db.run(async (tx) => {
      const contractNumber = dto.contractNumber ?? (await this.numbering.next('CON', tx));
      const [c] = await tx
        .insert(billingContracts)
        .values({ tenantId: ctx.tenantId!, ...dto, contractNumber, minimumMonthly: dto.minimumMonthly ? D(dto.minimumMonthly) : null, fixedMonthlyFee: dto.fixedMonthlyFee ? D(dto.fixedMonthlyFee) : null, taxRatePct: D(dto.taxRatePct), storageCap: dto.storageCap ?? null, createdById: ctx.userId })
        .returning();
      await this.audit.log({ action: 'contract.create', entityType: 'billing_contract', entityId: c.id, after: dto }, tx);
      return this.getInTx(tx, c.id);
    });
  }

  update(id: string, dto: z.infer<typeof UpdateContractSchema>) {
    return this.db.run(async (tx) => {
      const [before] = await tx.select().from(billingContracts).where(eq(billingContracts.id, id)).limit(1);
      if (!before) throw new NotFoundError('Contract', id);
      if (dto.status === 'ACTIVE') {
        // only one active contract per storer at a time (overlapping validity)
        const [clash] = await tx.select({ id: billingContracts.id }).from(billingContracts).where(and(eq(billingContracts.storerId, before.storerId), eq(billingContracts.status, 'ACTIVE'), sql`${billingContracts.id} <> ${id}`, or(isNull(billingContracts.validTo), gte(billingContracts.validTo, dto.validFrom ?? before.validFrom)))).limit(1);
        if (clash) throw new DomainError(ErrorCodes.INVALID_STATE, 'Another active contract overlaps this validity period; expire it first');
      }
      const [row] = await tx
        .update(billingContracts)
        .set({ ...dto, minimumMonthly: dto.minimumMonthly !== undefined ? D(dto.minimumMonthly) : undefined, fixedMonthlyFee: dto.fixedMonthlyFee !== undefined ? D(dto.fixedMonthlyFee) : undefined, taxRatePct: dto.taxRatePct !== undefined ? D(dto.taxRatePct) : undefined, storageCap: dto.storageCap === undefined ? undefined : dto.storageCap })
        .where(eq(billingContracts.id, id))
        .returning();
      await this.audit.log({ action: 'contract.update', entityType: 'billing_contract', entityId: id, before, after: row }, tx);
      return this.getInTx(tx, id);
    });
  }

  addTariff(contractId: string, dto: CreateTariffDto) {
    const ctx = requireRequestContext();
    return this.db.run(async (tx) => {
      const [c] = await tx.select().from(billingContracts).where(eq(billingContracts.id, contractId)).limit(1);
      if (!c) throw new NotFoundError('Contract', contractId);
      this.validateTariff(dto);
      const [t] = await tx.insert(tariffs).values({ tenantId: ctx.tenantId!, contractId, ...dto, rate: D(dto.rate), minimumCharge: dto.minimumCharge ? D(dto.minimumCharge) : null, tiers: dto.tiers ?? null }).returning();
      await this.audit.log({ action: 'tariff.create', entityType: 'tariff', entityId: t.id, after: dto }, tx);
      return serialize(t);
    });
  }

  updateTariff(id: string, dto: z.infer<typeof UpdateTariffSchema>) {
    return this.db.run(async (tx) => {
      const [before] = await tx.select().from(tariffs).where(eq(tariffs.id, id)).limit(1);
      if (!before) throw new NotFoundError('Tariff', id);
      const [t] = await tx.update(tariffs).set({ ...dto, rate: dto.rate !== undefined ? D(dto.rate) : undefined, minimumCharge: dto.minimumCharge !== undefined ? D(dto.minimumCharge) : undefined, tiers: dto.tiers === undefined ? undefined : dto.tiers }).where(eq(tariffs.id, id)).returning();
      await this.audit.log({ action: 'tariff.update', entityType: 'tariff', entityId: id, before, after: t }, tx);
      return serialize(t);
    });
  }

  deleteTariff(id: string) {
    return this.db.run(async (tx) => {
      const [before] = await tx.update(tariffs).set({ active: false }).where(eq(tariffs.id, id)).returning();
      if (!before) throw new NotFoundError('Tariff', id);
      await this.audit.log({ action: 'tariff.deactivate', entityType: 'tariff', entityId: id, before }, tx);
      return { ok: true };
    });
  }

  /** Active contract covering a date for a storer (null when none). */
  async activeContract(tx: Tx, storerId: string, dateIso: string): Promise<ContractRow | null> {
    const [c] = await tx
      .select()
      .from(billingContracts)
      .where(and(eq(billingContracts.storerId, storerId), eq(billingContracts.status, 'ACTIVE'), lte(billingContracts.validFrom, dateIso), or(isNull(billingContracts.validTo), gte(billingContracts.validTo, dateIso))))
      .orderBy(desc(billingContracts.validFrom))
      .limit(1);
    return c ?? null;
  }

  async activeTariffs(tx: Tx, contractId: string, dateIso: string): Promise<TariffRow[]> {
    return tx
      .select()
      .from(tariffs)
      .where(and(eq(tariffs.contractId, contractId), eq(tariffs.active, true), or(isNull(tariffs.validFrom), lte(tariffs.validFrom, dateIso)), or(isNull(tariffs.validTo), gte(tariffs.validTo, dateIso))))
      .orderBy(tariffs.sortOrder);
  }

  private validateTariff(dto: CreateTariffDto) {
    const storageUnits = ['PER_CBM_WEEK', 'PER_CBM_DAY', 'PER_PALLET_WEEK', 'PER_PALLET_DAY', 'FLAT_MONTHLY'];
    if (dto.group === 'STORAGE' && !storageUnits.includes(dto.chargeUnit)) throw new ValidationError('STORAGE tariffs must use a storage unit (per CBM/pallet per week/day, or flat monthly)');
    if (dto.trigger === 'STORAGE_PERIOD' && dto.group !== 'STORAGE') throw new ValidationError('STORAGE_PERIOD trigger is only valid for STORAGE tariffs');
    if (dto.tiers?.length) {
      const sorted = [...dto.tiers].sort((a, b) => (a.upTo ?? Infinity) - (b.upTo ?? Infinity));
      if (sorted[sorted.length - 1].upTo !== null) throw new ValidationError('The last tier must have upTo = null (open ended)');
    }
  }
}
