import { Injectable, NotFoundException, ConflictException, BadRequestException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { FindOptionsOrder, FindOptionsWhere, ILike, Not, Repository } from 'typeorm';
import { CreateCargoDto } from './dto/create-cargo.dto';
import { UpdateCargoDto } from './dto/update-cargo.dto';
import { LoadCargoDto } from './dto/load-cargo.dto';
import { DeliverCargoDto } from './dto/deliver-cargo.dto';
import { Cargo } from './entities/cargoes.entity';
import { Container } from '../container-management/entities/container.entity';
import { CargoType } from '../rule-engine/entities/cargo-type.entity';

@Injectable()
export class CargoesService {
  constructor(
    @InjectRepository(Cargo)
    private readonly cargoRepo: Repository<Cargo>,
    @InjectRepository(Container)
    private readonly containerRepo: Repository<Container>,
    @InjectRepository(CargoType)
    private readonly cargoTypeRepo: Repository<CargoType>,
  ) {}

  async create(dto: CreateCargoDto): Promise<Cargo> {
    const existing = await this.cargoRepo.findOne({
      where: { cargoReference: dto.cargoReference },
    });
    if (existing) {
      throw new ConflictException(`Cargo reference "${dto.cargoReference}" already exists`);
    }

    const container = await this.containerRepo.findOne({ where: { id: dto.containerId } });
    if (!container) {
      throw new NotFoundException(`Container ${dto.containerId} not found`);
    }
    await this.assertContainerCapacity(container, dto.weight);

    if (dto.cargoTypeId) {
      const cargoType = await this.cargoTypeRepo.findOne({
        where: { id: dto.cargoTypeId, isActive: true },
      });
      if (!cargoType) throw new NotFoundException(`Cargo type ${dto.cargoTypeId} not found`);
    }

    const cargo = this.cargoRepo.create(dto);
    return this.cargoRepo.save(cargo);
  }

  async findAll(query: Record<string, string | undefined> = {}): Promise<Cargo[]> {
    const where: FindOptionsWhere<Cargo>[] | FindOptionsWhere<Cargo> = [];
    const search = query.search?.trim();
    const status = query.status?.trim();
    const containerId = query.containerId?.trim();

    if (search) {
      where.push({
        cargoReference: ILike(`%${search}%`),
        ...(status ? { status } : {}),
        ...(containerId ? { containerId } : {}),
      });
      where.push({
        description: ILike(`%${search}%`),
        ...(status ? { status } : {}),
        ...(containerId ? { containerId } : {}),
      });
    }

    const sortBy = ['cargoReference', 'quantity', 'weight', 'volume', 'status'].includes(query.sortBy ?? '')
      ? (query.sortBy as keyof Cargo)
      : 'cargoReference';
    const sortOrder = query.sortOrder?.toUpperCase() === 'DESC' ? 'DESC' : 'ASC';

    return this.cargoRepo.find({
      where: search ? where : { ...(status ? { status } : {}), ...(containerId ? { containerId } : {}) },
      order: { [sortBy]: sortOrder } as FindOptionsOrder<Cargo>,
      skip: query.page && query.limit ? (Number(query.page) - 1) * Number(query.limit) : undefined,
      take: query.limit ? Number(query.limit) : undefined,
    });
  }

  async findById(id: string): Promise<Cargo> {
    const cargo = await this.cargoRepo.findOne({ where: { id } });
    if (!cargo) throw new NotFoundException(`Cargo ${id} not found`);
    return cargo;
  }

  async update(id: string, dto: UpdateCargoDto): Promise<Cargo> {
    const cargo = await this.findById(id);
    if (dto.cargoReference && dto.cargoReference !== cargo.cargoReference) {
      const existing = await this.cargoRepo.findOne({
        where: { cargoReference: dto.cargoReference },
      });
      if (existing) {
        throw new ConflictException(`Cargo reference "${dto.cargoReference}" already exists`);
      }
    }
    if (dto.containerId) {
      const container = await this.containerRepo.findOne({ where: { id: dto.containerId } });
      if (!container) throw new NotFoundException(`Container ${dto.containerId} not found`);
    }
    if (dto.cargoTypeId) {
      const cargoType = await this.cargoTypeRepo.findOne({
        where: { id: dto.cargoTypeId, isActive: true },
      });
      if (!cargoType) throw new NotFoundException(`Cargo type ${dto.cargoTypeId} not found`);
    }
    Object.assign(cargo, dto);
    return this.cargoRepo.save(cargo);
  }

  async remove(id: string): Promise<void> {
    const cargo = await this.findById(id);
    await this.cargoRepo.remove(cargo);
  }

  async loadCargo(id: string, dto: LoadCargoDto): Promise<Cargo> {
    const cargo = await this.cargoRepo.findOne({
      where: { id },
      relations: { container: true }, // ✅ fixed
    });
    if (!cargo) throw new NotFoundException('Cargo not found');
    if (cargo.status !== 'PENDING') {
      throw new ConflictException('Cargo already loaded or delivered');
    }

    if (cargo.container) {
      await this.assertContainerCapacity(cargo.container, dto.weight, cargo.id);
    }

    cargo.status = 'LOADED';
    cargo.loadedAt = new Date();
    cargo.quantity = dto.quantity;
    cargo.weight = dto.weight;
    cargo.volume = dto.volume ?? null;
    if (dto.description) cargo.description = dto.description;

    if (cargo.container) {
      cargo.container.status = 'LOADED';
      await this.containerRepo.save(cargo.container);
    }

    return this.cargoRepo.save(cargo);
  }

  async unloadCargo(id: string): Promise<Cargo> {
    const cargo = await this.cargoRepo.findOne({
      where: { id },
      relations: { container: true },
    });
    if (!cargo) throw new NotFoundException('Cargo not found');
    if (cargo.status !== 'LOADED') {
      throw new ConflictException('Cargo is not loaded');
    }
    cargo.status = 'UNLOADED';
    cargo.unloadedAt = new Date();
    const saved = await this.cargoRepo.save(cargo);

    // loadCargo flips the container to LOADED; on unload, free it back to
    // AVAILABLE once no other LOADED cargo still references the container.
    if (cargo.containerId != null && cargo.container) {
      const remaining = await this.cargoRepo.count({
        where: { containerId: cargo.containerId, status: 'LOADED', id: Not(cargo.id) },
      });
      if (remaining === 0) {
        cargo.container.status = 'AVAILABLE';
        await this.containerRepo.save(cargo.container);
      }
    }

    return saved;
  }

  async deliverCargo(id: string, dto?: DeliverCargoDto): Promise<Cargo> {
    const cargo = await this.cargoRepo.findOne({
      where: { id },
      relations: { container: true }, // ✅ fixed
    });
    if (!cargo) throw new NotFoundException('Cargo not found');
    if (cargo.status !== 'LOADED') {
      throw new ConflictException('Only loaded cargo can be delivered');
    }

    cargo.status = 'DELIVERED';
    cargo.deliveredAt = dto?.pickupDate ? new Date(dto.pickupDate) : new Date();
    if (dto?.receiverName) cargo.receiverName = dto.receiverName;
    if (dto?.deliveryRemarks) cargo.deliveryRemarks = dto.deliveryRemarks;

    // Exclude the cargo being delivered — it is still LOADED in the DB until the
    // save below, so counting it would keep `remaining` > 0 and never free the
    // container.
    const remaining =
      cargo.containerId != null
        ? await this.cargoRepo.count({
            where: { containerId: cargo.containerId, status: 'LOADED', id: Not(cargo.id) },
          })
        : 0;
    if (remaining === 0 && cargo.container) {
      cargo.container.status = 'AVAILABLE';
      await this.containerRepo.save(cargo.container);
    }

    return this.cargoRepo.save(cargo);
  }

  /**
   * Reject when placing `newWeightKg` on the container would exceed its max gross
   * weight. All values are kilograms: cargoes.weight is kg (entity), and the
   * container's tare_weight / max_gross_weight are kg (entity). Capacity check is
   * tare + already-LOADED cargo + new cargo <= max gross weight.
   */
  private async assertContainerCapacity(
    container: Container,
    newWeightKg: number,
    excludeCargoId?: string,
  ): Promise<void> {
    const qb = this.cargoRepo
      .createQueryBuilder('c')
      .select('COALESCE(SUM(c.weight), 0)', 'sum')
      .where('c.containerId = :containerId', { containerId: container.id })
      .andWhere('c.status = :status', { status: 'LOADED' });
    if (excludeCargoId) qb.andWhere('c.id != :excludeCargoId', { excludeCargoId });
    const raw = await qb.getRawOne<{ sum: string }>();

    const loadedKg = Number(raw?.sum ?? 0);
    const tareKg = Number(container.tareWeight);
    const maxGrossKg = Number(container.maxGrossWeight);
    if (tareKg + loadedKg + newWeightKg > maxGrossKg) {
      throw new BadRequestException(
        `Cargo weight exceeds container capacity: tare ${tareKg}kg + loaded ${loadedKg}kg + ` +
          `new ${newWeightKg}kg > max gross ${maxGrossKg}kg`,
      );
    }
  }
}
