import {
  Injectable,
  ConflictException,
  NotFoundException,
  ForbiddenException,
} from '@nestjs/common';
import { VehicleTypeRepository } from './repositories/vehicle-type.repository';
import { VehicleTypeMediaRepository } from './repositories/vehicle-type-media.repository';
import { MediaService } from '../../common/media/media.service';
import { CreateVehicleTypeDto, UpdateVehicleTypeDto } from './dto';
import { serviceMessages } from '../../common/constants/messages';

const MSG = serviceMessages('Vehicle type');

@Injectable()
export class VehicleTypeService {
  constructor(
    private readonly repo: VehicleTypeRepository,
    private readonly mediaRepo: VehicleTypeMediaRepository,
    private readonly media: MediaService,
  ) {}

  async findAll(query?: { page?: number; limit?: number; search?: string }) {
    return this.repo.findPaginated({
      page: query?.page,
      limit: query?.limit,
      search: query?.search,
      searchColumns: ['name'],
      order: { name: 'ASC' },
    });
  }

  async findAllActive() {
    return this.repo.findAllActive();
  }

  async findAllForLookup() {
    return this.repo.find({
      where: { is_active: true },
      // Capacity + operational_cap surfaced so booking forms and the
      // movement chart can render the inherited values without a
      // second round-trip per vehicle.
      select: {
        id: true,
        name: true,
        seating_capacity: true,
        operational_cap: true,
      },
      order: { name: 'ASC' },
    });
  }

  async findById(id: string) {
    const item = await this.repo.findById(id);
    if (!item) throw new NotFoundException(MSG.NOT_FOUND);
    return { ...item, media: await this.media.list(this.mediaRepo, id) };
  }

  async create(dto: CreateVehicleTypeDto) {
    const existing = await this.repo.findByName(dto.name);
    if (existing) throw new ConflictException(MSG.ALREADY_EXISTS('name'));

    // Operational cap can never exceed seating capacity — enforce here
    // so the master record stays internally consistent. A user typing
    // op_cap > seating gets a 409 instead of a silent over-allocation
    // in the planner.
    if (
      dto.seating_capacity != null &&
      dto.operational_cap != null &&
      dto.operational_cap > dto.seating_capacity
    ) {
      throw new ConflictException('Operational cap cannot exceed seating capacity');
    }
    const media = await this.media.prepare(this.mediaRepo, 'vehicle-types', dto.media);

    const item = await this.repo.save({
      name: dto.name,
      seating_capacity: dto.seating_capacity ?? null,
      operational_cap: dto.operational_cap ?? null,
      is_active: dto.is_active ?? true,
      is_system: false,
    });
    await this.media.sync(this.mediaRepo, item.id, media);

    // Backfill transport_pricing rows for this new type across every
    // existing transport service in the tenant. Mirrors the inverse
    // path (TransportService.create seeds pricing for every type) so
    // the pricing matrix stays full no matter which side ops adds
    // first. Best-effort — failures don't block the type create.
    try {
      if (item.is_active !== false) {
        await this.backfillPricingForType(item.id);
      }
    } catch {
      /* Don't fail the type create if pricing backfill hiccups. */
    }

    return { id: item.id, message: MSG.CREATED };
  }

  async update(id: string, dto: UpdateVehicleTypeDto) {
    const item = await this.repo.findById(id);
    if (!item) throw new NotFoundException(MSG.NOT_FOUND);

    if (dto.name && dto.name !== item.name) {
      const existing = await this.repo.findByName(dto.name);
      if (existing && existing.id !== id) throw new ConflictException(MSG.ALREADY_EXISTS('name'));
    }

    // Resolve effective values for the consistency check — caller
    // may have updated only one of the two fields.
    const nextSeating = dto.seating_capacity ?? item.seating_capacity;
    const nextCap = dto.operational_cap ?? item.operational_cap;
    if (nextSeating != null && nextCap != null && nextCap > nextSeating) {
      throw new ConflictException('Operational cap cannot exceed seating capacity');
    }
    const media = await this.media.prepare(this.mediaRepo, 'vehicle-types', dto.media, id);

    const updateData: Record<string, unknown> = {};
    const dtoRecord = dto as Record<string, unknown>;
    for (const field of ['name', 'seating_capacity', 'operational_cap', 'is_active'] as const) {
      if (dtoRecord[field] !== undefined) updateData[field] = dtoRecord[field];
    }

    await this.repo.update(id, updateData);
    await this.media.sync(this.mediaRepo, id, media);

    // Mirror activity update sync — keep the transport_pricing matrix
    // complete when the type changes shape:
    //   - name change: rename existing rows + backfill any missing
    //   - is_active flips false→true: backfill rows that were skipped
    //     while the type was inactive
    try {
      const renamed = updateData.name && updateData.name !== item.name;
      const reactivated = updateData.is_active === true && item.is_active === false;

      // Rename no longer needs to update transport_pricing rows —
      // pricing references vehicle_type_id (FK), so a name change in
      // vehicle_types propagates automatically via the join. Pre-FK
      // refactor we had to UPDATE the denormalised cab_type column
      // here; gone now.

      const effectiveActive = updateData.is_active ?? item.is_active;
      if ((renamed || reactivated) && effectiveActive !== false) {
        await this.backfillPricingForType(id);
      }
    } catch {
      /* don't fail update if pricing sync hiccups */
    }

    return { id, message: MSG.UPDATED };
  }

  async remove(id: string) {
    const item = await this.repo.findById(id);
    if (!item) throw new NotFoundException(MSG.NOT_FOUND);
    // UAT 2026-06-30 — stakeholder asked to allow system entries to be
    // deleted (soft delete, so still recoverable). The is_system flag
    // is now purely a labelling hint on the FE badge.
    await this.repo.softDelete(id);
    return { message: MSG.DELETED };
  }

  /**
   * Idempotent backfill — for the given vehicle_type_id, ensure a
   * transport_pricing row exists for every active transport service
   * in the tenant. Mirrors ActivityService's update-time sync; relies
   * on (tenant_id, transport_service_id, vehicle_type_id, currency_id)
   * unique constraint + ON CONFLICT DO NOTHING for safe re-runs.
   */
  private async backfillPricingForType(vehicleTypeId: string) {
    // Cross-service raw bulk insert — uses TenantAwareRepository's
    // public accessors for the current tenant context + manager.
    const tenantId = this.repo.currentTenantId();
    const userId = this.repo.currentUserId();
    const manager = this.repo.manager;

    const services = await manager.query(
      `SELECT ts.id, ts.destination_id, ts.category, d.currency_id
       FROM transport_services ts
       JOIN destinations d ON d.id = ts.destination_id
       WHERE ts.tenant_id = $1 AND ts.is_deleted = false AND ts.is_active = true`,
      [tenantId],
    );

    // PVT only — SIC pricing has no vehicle-type axis
    // (vehicle_type_id is NULL on SIC rows), so a new vehicle type
    // doesn't fan out to SIC. SIC rows are seeded once per route in
    // the transport-service create path.
    for (const svc of services) {
      if (!svc.currency_id) continue;
      await manager
        .createQueryBuilder()
        .insert()
        .into('transport_pricing')
        .values({
          tenant_id: tenantId,
          destination_id: svc.destination_id,
          transport_service_id: svc.id,
          category: svc.category,
          vehicle_type_id: vehicleTypeId,
          mode: 'PVT',
          currency_id: svc.currency_id,
          created_by: userId,
          updated_by: userId,
        })
        .orIgnore()
        .execute();
    }
  }
}
