import { Injectable, BadRequestException } from '@nestjs/common';
import { ClsService } from 'nestjs-cls';
import type { EntityManager } from 'typeorm';
import { BookingRepository } from '../repositories/booking.repository';
import { CreateQuickBookingDto } from '../dto';
import { BookingStatus } from '../../../entities/booking.entity';
import { QueryStage } from '../../../entities/query.entity';
import { AuditService } from '../../audit/audit.service';
import { AuditEntityType } from '../../audit/audit-types';
import { computeHotelStayTotal } from '../../../common/hotel/hotel-totals';
import type { QuickHotelItemDto } from '../dto/create-quick-booking.dto';
import { BookingHotelPeriodEntity } from '../../../entities/booking-hotel-period.entity';
import { BookingHotelExtraEntity } from '../../../entities/booking-hotel-extra.entity';
import { QueryQuoteHotelPeriodEntity } from '../../../entities/query-quote-hotel-period.entity';
import { QueryQuoteHotelExtraEntity } from '../../../entities/query-quote-hotel-extra.entity';
import { assertNightsInRange } from '../../../common/hotel/hotel-validation';
import {
  allocateBookingNumberFromQuery,
  allocateUniqueReferenceNumber,
  DEFAULT_BOOKING_PREFIX,
} from '../../../common/utils/reference-number';

/**
 * Insert a hotel stay's periods + extras rows in one go via the
 * transaction's TypeORM repository — no raw SQL. `side` picks the
 * entity pair (quote vs booking); both pairs have identical shapes.
 *
 * All child rows inherit their parent's tenant_id explicitly so the
 * DB-level tenant-consistency trigger accepts them.
 */
async function insertHotelChildren(
  tx: EntityManager,
  tenantId: string,
  hotelItemId: string,
  userId: string,
  h: QuickHotelItemDto,
  side: 'quote' | 'booking',
): Promise<void> {
  const periods = h.periods ?? [];
  const extras = h.extras ?? [];
  // App-layer bound check — DB has an absolute-max CHECK (0..99) but
  // this catches the more common mistake of pointing at nights outside
  // the parent stay's window before it hits the DB.
  assertNightsInRange(periods, extras, h.nights);

  if (periods.length > 0) {
    const periodRepo = tx.getRepository(
      side === 'quote' ? QueryQuoteHotelPeriodEntity : BookingHotelPeriodEntity,
    );
    await periodRepo.save(
      periods.map((p, i) => ({
        hotel_item_id: hotelItemId,
        tenant_id: tenantId,
        sort_order: p.sort_order ?? i,
        nights: p.nights ?? [],
        room_type: p.room_type ?? '',
        meal_plan: p.meal_plan ?? '',
        num_rooms: p.num_rooms,
        pax_per_room: p.pax_per_room,
        rate_per_night: p.rate_per_night,
        created_by: userId,
        updated_by: userId,
      })),
    );
  }
  if (extras.length > 0) {
    const extraRepo = tx.getRepository(
      side === 'quote' ? QueryQuoteHotelExtraEntity : BookingHotelExtraEntity,
    );
    await extraRepo.save(
      extras.map((e, i) => ({
        hotel_item_id: hotelItemId,
        tenant_id: tenantId,
        sort_order: e.sort_order ?? i,
        kind: e.kind,
        description: e.description ?? null,
        quantity: e.quantity,
        rate: e.rate,
        nights: e.nights ?? [],
        scale_by_rooms: e.scale_by_rooms ?? false,
        created_by: userId,
        updated_by: userId,
      })),
    );
  }
}

const ENTITY_TYPE = AuditEntityType.BOOKING;

/**
 * QuickBookingService — walk-in / ad-hoc booking creation.
 *
 * Extracted from BookingService (2026-05-12 refactor — keep services
 * under ~800 LOC each per the architecture audit). Logic moved
 * verbatim from BookingService.createQuick — no behaviour change.
 *
 * Why self-contained: createQuick uses a single raw-SQL transaction
 * via the EntityManager and doesn't share state with the rest of
 * BookingService beyond the (tiny) getTenantId helper, which is
 * inlined here. No injection into BookingService needed.
 */
@Injectable()
export class QuickBookingService {
  constructor(
    private readonly repo: BookingRepository,
    private readonly cls: ClsService,
    private readonly auditService: AuditService,
  ) {}

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

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

    // Items are optional at quick-create time — user will build the itinerary
    // in the booking detail editor after navigating there.
    const hotelItems = dto.hotel_items || [];
    const transportItems = dto.transport_items || [];
    const activityItems = dto.activity_items || [];
    const specialItems = dto.special_items || [];

    // Resolve source — default to "Direct Walk-in" trip source in this tenant
    const manager = this.repo.ormRepo.manager;
    let sourceId = dto.source_id;
    if (!sourceId) {
      const walkIn = await manager.query(
        `SELECT id FROM trip_sources WHERE tenant_id = $1 AND is_deleted = false
           AND (LOWER(name) LIKE '%walk%in%' OR LOWER(short_name) LIKE '%walk%')
         ORDER BY created_at ASC LIMIT 1`,
        [tenantId],
      );
      sourceId = walkIn?.[0]?.id;
      if (!sourceId) {
        throw new BadRequestException(
          'No "Direct Walk-in" trip source configured. Select a source manually.',
        );
      }
    }

    // Resolve currency — default to destination currency.
    // tenant_id filter is mandatory: an attacker-supplied destination
    // UUID from another tenant must NOT resolve here.
    let currencyId = dto.currency_id;
    if (!currencyId) {
      const dest = await manager.query(
        `SELECT currency_id FROM destinations WHERE id = $1 AND tenant_id = $2 AND is_deleted = false`,
        [dto.destination_id, tenantId],
      );
      currencyId = dest?.[0]?.currency_id;
    }

    // Totals
    const hotelTotal = hotelItems.reduce((s, i) => s + Number(i.subtotal || 0), 0);
    const transportTotal = transportItems.reduce((s, i) => s + Number(i.subtotal || 0), 0);
    const activityTotal = activityItems.reduce((s, i) => s + Number(i.subtotal || 0), 0);
    const specialTotal = specialItems.reduce((s, i) => s + Number(i.total_price || 0), 0);
    const grandTotal = hotelTotal + transportTotal + activityTotal + specialTotal;
    const sellingTotal = dto.selling_total ?? grandTotal;

    // Auto-derive booking status from trip dates
    const today = new Date().toISOString().slice(0, 10);
    const startDate = dto.start_date;
    const endDate = dto.end_date || dto.start_date;
    let status = BookingStatus.UPCOMING;
    if (endDate < today) status = BookingStatus.COMPLETED;
    else if (startDate <= today && today <= endDate) status = BookingStatus.ONGOING;

    // ── Transactional write ──
    return manager.transaction(async (tx) => {
      const runSql: (sql: string, params: unknown[]) => Promise<unknown[]> = (sql, params) =>
        tx.query(sql, params);

      // 1. Query — per-tenant 6-digit random `QRY-NNNNNN`.
      const queryNumber = await allocateUniqueReferenceNumber('QRY', tenantId, runSql);
      const queryInsert = await tx.query(
        `INSERT INTO queries (id, tenant_id, query_number, source_id, destination_id, currency_id,
           sales_team, start_date, end_date, adults, children_ages, infants,
           guest_name, guest_email, guest_mobile, guest_alt_phone, notes, stage, is_walk_in,
           created_by, updated_by, created_at, updated_at, is_deleted)
         VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, true, $18, $18, NOW(), NOW(), false)
         RETURNING id`,
        [
          tenantId,
          queryNumber,
          sourceId,
          dto.destination_id,
          currencyId,
          JSON.stringify(dto.sales_team || [userId].filter(Boolean)),
          dto.start_date,
          endDate,
          dto.adults,
          dto.children_ages?.length ? JSON.stringify(dto.children_ages) : null,
          dto.infants ?? 0,
          dto.guest_name || null,
          dto.guest_email || null,
          dto.guest_mobile || null,
          dto.guest_alt_phone || null,
          dto.notes || null,
          QueryStage.CONVERTED,
          userId,
        ],
      );
      const queryId = queryInsert[0].id;

      // 2. Quote
      const quoteInsert = await tx.query(
        `INSERT INTO query_quotes (id, tenant_id, query_id, quote_number, is_selected,
           hotel_total, transport_total, activity_total, special_total, grand_total,
           markup_type, markup_amount, pricing_strategy,
           selling_currency_id, exchange_rate, selling_total,
           created_by, updated_by, created_at, updated_at, is_deleted)
         VALUES (gen_random_uuid(), $1, $2, 1, true, $3, $4, $5, $6, $7,
                 'fixed', 0, 'overall',
                 $8, $9, $10, $11, $11, NOW(), NOW(), false)
         RETURNING id`,
        [
          tenantId,
          queryId,
          hotelTotal,
          transportTotal,
          activityTotal,
          specialTotal,
          grandTotal,
          dto.selling_currency_id || currencyId,
          dto.exchange_rate ?? 1,
          sellingTotal,
          userId,
        ],
      );
      const quoteId = quoteInsert[0].id;

      // 3. Quote children — hotel + its periods + extras.
      for (const h of hotelItems) {
        const stayTotal = computeHotelStayTotal(h.periods, h.extras);
        const [{ id: hotelItemId }] = await tx.query<{ id: string }[]>(
          `INSERT INTO query_quote_hotels (id, tenant_id, quote_id, hotel_id,
             check_in_date, check_out_date, nights, subtotal,
             created_by, updated_by, created_at, updated_at, is_deleted)
           VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $8, NOW(), NOW(), false)
           RETURNING id`,
          [
            tenantId,
            quoteId,
            h.hotel_id,
            h.check_in_date,
            h.check_out_date,
            h.nights,
            stayTotal,
            userId,
          ],
        );
        await insertHotelChildren(tx, tenantId, hotelItemId, userId, h, 'quote');
      }
      for (const t of transportItems) {
        await tx.query(
          `INSERT INTO query_quote_transports (id, tenant_id, quote_id, transport_service_id,
             vehicle_type_id, quantity, unit_price, subtotal, date,
             pickup_time, dropoff_time, notes,
             created_by, updated_by, created_at, updated_at, is_deleted)
           VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8,
             $9, $10, $11, $12, $12, NOW(), NOW(), false)`,
          [
            tenantId,
            quoteId,
            t.transport_service_id,
            t.vehicle_type_id || null,
            t.quantity,
            t.unit_price,
            t.subtotal,
            t.date || startDate,
            t.pickup_time || null,
            t.dropoff_time || null,
            t.notes || null,
            userId,
          ],
        );
      }
      for (const a of activityItems) {
        await tx.query(
          `INSERT INTO query_quote_activities (id, tenant_id, quote_id, activity_id, ticket_id,
             age_group, quantity, unit_price, subtotal, date, slot, notes,
             created_by, updated_by, created_at, updated_at, is_deleted)
           VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $12, NOW(), NOW(), false)`,
          [
            tenantId,
            quoteId,
            a.activity_id,
            a.ticket_id || null,
            a.age_group,
            a.quantity,
            a.unit_price,
            a.subtotal,
            a.date || startDate,
            a.slot || null,
            a.notes || null,
            userId,
          ],
        );
      }
      for (const s of specialItems) {
        await tx.query(
          `INSERT INTO query_quote_specials (id, tenant_id, quote_id, service_name, total_price,
             date, comments, created_by, updated_by, created_at, updated_at, is_deleted)
           VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $7, NOW(), NOW(), false)`,
          [
            tenantId,
            quoteId,
            s.service_name,
            s.total_price,
            s.date || startDate,
            s.comments || null,
            userId,
          ],
        );
      }

      // 4. Booking — walk-in creates query + booking together (1:1), so
      //    reuse the query's 6 digits with the tenant prefix:
      //    QRY-093254 → {prefix}-093254.
      const prefixRows: Array<{ booking_prefix: string | null }> = (await runSql(
        `SELECT booking_prefix FROM tenant_settings
         WHERE tenant_id = $1 AND is_deleted = false LIMIT 1`,
        [tenantId],
      )) as Array<{ booking_prefix: string | null }>;
      const bookingPrefix = prefixRows?.[0]?.booking_prefix || DEFAULT_BOOKING_PREFIX;
      const bookingNumber = await allocateBookingNumberFromQuery(
        queryNumber,
        tenantId,
        runSql,
        bookingPrefix,
      );
      const bookingInsert = await tx.query(
        `INSERT INTO bookings (id, tenant_id, booking_number, query_id, quote_id,
           hotel_total, transport_total, activity_total, special_total, grand_total,
           selling_currency_id, exchange_rate,
           selling_hotel_total, selling_transport_total, selling_activity_total, selling_special_total, selling_total,
           status, notes, is_quick_booking,
           created_by, updated_by, created_at, updated_at, is_deleted)
         VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, true, $19, $19, NOW(), NOW(), false)
         RETURNING id`,
        [
          tenantId,
          bookingNumber,
          queryId,
          quoteId,
          hotelTotal,
          transportTotal,
          activityTotal,
          specialTotal,
          grandTotal,
          dto.selling_currency_id || currencyId,
          dto.exchange_rate ?? 1,
          hotelTotal,
          transportTotal,
          activityTotal,
          specialTotal,
          sellingTotal,
          status,
          dto.notes || null,
          userId,
        ],
      );
      const bookingId = bookingInsert[0].id;

      // 5. Booking children — hotel + its periods + extras (mirror of quote).
      for (const h of hotelItems) {
        const stayTotal = computeHotelStayTotal(h.periods, h.extras);
        const [{ id: hotelItemId }] = await tx.query<{ id: string }[]>(
          `INSERT INTO booking_hotel_items (id, tenant_id, booking_id, hotel_id,
             check_in_date, check_out_date, nights, subtotal,
             created_by, updated_by, created_at, updated_at, is_deleted)
           VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $8, NOW(), NOW(), false)
           RETURNING id`,
          [
            tenantId,
            bookingId,
            h.hotel_id,
            h.check_in_date,
            h.check_out_date,
            h.nights,
            stayTotal,
            userId,
          ],
        );
        await insertHotelChildren(tx, tenantId, hotelItemId, userId, h, 'booking');
      }
      // ORDER MATTERS — activities are persisted BEFORE transports so
      // any transport carrying linked_activity_link_key can resolve to
      // a freshly-minted activity UUID (1:N "dedicated transport for
      // this activity" link). Map link_key → saved id while inserting.
      const linkKeyToActivityId = new Map<string, string>();
      for (const a of activityItems) {
        const newId = (
          await tx.query(
            `INSERT INTO booking_activity_items (id, tenant_id, booking_id, activity_id, ticket_id,
             age_group, quantity, unit_price, subtotal, date, slot, notes,
             created_by, updated_by, created_at, updated_at, is_deleted)
           VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $12, NOW(), NOW(), false)
           RETURNING id`,
            [
              tenantId,
              bookingId,
              a.activity_id,
              a.ticket_id || null,
              a.age_group,
              a.quantity,
              a.unit_price,
              a.subtotal,
              a.date || startDate,
              a.slot || null,
              a.notes || null,
              userId,
            ],
          )
        )?.[0]?.id as string | undefined;
        // link_key is a FE-only transient field for cross-linking transport rows
        // to freshly-inserted activities in the same payload — not on the DTO.
        const aExtra = a as unknown as { link_key?: string };
        if (newId && aExtra.link_key) linkKeyToActivityId.set(String(aExtra.link_key), newId);
      }
      for (const t of transportItems) {
        // PREFER link_key — same rationale as the quote save flow:
        // booking_activity_items get delete-and-replaced on update, so
        // any persisted linked_activity_id the FE remembers points at
        // a just-deleted row. link_key resolves to the freshly-inserted
        // UUID, preventing FK violations.
        const tExtra = t as unknown as {
          linked_activity_link_key?: string;
          linked_activity_id?: string;
        };
        const linkedFromKey = tExtra.linked_activity_link_key
          ? linkKeyToActivityId.get(String(tExtra.linked_activity_link_key)) || null
          : null;
        const linkedActivityId = linkedFromKey || tExtra.linked_activity_id || null;
        await tx.query(
          `INSERT INTO booking_transport_items (id, tenant_id, booking_id, transport_service_id,
             vehicle_type_id, quantity, unit_price, subtotal, date,
             pickup_time, dropoff_time, pickup_address, dropoff_address, notes,
             linked_activity_id,
             created_by, updated_by, created_at, updated_at, is_deleted)
           VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8,
             $9, $10, $11, $12, $13, $14, $15, $15, NOW(), NOW(), false)`,
          [
            tenantId,
            bookingId,
            t.transport_service_id,
            t.vehicle_type_id || null,
            t.quantity,
            t.unit_price,
            t.subtotal,
            t.date || startDate,
            t.pickup_time || null,
            t.dropoff_time || null,
            t.pickup_address || null,
            t.dropoff_address || null,
            t.notes || null,
            linkedActivityId,
            userId,
          ],
        );
      }
      for (const s of specialItems) {
        await tx.query(
          `INSERT INTO booking_special_items (id, tenant_id, booking_id, service_name, total_price,
             date, comments, created_by, updated_by, created_at, updated_at, is_deleted)
           VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $7, NOW(), NOW(), false)`,
          [
            tenantId,
            bookingId,
            s.service_name,
            s.total_price,
            s.date || startDate,
            s.comments || null,
            userId,
          ],
        );
      }

      // Audit (outside the raw SQL — uses its own service)
      await this.auditService.log(ENTITY_TYPE, bookingId, 'created');

      return {
        id: bookingId,
        booking_number: bookingNumber,
        query_id: queryId,
        quote_id: quoteId,
        message: 'Quick booking created successfully',
      };
    });
  }
}
