import {
  Injectable,
  ConflictException,
  NotFoundException,
  BadRequestException,
  Logger,
} from '@nestjs/common';
import { HotelRepository } from './repositories/hotel.repository';
import { HotelMediaRepository } from './repositories/hotel-media.repository';
import { MediaService } from '../../common/media/media.service';
import { CreateHotelDto, UpdateHotelDto } from './dto';
import { HotelEntity } from '../../entities/hotel.entity';
import { serviceMessages } from '../../common/constants/messages';
import { LOOKUP_MAX } from '../../common/constants/lookup-limits';
import { deriveUniqueShortCode } from '../../common/utils/short-code';
import type { FindOptionsWhere } from 'typeorm';

const MSG = serviceMessages('Hotel');

@Injectable()
export class HotelService {
  private readonly logger = new Logger(HotelService.name);

  constructor(
    private readonly repo: HotelRepository,
    private readonly mediaRepo: HotelMediaRepository,
    private readonly media: MediaService,
  ) {}

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

    return this.repo.findPaginated({
      page: query?.page,
      limit: query?.limit,
      search: query?.search,
      searchColumns: ['name', 'address', 'contact_person'],
      relations: ['destination'],
      order: { name: 'ASC' },
      where,
    });
  }

  async findAllForLookup() {
    return this.repo.find({
      where: { is_active: true },
      select: {
        id: true,
        name: true,
        short_code: true,
        destination_id: true,
        // address feeds the "Hotel pickup fallback" on Quote/Booking
        // transport rows — when a PICKUP-category transport row's
        // pickup_address is blank, the FE falls back to the day's
        // hotel address. pickup_gate is the preferred drop-off gate
        // when present (e.g. "Lobby 2") so we surface both.
        address: true,
        pickup_gate: true,
      },
      order: { name: 'ASC' },
      take: LOOKUP_MAX,
    });
  }

  /**
   * Find-or-create a hotel by typed name + destination. Used by the
   * Quote/Booking form so ops can type a brand-new hotel and have it
   * auto-created on submit. Auto-derives short_code; address +
   * contact_person are placeholder defaults that can be filled in
   * later via the Hotel master page.
   *
   * Safe for any authed tenant user — no settings permission required.
   */
  async createPlaceholder(payload: { name: string; destination_id: string }) {
    const trimmed = (payload?.name || '').trim();
    if (!trimmed) throw new BadRequestException('Hotel name is required');
    if (!payload?.destination_id) {
      throw new BadRequestException('Destination is required to create a hotel');
    }

    // 1. Exact-name match for this destination → return existing.
    const existing = await this.repo.findByNameAndDestination(trimmed, payload.destination_id);
    if (existing)
      return { id: existing.id, name: existing.name, destination_id: existing.destination_id };

    // 2. Derive a unique short_code via the shared helper.
    const shortCode = await deriveUniqueShortCode(trimmed, 'HTL', (code) =>
      this.repo.findByShortCode(code).then(Boolean),
    );

    try {
      const item = await this.repo.save({
        short_code: shortCode,
        name: trimmed,
        address: '(TBC)',
        contact_person: trimmed,
        destination_id: payload.destination_id,
        is_active: true,
      });
      return { id: item.id, name: item.name, destination_id: item.destination_id };
    } catch (err: unknown) {
      const e = err as { message?: string; stack?: string; detail?: string };
      this.logger.error(`createPlaceholder failed for "${trimmed}": ${e?.message}`, e?.stack);
      const msg = e?.detail || e?.message || 'Could not create hotel';
      throw new BadRequestException(`Failed to create hotel — ${msg}`);
    }
  }

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

  async create(dto: CreateHotelDto) {
    const existing = await this.repo.findByNameAndDestination(dto.name, dto.destination_id);
    if (existing) throw new ConflictException(MSG.ALREADY_EXISTS('name'));
    const media = await this.media.prepare(this.mediaRepo, 'hotels', dto.media);

    // short_code is auto-derived from the name and tenant-unique.
    // UI no longer collects it; we tolerate a DTO value for seeders
    // and Excel imports.
    const shortCode =
      dto.short_code ||
      (await deriveUniqueShortCode(dto.name, 'HTL', (code) =>
        this.repo.findByShortCode(code).then(Boolean),
      ));

    const item = await this.repo.save({
      destination_id: dto.destination_id,
      short_code: shortCode,
      name: dto.name,
      address: dto.address?.trim() || '(TBC)',
      location: dto.location || null,
      gps_latitude: dto.gps_latitude || null,
      gps_longitude: dto.gps_longitude || null,
      pickup_gate: dto.pickup_gate || null,
      dropoff_gate: dto.dropoff_gate || null,
      contact_person: dto.contact_person?.trim() || dto.name,
      contact_phone: dto.contact_phone || null,
      special_instructions: dto.special_instructions || null,
      meeting_point: dto.meeting_point || null,
      is_active: dto.is_active ?? true,
    });
    await this.media.sync(this.mediaRepo, item.id, media);

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

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

    if (dto.name || dto.destination_id) {
      const checkName = dto.name || item.name;
      const checkDest = dto.destination_id || item.destination_id;
      if (checkName !== item.name || checkDest !== item.destination_id) {
        const existing = await this.repo.findByNameAndDestination(checkName, checkDest);
        if (existing && existing.id !== id) throw new ConflictException(MSG.ALREADY_EXISTS('name'));
      }
    }
    const media = await this.media.prepare(this.mediaRepo, 'hotels', dto.media, id);

    // short_code is immutable post-create — strip it from the payload
    // so a stale form value can't accidentally rewrite it.
    const updateData: Record<string, unknown> = {};
    const fields = [
      'destination_id',
      'name',
      'address',
      'location',
      'gps_latitude',
      'gps_longitude',
      'pickup_gate',
      'dropoff_gate',
      'contact_person',
      'contact_phone',
      'special_instructions',
      'meeting_point',
      'is_active',
    ] as const;
    const dtoRecord = dto as Record<string, unknown>;
    for (const field of fields) {
      if (dtoRecord[field] !== undefined) updateData[field] = dtoRecord[field];
    }

    await this.repo.update(id, updateData);
    await this.media.sync(this.mediaRepo, id, media);
    return { id, message: MSG.UPDATED };
  }

  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 };
  }
}
