import { Injectable, NotFoundException } from '@nestjs/common';
import { FindOptionsOrder, FindOptionsWhere, Repository } from 'typeorm';
import { ClsService } from 'nestjs-cls';
import { TransportServiceRepository } from './repositories/transport-service.repository';
import { TransportServiceEntity } from '../../entities/transport-service.entity';
import { CreateTransportServiceDto, UpdateTransportServiceDto } from './dto';
import { serviceMessages } from '../../common/constants/messages';
import { deriveUniqueShortCode } from '../../common/utils/short-code';

const MSG = serviceMessages('Transport service');

// Repo internals — TenantAwareRepository protects these. The pricing
// matrix sync needs the underlying EntityManager + tenant CLS, so we
// narrow the unsafe cast to this shape rather than `any`.
interface RepoInternals {
  getTenantId(): string;
  cls: ClsService;
  repo: Repository<TransportServiceEntity>;
}

@Injectable()
export class TransportServiceService {
  constructor(private readonly repo: TransportServiceRepository) {}

  async findAll(
    query?: { page?: number; limit?: number; search?: string },
    filters?: { destinationId?: string; category?: string },
  ) {
    const where: FindOptionsWhere<TransportServiceEntity> = {};
    if (filters?.destinationId) where.destination_id = filters.destinationId;
    if (filters?.category) where.category = filters.category;

    return this.repo.findPaginated({
      page: query?.page,
      limit: query?.limit,
      search: query?.search,
      // Search covers route + long-form text so ops can find a
      // transport service by content keyword ("city tour", "airport",
      // "no smoking") in addition to the route cities and code.
      // Aligned with the activity search fields (see activity.service).
      searchColumns: [
        'short_code',
        'from_city',
        'to_city',
        'from_address',
        'to_address',
        'description',
        'customer_whatsapp_body',
        'supplier_whatsapp_body',
      ],
      relations: ['destination'],
      order: { created_at: 'DESC' },
      where,
    });
  }

  async findAllForLookup() {
    return this.repo.find({
      where: { is_active: true },
      // is_airport_service surfaced in the lookup so booking forms
      // and the Movement Planner know which routes need flight-detail
      // capture without an extra round-trip.
      select: {
        id: true,
        short_code: true,
        category: true,
        from_city: true,
        to_city: true,
        // from_address / to_address feed the Quote + Booking auto-fill
        // on transport row select — the form prefers the real street
        // (this column) over the city descriptor; falls back to the
        // day's hotel address when this side of the master is blank.
        from_address: true,
        to_address: true,
        destination_id: true,
        is_airport_service: true,
        is_active: true,
        // Disposal services surface duty_type + duration_hours so the
        // dropdown label (formatTransportRowLabel) reads "Dubai · 4 h
        // Disposal" instead of "Dubai → Dubai", and the booking row
        // can render the time-band UI without a second lookup.
        duty_type: true,
        duration_hours: true,
      },
      order: { short_code: 'ASC' } as FindOptionsOrder<TransportServiceEntity>,
    });
  }

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

  async create(dto: CreateTransportServiceDto) {
    // short_code is auto-derived from from_city + to_city + category,
    // tenant-unique. UI no longer collects it. Seeders may still pass
    // an explicit value via the DTO. Wider maxLen (12) than the other
    // modules because route codes get bumpy fast ("DUBAIA", "DUBAIA1"…
    // is unreadable when the user has 200 routes).
    const baseSource = [dto.from_city, dto.to_city, dto.category].filter(Boolean).join('');
    const shortCode =
      dto.short_code ||
      (await deriveUniqueShortCode(
        baseSource,
        'ROUTE',
        (code) => this.repo.findByShortCode(code).then(Boolean),
        12,
      ));

    const item = await this.repo.save({
      destination_id: dto.destination_id,
      short_code: shortCode,
      category: dto.category,
      from_city: dto.from_city,
      from_address: dto.from_address || null,
      from_gps_latitude: dto.from_gps_latitude || null,
      from_gps_longitude: dto.from_gps_longitude || null,
      to_city: dto.to_city,
      to_address: dto.to_address || null,
      to_gps_latitude: dto.to_gps_latitude || null,
      to_gps_longitude: dto.to_gps_longitude || null,
      return_from_city: dto.return_from_city || null,
      return_from_address: dto.return_from_address || null,
      return_from_gps_latitude: dto.return_from_gps_latitude || null,
      return_from_gps_longitude: dto.return_from_gps_longitude || null,
      return_to_city: dto.return_to_city || null,
      return_to_address: dto.return_to_address || null,
      return_to_gps_latitude: dto.return_to_gps_latitude || null,
      return_to_gps_longitude: dto.return_to_gps_longitude || null,
      distance_km: dto.distance_km ?? null,
      duration_mins: dto.duration_mins ?? null,
      return_duration_mins: dto.return_duration_mins ?? null,
      // Disposal fields — duty_type only applies when category is
      // DISPOSAL. duration_hours mirrors the enum for fixed-duty types
      // (HALF_DAY_4H → 4, FULL_DAY_8H → 8, …) or carries the ops-typed
      // custom value for HOURLY.
      duty_type: dto.category === 'DISPOSAL' ? (dto.duty_type ?? null) : null,
      duration_hours:
        dto.category === 'DISPOSAL' ? (dto.duration_hours ?? null) : null,
      description: dto.description || null,
      is_airport_service: dto.is_airport_service ?? false,
      is_active: dto.is_active ?? true,
    });

    // Auto-create a transport-pricing row for EVERY active vehicle
    // type in the tenant master — per stakeholder spec, ops may
    // know a route uses a Sedan even before they've registered
    // physical Sedan vehicles for the destination, so the pricing
    // row should already be there waiting for a number. If a tenant
    // adds a new vehicle type later, the vehicle-type create path
    // backfills rows for existing services (mirror logic).
    //
    // Mirrors ActivityService — uses .orIgnore() so re-runs are
    // idempotent against the (tenant, service, vehicle_type_id, currency)
    // unique constraint.
    try {
      await this.syncPricingMatrix(item.id, dto.destination_id, dto.category);
    } catch {
      /* Don't fail route creation if pricing auto-create fails */
    }

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

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

    // short_code is immutable post-create — strip from update payload.
    const updateData: Partial<TransportServiceEntity> = {};
    const fields = [
      'destination_id',
      'category',
      'from_city',
      'from_address',
      'from_gps_latitude',
      'from_gps_longitude',
      'to_city',
      'to_address',
      'to_gps_latitude',
      'to_gps_longitude',
      'return_from_city',
      'return_from_address',
      'return_from_gps_latitude',
      'return_from_gps_longitude',
      'return_to_city',
      'return_to_address',
      'return_to_gps_latitude',
      'return_to_gps_longitude',
      'distance_km',
      'duration_mins',
      'return_duration_mins',
      'duty_type',
      'duration_hours',
      'description',
      'is_airport_service',
      'is_active',
      // Per-service WhatsApp body (Phase D2b). Must be in this
      // whitelist for PATCHes to persist — the transport-service
      // update copies DTO → entity field by field, so anything
      // missing here is silently dropped even if the DTO validates.
      'customer_whatsapp_body',
      'supplier_whatsapp_body',
    ];
    for (const field of fields) {
      const value = (dto as Record<string, unknown>)[field];
      if (value !== undefined) {
        (updateData as Record<string, unknown>)[field] = value;
      }
    }

    await this.repo.update(id, updateData);

    // Sync category to transport pricing
    if (updateData.category) {
      const internals = this.repo as unknown as RepoInternals;
      const tenantId = internals.getTenantId();
      await internals.repo.manager.update(
        'transport_pricing',
        { transport_service_id: id, tenant_id: tenantId, is_deleted: false },
        { category: updateData.category },
      );
    }

    // Backfill any missing (route × vehicle type) rows — mirrors the
    // activity update sync. Covers the case where a vehicle type was
    // added AFTER this route was created but the cross-create backfill
    // failed, or where destination/category was changed and we want
    // the pricing matrix to stay complete.
    try {
      const nextDest = updateData.destination_id || item.destination_id;
      const nextCategory = updateData.category || item.category;
      await this.syncPricingMatrix(id, nextDest, nextCategory);
    } catch {
      /* don't fail update if sync fails */
    }

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

  /**
   * Idempotent upsert of transport_pricing rows for the given route
   * across every active vehicle type in the tenant master. Mirrors
   * ActivityService's age_group × ticket matrix sync.
   *
   * Relies on the (tenant_id, transport_service_id, vehicle_type_id,
   * currency_id) unique constraint + ON CONFLICT DO NOTHING so it's
   * safe to call on both create and update paths.
   */
  private async syncPricingMatrix(
    transportServiceId: string,
    destinationId: string,
    category: string,
  ) {
    const internals = this.repo as unknown as RepoInternals;
    const tenantId = internals.getTenantId();
    const userId = internals.cls?.get<string>('user_id') || null;
    const manager = internals.repo.manager;

    const destRows: { currency_id: string | null }[] = await manager.query(
      `SELECT currency_id FROM destinations WHERE id = $1 LIMIT 1`,
      [destinationId],
    );
    const currencyId = destRows[0]?.currency_id;
    if (!currencyId) return;

    // Scaffold pricing rows only for vehicle types LINKED to this
    // destination (destination_vehicle_types). Two reasons:
    //   1. Tenants run different fleets per destination — a 53-seater
    //      bus mapped only for Sharjah shouldn't get a blank pricing
    //      row on every Dubai route.
    //   2. Ops navigates the Transport Pricing matrix one destination
    //      at a time; surfacing types that aren't even configured for
    //      this destination clutters the grid.
    //
    // Falls back to the full active-types list ONLY when the
    // destination has no DVT mappings yet — keeps the legacy seeder
    // path / first-onboard flow working before ops has configured the
    // destination's fleet.
    const dvtRows: { id: string }[] = await manager.query(
      `SELECT vt.id FROM destination_vehicle_types dvt
         JOIN vehicle_types vt ON vt.id = dvt.vehicle_type_id
        WHERE dvt.tenant_id = $1
          AND dvt.destination_id = $2
          AND dvt.is_deleted = false
          AND vt.is_deleted = false
          AND vt.is_active = true`,
      [tenantId, destinationId],
    );
    let vehicleTypeIds: string[] = Array.from(new Set(dvtRows.map((t) => t.id)));
    if (vehicleTypeIds.length === 0) {
      const fallback: { id: string }[] = await manager.query(
        `SELECT id FROM vehicle_types
         WHERE tenant_id = $1 AND is_deleted = false AND is_active = true`,
        [tenantId],
      );
      vehicleTypeIds = Array.from(new Set(fallback.map((t) => t.id)));
      if (vehicleTypeIds.length === 0) return;
    }

    // PVT — one row per (route × vehicle type). Existing behaviour.
    for (const vehicleTypeId of vehicleTypeIds) {
      await manager
        .createQueryBuilder()
        .insert()
        .into('transport_pricing')
        .values({
          tenant_id: tenantId,
          destination_id: destinationId,
          transport_service_id: transportServiceId,
          category,
          vehicle_type_id: vehicleTypeId,
          mode: 'PVT',
          currency_id: currencyId,
          created_by: userId,
          updated_by: userId,
        })
        .orIgnore()
        .execute();
    }
    // SIC — exactly ONE row per route (vehicle_type_id = NULL). Per-
    // person rates don't depend on which coach the supplier
    // dispatches, so the matrix has no vehicle axis here.
    await manager
      .createQueryBuilder()
      .insert()
      .into('transport_pricing')
      .values({
        tenant_id: tenantId,
        destination_id: destinationId,
        transport_service_id: transportServiceId,
        category,
        vehicle_type_id: null,
        mode: 'SIC',
        currency_id: currencyId,
        created_by: userId,
        updated_by: userId,
      })
      .orIgnore()
      .execute();
  }

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

    await this.repo.softDelete(id);
    return { message: MSG.DELETED };
  }
}
