import {
  Injectable,
  NotFoundException,
  BadRequestException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, LessThanOrEqual, MoreThanOrEqual, Between } from 'typeorm';
import { ClsService } from 'nestjs-cls';
import { BookingRepository } from './repositories/booking.repository';
import { UpdateBookingDto, AssignTransportItemDto, BulkAssignTransportItemDto } from './dto';
import { BookingEntity, BookingStatus } from '../../entities/booking.entity';
import { BookingHotelItemEntity } from '../../entities/booking-hotel-item.entity';
import { BookingTransportItemEntity } from '../../entities/booking-transport-item.entity';
import { BookingActivityItemEntity } from '../../entities/booking-activity-item.entity';
import { BookingSpecialItemEntity } from '../../entities/booking-special-item.entity';
import { QueryEntity, QueryStage } from '../../entities/query.entity';
import { QueryQuoteEntity } from '../../entities/query-quote.entity';
import { QueryQuoteHotelEntity } from '../../entities/query-quote-hotel.entity';
import { QueryQuoteTransportEntity } from '../../entities/query-quote-transport.entity';
import { QueryQuoteActivityEntity } from '../../entities/query-quote-activity.entity';
import { QueryQuoteSpecialEntity } from '../../entities/query-quote-special.entity';
import { AuditService } from '../audit/audit.service';
import { serviceMessages } from '../../common/constants/messages';

const MSG = serviceMessages('Booking');
const ENTITY_TYPE = 'booking';

@Injectable()
export class BookingService {
  constructor(
    private readonly repo: BookingRepository,
    private readonly cls: ClsService,
    private readonly auditService: AuditService,
    @InjectRepository(BookingHotelItemEntity) private readonly hotelItemRepo: Repository<BookingHotelItemEntity>,
    @InjectRepository(BookingTransportItemEntity) private readonly transportItemRepo: Repository<BookingTransportItemEntity>,
    @InjectRepository(BookingActivityItemEntity) private readonly activityItemRepo: Repository<BookingActivityItemEntity>,
    @InjectRepository(BookingSpecialItemEntity) private readonly specialItemRepo: Repository<BookingSpecialItemEntity>,
    @InjectRepository(QueryEntity) private readonly queryRepo: Repository<QueryEntity>,
    @InjectRepository(QueryQuoteEntity) private readonly quoteRepo: Repository<QueryQuoteEntity>,
    @InjectRepository(QueryQuoteHotelEntity) private readonly quoteHotelRepo: Repository<QueryQuoteHotelEntity>,
    @InjectRepository(QueryQuoteTransportEntity) private readonly quoteTransportRepo: Repository<QueryQuoteTransportEntity>,
    @InjectRepository(QueryQuoteActivityEntity) private readonly quoteActivityRepo: Repository<QueryQuoteActivityEntity>,
    @InjectRepository(QueryQuoteSpecialEntity) private readonly quoteSpecialRepo: Repository<QueryQuoteSpecialEntity>,
  ) {}

  private getTenantId(): string {
    return this.cls.get('tenant_id');
  }

  private computeSellingComponent(quote: any, component: 'hotel' | 'transport' | 'activity' | 'special'): number {
    const costMap = { hotel: quote.hotel_total, transport: quote.transport_total, activity: quote.activity_total, special: quote.special_total };
    const markupMap = { hotel: quote.hotel_markup, transport: quote.transport_markup, activity: quote.activity_markup, special: quote.special_markup };
    const cost = Number(costMap[component]) || 0;
    const rate = Number(quote.exchange_rate) || 1;
    const isPerComponent = quote.pricing_strategy?.includes('component');

    let total: number;
    if (isPerComponent) {
      const mkp = Number(markupMap[component]) || 0;
      total = quote.markup_type === 'percentage' ? cost + (cost * mkp / 100) : cost + mkp;
    } else {
      // Overall strategy — apply same markup ratio to each component
      const grandTotal = Number(quote.hotel_total) + Number(quote.transport_total) + Number(quote.activity_total) + Number(quote.special_total);
      const mkp = Number(quote.markup_amount) || 0;
      const markedUpTotal = quote.markup_type === 'percentage' ? grandTotal + (grandTotal * mkp / 100) : grandTotal + mkp;
      const ratio = grandTotal > 0 ? markedUpTotal / grandTotal : 1;
      total = cost * ratio;
    }
    return Math.round(total * rate * 100) / 100;
  }

  async findAll(query?: { page?: number; limit?: number; search?: string }, filters?: { status?: string; destinationId?: string; dateFrom?: string; dateTo?: string }) {
    const where: any = {};
    if (filters?.status) where.status = filters.status;
    if (filters?.destinationId) where.destination_id = filters.destinationId;
    if (filters?.dateFrom && filters?.dateTo) {
      where.start_date = Between(filters.dateFrom, filters.dateTo);
    } else if (filters?.dateFrom) {
      where.start_date = MoreThanOrEqual(filters.dateFrom);
    } else if (filters?.dateTo) {
      where.start_date = LessThanOrEqual(filters.dateTo);
    }

    const result = await this.repo.findPaginated({
      page: query?.page,
      limit: query?.limit,
      search: query?.search,
      searchColumns: ['booking_number', 'guest_name', 'guest_mobile'],
      relations: ['destination', 'destination.currency', 'selling_currency'],
      order: { created_at: 'DESC' },
      where,
    });
    await this.resolveUserNames(result.items);
    return result;
  }

  /**
   * Resolve user names for sales_team UUID arrays (attaches sales_team_users to each booking).
   */
  private async resolveUserNames(bookings: any[]) {
    const allIds = new Set<string>();
    for (const b of bookings) (b.sales_team || []).forEach((id: string) => allIds.add(id));
    if (allIds.size === 0) { for (const b of bookings) b.sales_team_users = []; return; }
    const userRows = await (this.repo as any).repo.manager.query(
      `SELECT id, name FROM users WHERE id = ANY($1) AND is_deleted = false`,
      [Array.from(allIds)],
    );
    const userMap = new Map(userRows.map((u: any) => [u.id, u.name]));
    for (const b of bookings) {
      b.sales_team_users = (b.sales_team || []).filter((id: string) => userMap.has(id)).map((id: string) => ({ id, name: userMap.get(id) }));
    }
  }

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

    // Backfill selling data from quote if missing (for bookings created before selling fields)
    if (Number(item.selling_total) === 0 && item.quote) {
      const quote = item.quote as any;
      item.selling_currency_id = quote.selling_currency_id || null;
      item.selling_currency = quote.selling_currency || null;
      item.exchange_rate = Number(quote.exchange_rate) || 1;
      item.selling_hotel_total = this.computeSellingComponent(quote, 'hotel') as any;
      item.selling_transport_total = this.computeSellingComponent(quote, 'transport') as any;
      item.selling_activity_total = this.computeSellingComponent(quote, 'activity') as any;
      item.selling_special_total = this.computeSellingComponent(quote, 'special') as any;
      item.selling_total = Number(quote.selling_total) || 0 as any;
    }

    return item;
  }

  async createFromQuery(queryId: string) {
    const tenantId = this.getTenantId();
    const userId = this.cls.get('user_id') || null;

    // 1. Load query with relations
    const query = await this.queryRepo.findOne({
      where: { id: queryId, tenant_id: tenantId, is_deleted: false } as any,
    });
    if (!query) throw new NotFoundException('Query not found');

    // 2. Find selected quote
    const selectedQuote = await this.quoteRepo.findOne({
      where: { query_id: queryId, tenant_id: tenantId, is_selected: true, is_deleted: false } as any,
    });
    if (!selectedQuote) throw new BadRequestException('No quote selected. Please select a quote before converting to booking.');

    // 3. Check if booking already exists for this query
    const existingBooking = await this.repo.findOne({
      where: { query_id: queryId } as any,
    });
    if (existingBooking) throw new BadRequestException('A booking already exists for this query.');

    // 4. Load quote child items
    const [quoteHotels, quoteTransports, quoteActivities, quoteSpecials] = await Promise.all([
      this.quoteHotelRepo.find({ where: { quote_id: selectedQuote.id, tenant_id: tenantId, is_deleted: false } as any }),
      this.quoteTransportRepo.find({ where: { quote_id: selectedQuote.id, tenant_id: tenantId, is_deleted: false } as any }),
      this.quoteActivityRepo.find({ where: { quote_id: selectedQuote.id, tenant_id: tenantId, is_deleted: false } as any }),
      this.quoteSpecialRepo.find({ where: { quote_id: selectedQuote.id, tenant_id: tenantId, is_deleted: false } as any }),
    ]);

    // 5. Generate booking number
    const bookingNumber = await this.repo.getNextBookingNumber();

    // 6. Create booking
    const booking = await this.repo.save({
      booking_number: bookingNumber,
      query_id: queryId,
      quote_id: selectedQuote.id,
      destination_id: query.destination_id,
      currency_id: query.currency_id || null,
      sales_team: query.sales_team || [],
      source_tags: (query as any).source_tags || null,
      remark_tags: (query as any).remark_tags || null,
      guest_name: query.guest_name,
      guest_email: query.guest_email,
      guest_mobile: query.guest_mobile,
      guest_alt_phone: query.guest_alt_phone,
      start_date: query.start_date,
      end_date: query.end_date,
      adults: query.adults,
      children_ages: query.children_ages,
      infants: query.infants,
      hotel_total: selectedQuote.hotel_total,
      transport_total: selectedQuote.transport_total,
      activity_total: selectedQuote.activity_total,
      special_total: selectedQuote.special_total,
      grand_total: selectedQuote.grand_total,
      // Selling price from quote
      selling_currency_id: selectedQuote.selling_currency_id || null,
      exchange_rate: selectedQuote.exchange_rate || 1,
      selling_hotel_total: this.computeSellingComponent(selectedQuote, 'hotel'),
      selling_transport_total: this.computeSellingComponent(selectedQuote, 'transport'),
      selling_activity_total: this.computeSellingComponent(selectedQuote, 'activity'),
      selling_special_total: this.computeSellingComponent(selectedQuote, 'special'),
      selling_total: selectedQuote.selling_total || 0,
      notes: null,
      status: BookingStatus.UPCOMING,
    });

    // 7. Copy hotel items
    for (const hi of quoteHotels) {
      await this.hotelItemRepo.save({
        booking_id: booking.id, tenant_id: tenantId,
        hotel_id: hi.hotel_id, room_type: hi.room_type, meal_plan: hi.meal_plan,
        check_in_date: hi.check_in_date, check_out_date: hi.check_out_date, nights: hi.nights,
        pax_per_room: hi.pax_per_room, num_rooms: hi.num_rooms,
        aweb: hi.aweb, cweb: hi.cweb, cnb: hi.cnb,
        nightly_rates: hi.nightly_rates, subtotal: hi.subtotal,
        created_by: userId, updated_by: userId,
      });
    }

    // 8. Copy transport items
    for (const ti of quoteTransports) {
      await this.transportItemRepo.save({
        booking_id: booking.id, tenant_id: tenantId,
        transport_service_id: ti.transport_service_id, vehicle_type: ti.vehicle_type,
        quantity: ti.quantity, unit_price: ti.unit_price, subtotal: ti.subtotal,
        date: ti.date, sort_order: ti.sort_order, notes: ti.notes,
        created_by: userId, updated_by: userId,
      });
    }

    // 9. Copy activity items
    for (const ai of quoteActivities) {
      await this.activityItemRepo.save({
        booking_id: booking.id, tenant_id: tenantId,
        activity_id: ai.activity_id, ticket_id: ai.ticket_id,
        age_group: ai.age_group, quantity: ai.quantity,
        unit_price: ai.unit_price, subtotal: ai.subtotal,
        slot: ai.slot, date: ai.date, sort_order: ai.sort_order, notes: ai.notes,
        created_by: userId, updated_by: userId,
      });
    }

    // 10. Copy special items
    for (const si of quoteSpecials) {
      await this.specialItemRepo.save({
        booking_id: booking.id, tenant_id: tenantId,
        service_name: si.service_name, total_price: si.total_price,
        date: si.date, sort_order: si.sort_order, comments: si.comments,
        created_by: userId, updated_by: userId,
      });
    }

    // 11. Lock the quote (prevent further edits)
    await this.quoteRepo.update(selectedQuote.id, { is_selected: true } as any);

    // 12. Transition query stage to CONVERTED
    if (query.stage !== QueryStage.CONVERTED) {
      await this.queryRepo.update(queryId, { stage: QueryStage.CONVERTED } as any);
      await this.auditService.log('query', queryId, 'stage_changed', [
        { field: 'stage', old_value: query.stage, new_value: 'Converted' },
      ]);
    }

    // 13. Log audit
    await this.auditService.log(ENTITY_TYPE, booking.id, 'created');

    return { id: booking.id, booking_number: bookingNumber, message: 'Booking created successfully' };
  }

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

    const tenantId = this.getTenantId();
    const userId = this.cls.get('user_id') || null;
    const changes: { field: string; old_value: any; new_value: any }[] = [];

    // Update top-level fields
    const updateData: any = {};
    // Basic details
    const basicFields = ['guest_name', 'guest_mobile', 'guest_email', 'guest_alt_phone', 'start_date', 'end_date', 'adults', 'children_ages', 'infants', 'notes'];
    for (const field of basicFields) {
      if ((dto as any)[field] !== undefined) {
        if ((item as any)[field] !== (dto as any)[field]) {
          changes.push({ field, old_value: (item as any)[field], new_value: (dto as any)[field] });
        }
        updateData[field] = (dto as any)[field];
      }
    }
    if (dto.sales_team !== undefined) {
      const currentVal = JSON.stringify((item as any).sales_team || []);
      const newVal = JSON.stringify(dto.sales_team || []);
      if (currentVal !== newVal) changes.push({ field: 'sales_team', old_value: (item as any).sales_team, new_value: dto.sales_team });
      updateData.sales_team = dto.sales_team;
    }
    if (dto.source_tags !== undefined) updateData.source_tags = dto.source_tags;
    if (dto.remark_tags !== undefined) updateData.remark_tags = dto.remark_tags;
    // Selling price fields
    if (dto.selling_currency_id !== undefined) updateData.selling_currency_id = dto.selling_currency_id || null;
    if (dto.exchange_rate !== undefined) updateData.exchange_rate = dto.exchange_rate;
    if (dto.selling_total !== undefined) updateData.selling_total = dto.selling_total;
    if (Object.keys(updateData).length > 0) await this.repo.update(id, updateData);

    // Sync basic details back to source query
    if (item.query_id) {
      const syncFields = ['guest_name', 'guest_mobile', 'guest_email', 'guest_alt_phone', 'start_date', 'end_date', 'adults', 'children_ages', 'infants'];
      const querySyncData: any = {};
      for (const field of syncFields) {
        if (updateData[field] !== undefined) querySyncData[field] = updateData[field];
      }
      if (updateData.sales_team !== undefined) querySyncData.sales_team = updateData.sales_team;
      if (updateData.source_tags !== undefined) querySyncData.source_tags = updateData.source_tags;
      if (updateData.remark_tags !== undefined) querySyncData.remark_tags = updateData.remark_tags;
      if (Object.keys(querySyncData).length > 0) {
        await (this.repo as any).repo.manager.update('queries', { id: item.query_id, tenant_id: tenantId, is_deleted: false }, querySyncData);
      }
    }

    // Replace child items if provided
    const hasItems = dto.hotel_items || dto.transport_items || dto.activity_items || dto.special_items;
    if (hasItems) {
      changes.push({ field: 'itinerary', old_value: null, new_value: 'Updated itinerary details' });

      // Hard-delete existing items and re-create
      if (dto.hotel_items) {
        await this.hotelItemRepo.delete({ booking_id: id, tenant_id: tenantId } as any);
        for (const hi of dto.hotel_items) {
          await this.hotelItemRepo.save({ ...hi, booking_id: id, tenant_id: tenantId, created_by: userId, updated_by: userId });
        }
      }
      if (dto.transport_items) {
        await this.transportItemRepo.delete({ booking_id: id, tenant_id: tenantId } as any);
        for (const ti of dto.transport_items) {
          await this.transportItemRepo.save({ ...ti, booking_id: id, tenant_id: tenantId, created_by: userId, updated_by: userId });
        }
      }
      if (dto.activity_items) {
        await this.activityItemRepo.delete({ booking_id: id, tenant_id: tenantId } as any);
        for (const ai of dto.activity_items) {
          await this.activityItemRepo.save({ ...ai, booking_id: id, tenant_id: tenantId, ticket_id: ai.ticket_id || null, created_by: userId, updated_by: userId });
        }
      }
      if (dto.special_items) {
        await this.specialItemRepo.delete({ booking_id: id, tenant_id: tenantId } as any);
        for (const si of dto.special_items) {
          await this.specialItemRepo.save({ ...si, booking_id: id, tenant_id: tenantId, created_by: userId, updated_by: userId });
        }
      }

      await this.recalculateTotals(id, tenantId);
    }

    if (changes.length > 0) {
      await this.auditService.log(ENTITY_TYPE, id, 'updated', changes);
    }

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

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

    const validTransitions: Record<string, string[]> = {
      [BookingStatus.UPCOMING]: [BookingStatus.ONGOING, BookingStatus.CANCELLED],
      [BookingStatus.ONGOING]: [BookingStatus.COMPLETED],
      [BookingStatus.COMPLETED]: [],
      [BookingStatus.CANCELLED]: [BookingStatus.UPCOMING],
    };

    const allowed = validTransitions[item.status] || [];
    if (!allowed.includes(status)) {
      throw new BadRequestException(`Cannot change status from "${item.status}" to "${status}"`);
    }

    await this.repo.update(id, { status });
    await this.auditService.log(ENTITY_TYPE, id, 'stage_changed', [
      { field: 'status', old_value: item.status, new_value: status },
    ]);

    return { id, status, message: `Status changed to ${status}` };
  }

  async remove(id: string) {
    const item = await this.repo.findById(id);
    if (!item) throw new NotFoundException(MSG.NOT_FOUND);
    await this.repo.softDelete(id);
    await this.auditService.log(ENTITY_TYPE, id, 'deleted');
    return { message: MSG.DELETED };
  }

  async getTransportDates(destinationId: string) {
    const tenantId = this.getTenantId();
    const result = await this.transportItemRepo.createQueryBuilder('ti')
      .select('DISTINCT ti.date', 'date')
      .innerJoin('bookings', 'b', 'b.id = ti.booking_id')
      .where('b.tenant_id = :tenantId', { tenantId })
      .andWhere('b.destination_id = :destinationId', { destinationId })
      .andWhere('b.is_deleted = false')
      .andWhere('ti.is_deleted = false')
      .andWhere('ti.date IS NOT NULL')
      .orderBy('ti.date', 'ASC')
      .getRawMany();
    return result.map((r: any) => r.date);
  }

  async getMovementChart(destinationId: string, date: string) {
    const tenantId = this.getTenantId();

    // 1. Fetch all transport items for this destination + date
    const transportItems = await this.transportItemRepo.createQueryBuilder('ti')
      .leftJoinAndSelect('ti.booking', 'b')
      .leftJoinAndSelect('ti.transport_service', 'ts')
      .leftJoinAndSelect('ti.driver', 'd')
      .leftJoinAndSelect('ti.vehicle', 'v')
      .leftJoinAndSelect('ti.supplier', 's')
      .where('ti.tenant_id = :tenantId', { tenantId })
      .andWhere('ti.is_deleted = false')
      .andWhere('ti.date = :date', { date })
      .andWhere('b.destination_id = :destinationId', { destinationId })
      .andWhere('b.is_deleted = false')
      .andWhere('b.status != :cancelled', { cancelled: BookingStatus.CANCELLED })
      .orderBy('ti.pickup_time', 'ASC')
      .addOrderBy('ti.sort_order', 'ASC')
      .getMany();

    // 2. Fetch all activity items for this destination + date
    const activityItems = await this.activityItemRepo.createQueryBuilder('ai')
      .leftJoinAndSelect('ai.booking', 'b')
      .leftJoinAndSelect('ai.activity', 'a')
      .leftJoinAndSelect('ai.ticket', 't')
      .leftJoinAndSelect('ai.supplier', 's')
      .where('ai.tenant_id = :tenantId', { tenantId })
      .andWhere('ai.is_deleted = false')
      .andWhere('ai.date = :date', { date })
      .andWhere('b.destination_id = :destinationId', { destinationId })
      .andWhere('b.is_deleted = false')
      .andWhere('b.status != :cancelled', { cancelled: BookingStatus.CANCELLED })
      .orderBy('ai.slot', 'ASC')
      .addOrderBy('ai.sort_order', 'ASC')
      .getMany();

    // Helper: compute pax from booking
    const getPax = (b: any) => (b?.adults || 0) + ((b?.children_ages as any)?.length || 0) + (b?.infants || 0);

    // 3. Build transport ops
    const transportOps = transportItems.map(ti => ({
      id: ti.id,
      type: 'transport' as const,
      booking_id: ti.booking_id,
      booking_number: ti.booking?.booking_number || '',
      guest_name: ti.booking?.guest_name || '',
      guest_mobile: ti.booking?.guest_mobile || '',
      guest_hotel: '', // Could be derived from hotel items in future
      pax: getPax(ti.booking),
      time: ti.pickup_time || null,
      pickup_address: ti.pickup_address || null,
      dropoff_address: ti.dropoff_address || null,
      service_name: ti.transport_service ? `${ti.transport_service.from_city} → ${ti.transport_service.to_city}` : '',
      service_type: ti.transport_service?.category || '',
      driver_id: ti.driver_id,
      driver_name: ti.driver?.name || null,
      driver_phone: ti.driver?.phone || null,
      vehicle_id: ti.vehicle_id,
      vehicle_no: ti.vehicle?.vehicle_no || null,
      vehicle_type: ti.vehicle?.type || null,
      vehicle_capacity: ti.vehicle?.capacity || null,
      supplier_id: ti.supplier_id,
      supplier_name: ti.supplier?.name || null,
      status: ti.assignment_status || 'scheduled',
      notes: ti.notes,
    }));

    // 4. Build activity ops (group by activity_id + ticket_id + booking_id for pax aggregation)
    const activityMap = new Map<string, any>();
    for (const ai of activityItems) {
      const key = `${ai.booking_id}_${ai.activity_id}_${ai.ticket_id || 'none'}`;
      if (!activityMap.has(key)) {
        activityMap.set(key, {
          id: ai.id,
          type: 'activity' as const,
          booking_id: ai.booking_id,
          booking_number: ai.booking?.booking_number || '',
          guest_name: ai.booking?.guest_name || '',
          guest_mobile: ai.booking?.guest_mobile || '',
          pax: getPax(ai.booking),
          time: ai.slot || null,
          activity_id: ai.activity_id,
          activity_name: ai.activity?.name || '',
          activity_address: ai.activity?.address || '',
          ticket_id: ai.ticket_id,
          ticket_name: ai.ticket?.name || null,
          slot: ai.slot,
          supplier_id: ai.supplier_id,
          supplier_name: ai.supplier?.name || null,
          status: ai.assignment_status || 'scheduled',
          notes: ai.notes,
          age_groups: {} as Record<string, number>,
        });
      }
      const entry = activityMap.get(key);
      entry.age_groups[ai.age_group] = (entry.age_groups[ai.age_group] || 0) + ai.quantity;
    }
    const activityOps = Array.from(activityMap.values());

    // 5. Stats
    const allOps = [...transportOps, ...activityOps];
    const assignedTransports = transportOps.filter(t => t.driver_id);
    const completedOps = allOps.filter(o => o.status === 'completed');
    const uniqueDrivers = new Set(transportOps.filter(t => t.driver_id).map(t => t.driver_id));

    const summary = {
      total_ops: allOps.length,
      transfers: transportOps.length,
      activities: activityOps.length,
      active_drivers: uniqueDrivers.size,
      completed: completedOps.length,
      scheduled: allOps.length - completedOps.length,
    };

    // 6. By Driver view — group transport ops by driver
    const driverMap = new Map<string, any>();
    for (const op of transportOps) {
      const driverId = op.driver_id || '__unassigned__';
      if (!driverMap.has(driverId)) {
        driverMap.set(driverId, {
          driver_id: op.driver_id,
          driver_name: op.driver_name || 'Unassigned',
          driver_phone: op.driver_phone,
          vehicle_no: op.vehicle_no,
          vehicle_type: op.vehicle_type,
          vehicle_capacity: op.vehicle_capacity,
          ops: [],
        });
      }
      driverMap.get(driverId).ops.push(op);
    }
    const byDriver = Array.from(driverMap.values()).sort((a, b) => {
      if (a.driver_id && !b.driver_id) return -1;
      if (!a.driver_id && b.driver_id) return 1;
      return (a.driver_name || '').localeCompare(b.driver_name || '');
    });

    // 7. By Booking view — group all ops by booking
    const bookingMap = new Map<string, any>();
    for (const op of allOps) {
      if (!bookingMap.has(op.booking_id)) {
        bookingMap.set(op.booking_id, {
          booking_id: op.booking_id,
          booking_number: op.booking_number,
          guest_name: op.guest_name,
          guest_mobile: op.guest_mobile,
          pax: op.pax,
          ops: [],
        });
      }
      bookingMap.get(op.booking_id).ops.push(op);
    }
    const byBooking = Array.from(bookingMap.values()).sort((a, b) => a.booking_number.localeCompare(b.booking_number));

    // 8. By Activity view — group activity ops by activity
    const actGroupMap = new Map<string, any>();
    for (const op of activityOps) {
      if (!actGroupMap.has(op.activity_id)) {
        actGroupMap.set(op.activity_id, {
          activity_id: op.activity_id,
          activity_name: op.activity_name,
          activity_address: op.activity_address,
          items: [],
        });
      }
      actGroupMap.get(op.activity_id).items.push(op);
    }
    const byActivity = Array.from(actGroupMap.values()).sort((a, b) => a.activity_name.localeCompare(b.activity_name));

    return { summary, by_driver: byDriver, by_booking: byBooking, by_activity: byActivity };
  }

  async getDriverAssignments(destinationId: string, date: string, transportServiceId?: string) {
    const tenantId = this.getTenantId();

    const qb = this.transportItemRepo.createQueryBuilder('ti')
      .leftJoinAndSelect('ti.booking', 'b')
      .leftJoinAndSelect('ti.transport_service', 'ts')
      .leftJoinAndSelect('ti.driver', 'd')
      .leftJoinAndSelect('ti.vehicle', 'v')
      .leftJoinAndSelect('ti.supplier', 's')
      .where('ti.tenant_id = :tenantId', { tenantId })
      .andWhere('ti.is_deleted = false')
      .andWhere('ti.date = :date', { date })
      .andWhere('b.destination_id = :destinationId', { destinationId })
      .andWhere('b.is_deleted = false')
      .andWhere('b.status != :cancelled', { cancelled: BookingStatus.CANCELLED })
      .orderBy('ts.short_code', 'ASC')
      .addOrderBy('CASE WHEN ti.assignment_status = \'assigned\' THEN 0 ELSE 1 END', 'ASC')
      .addOrderBy('ti.pickup_time', 'ASC');

    if (transportServiceId) {
      qb.andWhere('ti.transport_service_id = :transportServiceId', { transportServiceId });
    }

    const items = await qb.getMany();

    // Group by transport service
    const groupMap = new Map<string, any>();
    for (const item of items) {
      const tsId = item.transport_service_id;
      if (!groupMap.has(tsId)) {
        const ts = item.transport_service;
        groupMap.set(tsId, {
          transport_service_id: tsId,
          transport_service_name: ts ? `${ts.short_code} (${ts.from_city} → ${ts.to_city})` : tsId,
          short_code: ts?.short_code || '',
          from_city: ts?.from_city || '',
          to_city: ts?.to_city || '',
          items: [],
        });
      }
      const booking = item.booking;
      const pax = (booking?.adults || 0) + ((booking?.children_ages as any)?.length || 0) + (booking?.infants || 0);
      groupMap.get(tsId).items.push({
        id: item.id,
        booking_id: item.booking_id,
        booking_number: booking?.booking_number || '',
        guest_name: booking?.guest_name || '',
        guest_mobile: booking?.guest_mobile || '',
        pax,
        pickup_time: item.pickup_time,
        pickup_address: item.pickup_address,
        dropoff_address: item.dropoff_address,
        supplier_id: item.supplier_id,
        supplier_name: item.supplier?.name || null,
        driver_id: item.driver_id,
        driver_name: item.driver?.name || null,
        driver_phone: item.driver?.phone || null,
        vehicle_id: item.vehicle_id,
        vehicle_no: item.vehicle?.vehicle_no || null,
        vehicle_type: item.vehicle?.type || null,
        vehicle_capacity: item.vehicle?.capacity || null,
        assignment_status: item.assignment_status || 'pending',
        notes: item.notes,
        vehicle_type_label: item.vehicle_type,
      });
    }

    const groups = Array.from(groupMap.values()).map(g => ({
      ...g,
      total_items: g.items.length,
      assigned_count: g.items.filter((i: any) => i.assignment_status === 'assigned').length,
      pending_count: g.items.filter((i: any) => i.assignment_status !== 'assigned').length,
      total_pax: g.items.reduce((sum: number, i: any) => sum + i.pax, 0),
    }));

    const totals = {
      routes: groups.length,
      total_transfers: items.length,
      assigned: items.filter(i => i.assignment_status === 'assigned').length,
      pending: items.filter(i => i.assignment_status !== 'assigned').length,
      total_pax: groups.reduce((sum, g) => sum + g.total_pax, 0),
    };

    return { summary: totals, groups };
  }

  async assignTransportItem(itemId: string, dto: AssignTransportItemDto) {
    const tenantId = this.getTenantId();
    const userId = this.cls.get('user_id') || null;

    const item = await this.transportItemRepo.findOne({
      where: { id: itemId, tenant_id: tenantId, is_deleted: false } as any,
    });
    if (!item) throw new NotFoundException('Transport item not found');

    // Check driver/vehicle capacity conflict
    if (dto.driver_id && item.date) {
      const existingAssignments = await this.transportItemRepo.createQueryBuilder('ti')
        .innerJoin('bookings', 'b', 'b.id = ti.booking_id')
        .where('ti.driver_id = :driverId', { driverId: dto.driver_id })
        .andWhere('ti.date = :date', { date: item.date })
        .andWhere('ti.transport_service_id = :tsId', { tsId: item.transport_service_id })
        .andWhere('ti.id != :itemId', { itemId })
        .andWhere('ti.tenant_id = :tenantId', { tenantId })
        .andWhere('ti.is_deleted = false')
        .andWhere('b.is_deleted = false')
        .getMany();

      if (existingAssignments.length > 0) {
        // Check vehicle capacity
        const vehicleId = dto.vehicle_id || item.vehicle_id;
        if (vehicleId) {
          const vehicle = await (this as any).transportItemRepo.manager.findOne('vehicles', { where: { id: vehicleId } });
          const capacity = vehicle?.capacity || 999;
          // Sum pax from existing assignments + current item's booking
          const booking = await this.repo.findOne({ where: { id: item.booking_id, tenant_id: tenantId } as any });
          const currentPax = (booking?.adults || 0) + ((booking?.children_ages as any)?.length || 0);
          const assignedPax = existingAssignments.reduce((sum, a) => sum + ((a as any).pax || 0), 0);

          if (assignedPax + currentPax > capacity) {
            throw new BadRequestException(
              `Driver already assigned to ${existingAssignments.length} booking(s) on this route for this date. Vehicle capacity (${capacity} pax) would be exceeded (${assignedPax + currentPax} pax needed).`
            );
          }
        }
      }
    }

    const updateData: any = { updated_by: userId };
    if (dto.supplier_id !== undefined) updateData.supplier_id = dto.supplier_id;
    if (dto.driver_id !== undefined) updateData.driver_id = dto.driver_id;
    if (dto.vehicle_id !== undefined) updateData.vehicle_id = dto.vehicle_id;
    if (dto.pickup_time !== undefined) updateData.pickup_time = dto.pickup_time;
    if (dto.pickup_address !== undefined) updateData.pickup_address = dto.pickup_address;
    if (dto.dropoff_address !== undefined) updateData.dropoff_address = dto.dropoff_address;

    // Auto-set assignment_status
    if (dto.driver_id) {
      updateData.assignment_status = 'assigned';
    } else if (dto.driver_id === null) {
      updateData.assignment_status = null;
    }

    await this.transportItemRepo.update(itemId, updateData);

    await this.auditService.log('booking_transport_item', itemId, 'updated', [
      { field: 'driver_assignment', old_value: item.driver_id, new_value: dto.driver_id || null },
    ]);

    return { id: itemId, message: 'Driver assigned successfully' };
  }

  async bulkAssignTransportItems(dto: BulkAssignTransportItemDto) {
    const tenantId = this.getTenantId();
    const userId = this.cls.get('user_id') || null;

    const updateData: any = {
      supplier_id: dto.supplier_id,
      driver_id: dto.driver_id,
      assignment_status: 'assigned',
      updated_by: userId,
    };
    if (dto.vehicle_id) updateData.vehicle_id = dto.vehicle_id;

    // Check vehicle capacity for bulk assignment
    if (dto.vehicle_id) {
      const vehicle = await (this as any).transportItemRepo.manager.findOne('vehicles', { where: { id: dto.vehicle_id } });
      if (vehicle?.capacity) {
        // Get total pax from all items being assigned
        let totalPax = 0;
        for (const itemId of dto.item_ids) {
          const item = await this.transportItemRepo.findOne({ where: { id: itemId, tenant_id: tenantId, is_deleted: false } as any });
          if (!item) continue;
          const booking = await this.repo.findOne({ where: { id: item.booking_id, tenant_id: tenantId } as any });
          totalPax += (booking?.adults || 0) + ((booking?.children_ages as any)?.length || 0);
        }

        // Also count already-assigned items for this driver on same date/route (not in current selection)
        const sampleItem = await this.transportItemRepo.findOne({ where: { id: dto.item_ids[0], tenant_id: tenantId } as any });
        if (sampleItem?.date) {
          const existingPax = await this.transportItemRepo.createQueryBuilder('ti')
            .innerJoin('bookings', 'b', 'b.id = ti.booking_id')
            .select('SUM(b.adults + COALESCE(array_length(b.children_ages, 1), 0))', 'pax')
            .where('ti.driver_id = :driverId', { driverId: dto.driver_id })
            .andWhere('ti.date = :date', { date: sampleItem.date })
            .andWhere('ti.transport_service_id = :tsId', { tsId: sampleItem.transport_service_id })
            .andWhere('ti.id NOT IN (:...itemIds)', { itemIds: dto.item_ids })
            .andWhere('ti.tenant_id = :tenantId', { tenantId })
            .andWhere('ti.is_deleted = false')
            .andWhere('b.is_deleted = false')
            .getRawOne();

          const existingTotal = parseInt(existingPax?.pax || '0', 10);
          if (existingTotal + totalPax > vehicle.capacity) {
            throw new BadRequestException(
              `Vehicle capacity exceeded. ${vehicle.capacity} pax capacity, but ${existingTotal + totalPax} pax needed (${existingTotal} existing + ${totalPax} new).`
            );
          }
        }
      }
    }

    for (const itemId of dto.item_ids) {
      const item = await this.transportItemRepo.findOne({
        where: { id: itemId, tenant_id: tenantId, is_deleted: false } as any,
      });
      if (!item) continue;
      await this.transportItemRepo.update(itemId, updateData);
    }

    return { count: dto.item_ids.length, message: `${dto.item_ids.length} items assigned successfully` };
  }

  private async recalculateTotals(bookingId: string, tenantId: string) {
    const sum = async (repo: Repository<any>, field: string) => {
      const result = await repo.createQueryBuilder('item')
        .select(`SUM(item.${field})`, 'total')
        .where('item.booking_id = :bookingId', { bookingId })
        .andWhere('item.tenant_id = :tenantId', { tenantId })
        .andWhere('item.is_deleted = false')
        .getRawOne();
      return Number(result?.total) || 0;
    };

    const hotelTotal = await sum(this.hotelItemRepo, 'subtotal');
    const transportTotal = await sum(this.transportItemRepo, 'subtotal');
    const activityTotal = await sum(this.activityItemRepo, 'subtotal');
    const specialTotal = await sum(this.specialItemRepo, 'total_price');

    const grandTotal = hotelTotal + transportTotal + activityTotal + specialTotal;

    // Recalculate selling totals from booking's markup config
    const booking = await this.repo.findById(bookingId);
    const updatePayload: any = {
      hotel_total: hotelTotal, transport_total: transportTotal,
      activity_total: activityTotal, special_total: specialTotal,
      grand_total: grandTotal,
    };

    if (booking) {
      // Use quote markup config to recompute selling component totals
      const quote = await this.quoteRepo.findOne({ where: { id: booking.quote_id, is_deleted: false } as any });
      if (quote) {
        const fakeQuote = { ...quote, hotel_total: hotelTotal, transport_total: transportTotal, activity_total: activityTotal, special_total: specialTotal };
        updatePayload.selling_hotel_total = this.computeSellingComponent(fakeQuote, 'hotel');
        updatePayload.selling_transport_total = this.computeSellingComponent(fakeQuote, 'transport');
        updatePayload.selling_activity_total = this.computeSellingComponent(fakeQuote, 'activity');
        updatePayload.selling_special_total = this.computeSellingComponent(fakeQuote, 'special');
      }
    }

    await this.repo.update(bookingId, updatePayload);
  }
}
