import { Injectable, ConflictException, NotFoundException } from '@nestjs/common';
import { DriverRepository } from './repositories/driver.repository';
import { CreateDriverDto, UpdateDriverDto } from './dto';
import { DriverEntity, DriverStatus } from '../../entities/driver.entity';
import type { FindOptionsWhere } from 'typeorm';
import { serviceMessages } from '../../common/constants/messages';
import { LOOKUP_MAX } from '../../common/constants/lookup-limits';

const MSG = serviceMessages('Driver');

@Injectable()
export class DriverService {
  constructor(private readonly repo: DriverRepository) {}

  async findAll(
    query?: { page?: number; limit?: number; search?: string },
    filters?: { supplierId?: string; status?: string; destinationId?: string },
  ) {
    const where: FindOptionsWhere<DriverEntity> = {};
    if (filters?.supplierId) where.supplier_id = filters.supplierId;
    if (filters?.status) where.status = filters.status as DriverEntity['status'];
    if (filters?.destinationId) where.destination_id = filters.destinationId;

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

  async findBySupplier(supplierId: string) {
    return this.repo.findBySupplier(supplierId);
  }

  async findAllForLookup() {
    return this.repo.find({
      where: { is_active: true },
      select: {
        id: true,
        name: true,
        phone: true,
        supplier_id: true,
        status: true,
      },
      order: { name: 'ASC' },
      take: LOOKUP_MAX,
    });
  }

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

  /**
   * Find-or-create a driver from a quick "sync to masters" flow (used
   * by the booking itinerary editor). Dedupes in this order:
   *   1. Same phone (digits-only comparison) — strongest signal.
   *   2. Same name (case-insensitive) at the same supplier — catches
   *      a retype without phone if ops already logged this driver.
   * Creates a new row only when neither match hits. license_no is
   * filled with an AUTO-<millis> placeholder so the NOT NULL unique
   * index is satisfied; ops can edit the real license in Masters.
   */
  async findOrCreateQuick(input: {
    name: string;
    phone: string;
    supplier_id: string;
    destination_id: string;
  }): Promise<{ id: string; created: boolean }> {
    const name = (input.name || '').trim();
    const phone = (input.phone || '').trim();
    if (!name || !phone || !input.supplier_id || !input.destination_id) {
      throw new ConflictException('name, phone, supplier_id, destination_id required');
    }
    const digits = phone.replace(/\D/g, '');
    if (digits.length < 8) {
      throw new ConflictException('Phone must contain at least 8 digits');
    }

    // Dedupe by phone first (any supplier), then by case-insensitive
    // name within the same supplier.
    const byPhone = await this.repo.findByPhoneDigits(digits);
    if (byPhone) return { id: byPhone.id, created: false };

    const byName = await this.repo.findByNameIForSupplier(name, input.supplier_id);
    if (byName) return { id: byName.id, created: false };

    const created = await this.repo.save({
      destination_id: input.destination_id,
      supplier_id: input.supplier_id,
      name,
      phone,
      // License optional — ops can add it later from Driver master.
      license_no: null,
      status: DriverStatus.AVAILABLE,
      is_active: true,
    });
    return { id: created.id, created: true };
  }

  /**
   * Auto-assign a placeholder driver ("Driver 1", "Driver 2", …) for a
   * supplier — Movement Chart ⚡ Auto. REUSES the lowest-numbered
   * placeholder that is free on `date` (not already on another live
   * transport row that day) and only creates the next number when every
   * existing placeholder is busy. Without `date` it reuses the lowest
   * placeholder outright. Serialised per supplier so concurrent clicks
   * can't mint duplicate names; supplier/destination must belong to the
   * caller's tenant.
   */
  async findOrCreatePlaceholder(input: {
    supplier_id: string;
    destination_id?: string;
    date?: string;
  }): Promise<{ id: string; name: string; created: boolean }> {
    if (!input.supplier_id) {
      throw new ConflictException('supplier_id required');
    }
    if (!(await this.repo.tenantSupplierExists(input.supplier_id))) {
      throw new NotFoundException('Supplier not found');
    }
    if (input.destination_id && !(await this.repo.tenantDestinationExists(input.destination_id))) {
      throw new NotFoundException('Destination not found');
    }

    return this.repo.withSupplierLock(input.supplier_id, async () => {
      const existing = await this.repo.find({
        where: {
          supplier_id: input.supplier_id,
          is_active: true,
        } as FindOptionsWhere<DriverEntity>,
        select: { id: true, name: true, destination_id: true },
      });
      const placeholders = existing
        .map((d) => ({ d, n: Number(/^Driver\s+(\d+)$/i.exec(d.name || '')?.[1] || 0) }))
        .filter((x) => x.n > 0)
        .sort((a, b) => a.n - b.n);

      if (placeholders.length > 0) {
        const busy = input.date
          ? await this.repo.findDriversBusyOn(
              placeholders.map((x) => x.d.id),
              input.date,
            )
          : new Set<string>();
        const free = placeholders.find((x) => !busy.has(x.d.id));
        if (free) return { id: free.d.id, name: free.d.name, created: false };
      }

      // Supplier's own destination first — an arbitrary existing
      // driver's destination is only a last resort.
      const destinationId =
        input.destination_id ||
        (await this.repo.resolveSupplierDestination(input.supplier_id)) ||
        existing.find((d) => d.destination_id)?.destination_id;
      if (!destinationId) {
        throw new ConflictException('destination_id required');
      }
      const taken = new Set<number>();
      for (const d of existing) {
        const m = /^Driver\s+(\d+)$/i.exec(d.name || '');
        if (m) taken.add(Number(m[1]));
      }
      let n = 1;
      while (taken.has(n)) n += 1;
      const name = `Driver ${n}`;
      const created = await this.repo.save({
        destination_id: destinationId,
        supplier_id: input.supplier_id,
        name,
        // Placeholder drivers have no real numbers on file — Movement
        // Chart surfaces the supplier phone when the driver's is empty.
        phone: null,
        license_no: null,
        status: DriverStatus.AVAILABLE,
        is_active: true,
      });
      return { id: created.id, name, created: true };
    });
  }

  /**
   * Find-or-create a driver by typed name at a supplier. Used by the
   * Movement Dashboard Cash Collection modal so ops can type a new
   * driver without a separate master-data screen. Dedupes case-
   * insensitive name at the same supplier.
   */
  async findOrCreateByName(input: {
    name: string;
    supplier_id: string;
    destination_id: string;
  }): Promise<{ id: string; name: string; created: boolean }> {
    const name = (input.name || '').trim();
    if (!name || !input.supplier_id || !input.destination_id) {
      throw new ConflictException('name, supplier_id, destination_id required');
    }
    const existing = await this.repo.findByNameIForSupplier(name, input.supplier_id);
    if (existing) return { id: existing.id, name: existing.name, created: false };

    const created = await this.repo.save({
      destination_id: input.destination_id,
      supplier_id: input.supplier_id,
      name,
      phone: null,
      license_no: null,
      status: DriverStatus.AVAILABLE,
      is_active: true,
    });
    return { id: created.id, name: created.name, created: true };
  }

  async create(dto: CreateDriverDto) {
    // License no is optional — only run the uniqueness check when
    // the DTO actually supplies one. Placeholder / pool drivers save
    // with null license.
    if (dto.license_no) {
      const existing = await this.repo.findByLicenseNo(dto.license_no);
      if (existing) throw new ConflictException(MSG.ALREADY_EXISTS('license number'));
    }

    const item = await this.repo.save({
      destination_id: dto.destination_id,
      supplier_id: dto.supplier_id,
      name: dto.name,
      phone: dto.phone || null,
      email: dto.email || null,
      alt_phone: dto.alt_phone || null,
      license_no: dto.license_no || null,
      license_expiry: dto.license_expiry || null,
      photo_url: dto.photo_url || null,
      address: dto.address || null,
      operational_hours: dto.operational_hours || null,
      status: dto.status || DriverStatus.AVAILABLE,
      notes: dto.notes || null,
      is_active: dto.is_active ?? true,
    });

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

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

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

    // Fields where an empty string from the FE should be treated as
    // "cleared" (NULL), not the literal empty string. Required because
    // the partial unique index on (tenant_id, license_no) treats two
    // empty strings as a duplicate — Postgres' NULL-is-distinct rule
    // saves us once we coerce. Same shape mirrors create() so update
    // behaviour matches.
    const NULLABLE_TEXT_FIELDS = new Set([
      'phone',
      'email',
      'alt_phone',
      'license_no',
      'license_expiry',
      'photo_url',
      'address',
      'operational_hours',
      'notes',
    ]);

    const updateData: Record<string, unknown> = {};
    const fields = [
      'destination_id',
      'supplier_id',
      'name',
      'phone',
      'email',
      'alt_phone',
      'license_no',
      'license_expiry',
      'photo_url',
      'address',
      'operational_hours',
      'status',
      'notes',
      'is_active',
    ] as const;
    const dtoRecord = dto as Record<string, unknown>;
    for (const field of fields) {
      if (dtoRecord[field] === undefined) continue;
      const v = dtoRecord[field];
      // Normalize '' → null on every nullable text field. Without
      // this an empty license_no would collide with another driver
      // who also has license_no='' on the unique index.
      updateData[field] = NULLABLE_TEXT_FIELDS.has(field) && v === '' ? null : v;
    }

    await this.repo.update(id, updateData);
    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 };
  }
}
