import { PaginatedResponse } from '@edr/types';
import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common';
import { CreatePriorityConfigDto } from '../dto/create-priority-config.dto';
import { ListPriorityConfigsQueryDto } from '../dto/list-rule-engine-query.dto';
import { UpdatePriorityConfigDto } from '../dto/update-priority-config.dto';
import { PriorityConfig } from '../entities/priority-config.entity';
import {
  IPriorityConfigsRepository,
  PRIORITY_CONFIGS_REPOSITORY,
} from '../interfaces/priority-configs.repository.interface';
import { DisplayOrderService } from './display-order.service';

/**
 * Lowest wagon count ≥ 1 not covered by any of `rules` — where the next range
 * must start. The chain is unbounded above, so there is always a next start.
 */
function nextRangeStart(
  rules: Pick<PriorityConfig, 'type' | 'minWagonCount' | 'maxWagonCount'>[],
): number {
  const sorted = [...rules].sort((a, b) => a.minWagonCount - b.minWagonCount);
  let next = 1;
  for (const r of sorted) {
    if (r.minWagonCount > next) break; // gap before this rule — fill it
    next = Math.max(next, r.maxWagonCount + 1);
  }
  return next;
}

@Injectable()
export class PriorityConfigsService {
  constructor(
    @Inject(PRIORITY_CONFIGS_REPOSITORY)
    private readonly repository: IPriorityConfigsRepository,
    private readonly displayOrder: DisplayOrderService,
  ) {}

  /** List priority configs — standard paginated envelope with server-side search. */
  async findAll(query: ListPriorityConfigsQueryDto): Promise<PaginatedResponse<PriorityConfig>> {
    return this.repository.findPaged(query);
  }

  async findById(id: string): Promise<PriorityConfig> {
    const entity = await this.repository.findById(id);
    if (!entity) throw new NotFoundException(`Priority config ${id} not found`);
    return entity;
  }

  async create(dto: CreatePriorityConfigDto): Promise<PriorityConfig> {
    this.validateCurrencyField(dto.type, dto.currency);
    await this.assertNoRangeCollision({
      type: dto.type,
      currency: dto.currency ?? null,
      minWagonCount: dto.minWagonCount,
      maxWagonCount: dto.maxWagonCount,
    });

    const displayOrder = await this.displayOrder.resolveCreateOrder(PriorityConfig, 'displayOrder', {});

    return this.repository.create({
      type: dto.type,
      label: dto.label,
      currency: dto.currency ?? null,
      minWagonCount: dto.minWagonCount,
      maxWagonCount: dto.maxWagonCount,
      scorePoints: dto.scorePoints ?? 0,
      isActive: dto.isActive ?? false,
      displayOrder,
    });
  }

  async update(id: string, dto: UpdatePriorityConfigDto): Promise<PriorityConfig> {
    const existing = await this.findById(id);

    const type = dto.type ?? existing.type;
    const currency = dto.currency !== undefined ? dto.currency : existing.currency;
    this.validateCurrencyField(type, currency);
    await this.assertNoRangeCollision({
      type,
      currency: currency ?? null,
      minWagonCount: dto.minWagonCount ?? existing.minWagonCount,
      maxWagonCount: dto.maxWagonCount ?? existing.maxWagonCount,
      excludeId: id,
    });

    const { ...patch } = dto;
    const updated = await this.repository.update(id, patch);
    if (!updated) throw new NotFoundException(`Priority config ${id} not found`);
    return updated;
  }

  /**
   * Range rules per type (and, for CURRENCY rules, per currency):
   *  - ranges never overlap — a booking matches at most one rule per type;
   *  - ranges are contiguous from 1: a new range must START at the lowest
   *    wagon count not yet covered (after 1–5 the next is 6–…; deleting a
   *    middle rule opens a gap and the next create must fill it first).
   * There is no upper ceiling — max wagon count is unbounded.
   * Ranges are inclusive on both ends.
   */
  async assertNoRangeCollision(input: {
    type: 'WAGON' | 'CURRENCY' | 'CUSTOMS';
    currency?: string | null;
    minWagonCount: number;
    maxWagonCount: number;
    excludeId?: string;
  }): Promise<void> {
    if (input.minWagonCount > input.maxWagonCount) {
      throw new BadRequestException(
        'Min wagon count cannot be greater than max wagon count',
      );
    }
    const siblings = (
      await this.repository.findAll({ where: { type: input.type } })
    ).filter(
      (s) =>
        s.id !== input.excludeId &&
        (input.type !== 'CURRENCY' ||
          (s.currency ?? null) === (input.currency ?? null)),
    );

    const expectedStart = nextRangeStart(siblings);
    // An edited rule may always KEEP its current start (so a gap lower in the
    // chain never blocks editing an upper rule's points/max) — or move down to
    // fill that lowest gap.
    const currentStart = input.excludeId
      ? (await this.repository.findById(input.excludeId))?.minWagonCount ?? null
      : null;
    if (
      input.minWagonCount !== expectedStart &&
      input.minWagonCount !== currentStart
    ) {
      throw new BadRequestException(
        `The next ${input.type} range must start at ${expectedStart} ` +
          `(ranges are contiguous — no gaps, no overlaps). ` +
          `You entered ${input.minWagonCount}–${input.maxWagonCount}.`,
      );
    }

    const clash = siblings.find(
      (s) =>
        input.minWagonCount <= s.maxWagonCount &&
        input.maxWagonCount >= s.minWagonCount,
    );
    if (clash) {
      throw new BadRequestException(
        `Wagon range ${input.minWagonCount}–${input.maxWagonCount} overlaps existing rule ` +
          `"${clash.label}" (${clash.minWagonCount}–${clash.maxWagonCount}). ` +
          'Adjust the range so rules do not collide.',
      );
    }
  }

  /**
   * Where the next range for a type/currency must start — feeds the create
   * form so the min field is auto-filled and locked. Always a number: the
   * chain has no ceiling, so another range always fits.
   */
  async nextRange(
    type: 'WAGON' | 'CURRENCY' | 'CUSTOMS',
    currency?: string | null,
  ): Promise<{ nextMin: number }> {
    const siblings = (
      await this.repository.findAll({ where: { type } })
    ).filter(
      (s) =>
        type !== 'CURRENCY' || (s.currency ?? null) === (currency ?? null),
    );
    return { nextMin: nextRangeStart(siblings) };
  }

  async remove(id: string): Promise<void> {
    await this.findById(id);
    await this.repository.softDelete(id);
  }

  async reorder(ids: string[]): Promise<void> {
    await this.displayOrder.reorderByIds(PriorityConfig, 'displayOrder', ids);
  }

  async moveOrder(id: string, direction: 'up' | 'down'): Promise<void> {
    await this.findById(id);
    await this.displayOrder.moveOne(PriorityConfig, 'displayOrder', id, direction);
  }

  private validateCurrencyField(
    type: 'WAGON' | 'CURRENCY' | 'CUSTOMS',
    currency: string | undefined | null,
  ): void {
    if (type === 'CURRENCY' && !currency) {
      throw new BadRequestException('currency field is required when type is CURRENCY');
    }
    if (type !== 'CURRENCY' && currency) {
      throw new BadRequestException(`currency field must be null when type is ${type}`);
    }
  }
}
