import { Injectable, ConflictException, NotFoundException } from '@nestjs/common';
import { FindOptionsOrder, FindOptionsWhere } from 'typeorm';
import { SupplierRepository } from './repositories/supplier.repository';
import { SupplierEntity } from '../../entities/supplier.entity';
import { CreateSupplierDto, UpdateSupplierDto } from './dto';
import { serviceMessages } from '../../common/constants/messages';
import { LOOKUP_MAX } from '../../common/constants/lookup-limits';
import { MediaService } from '../../common/media/media.service';

const MSG = serviceMessages('Supplier');

@Injectable()
export class SupplierService {
  constructor(
    private readonly repo: SupplierRepository,
    private readonly media: MediaService,
  ) {}

  async findAll(
    query?: { page?: number; limit?: number; search?: string },
    filters?: { destinationId?: string; supplierType?: string },
  ) {
    const { Like } = await import('typeorm');
    const where: FindOptionsWhere<SupplierEntity> = {};
    if (filters?.destinationId) where.destination_id = filters.destinationId;
    // supplier_types is simple-json (serialized array) — use Like to match within JSON string
    if (filters?.supplierType) where.supplier_types = Like(`%"${filters.supplierType}"%`) as never;

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

  async findAllForLookup() {
    return this.repo.find({
      where: { is_active: true },
      // `phone` is included so the booking driver-assignment UI can fire
      // a WhatsApp deep link to the supplier without a second round-trip.
      select: {
        id: true,
        name: true,
        supplier_types: true,
        destination_id: true,
        phone: true,
      },
      order: { name: 'ASC' } as FindOptionsOrder<SupplierEntity>,
      // Cap payload — big tenants can grow past a few thousand
      // suppliers; a 10MB dropdown response chokes both the browser
      // and the mobile network. See LOOKUP_MAX contract.
      take: LOOKUP_MAX,
    });
  }

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

  async create(dto: CreateSupplierDto) {
    const existing = await this.repo.findByNameAndDestination(dto.name, dto.destination_id);
    if (existing) throw new ConflictException(MSG.ALREADY_EXISTS('name'));
    const photoUrl = await this.media.prepareSinglePhoto(this.repo, 'suppliers', dto.photo_url);

    const item = await this.repo.save({
      destination_id: dto.destination_id,
      supplier_types: dto.supplier_types,
      name: dto.name,
      contact_person: dto.contact_person,
      phone: dto.phone,
      email: dto.email || null,
      alt_phone: dto.alt_phone || null,
      photo_url: photoUrl,
      address: dto.address || null,
      // Transport per-type fields
      linked_transport_services: dto.linked_transport_services || null,
      transport_operational_area: dto.transport_operational_area || null,
      transport_operational_hours: dto.transport_operational_hours || null,
      fleet_count: dto.fleet_count ?? null,
      transport_notes: dto.transport_notes || null,
      // Activity per-type fields
      linked_activities: dto.linked_activities || null,
      activity_operational_area: dto.activity_operational_area || null,
      activity_operational_hours: dto.activity_operational_hours || null,
      capacity: dto.capacity ?? null,
      pricing_type: dto.pricing_type || null,
      activity_notes: dto.activity_notes || null,
      is_active: dto.is_active ?? true,
    });

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

  /**
   * Find-or-create a supplier by name+destination. Used by the
   * Movement Dashboard Planner when ops types a free-text supplier
   * name that doesn't exist in the master list — we auto-spin up a
   * minimal record so downstream reports, pricing, driver assignment
   * all reference a real FK rather than an orphan text field.
   *
   * Idempotent: calling twice with the same (name, destination)
   * returns the same id. Safe under concurrent Planner edits.
   *
   * Minimal fields only — name, destination, and the caller-provided
   * supplier_types (so the new row shows up in type-filtered
   * dropdowns). Ops can fill in contact / phone / address later from
   * the Supplier master screen.
   */
  async findOrCreateByName(input: {
    name: string;
    destination_id: string;
    supplier_types?: string[];
  }): Promise<{ id: string; name: string; created: boolean }> {
    const trimmed = input.name.trim();
    if (!trimmed) throw new ConflictException('Supplier name cannot be empty');
    const existing = await this.repo.findByNameAndDestination(trimmed, input.destination_id);
    if (existing) return { id: existing.id, name: existing.name, created: false };
    // NOTE: `supplier_types` is TypeORM `simple-json` (TEXT column
    // with JSON encode/decode). Passing the JS array directly works
    // through `this.repo.save()` (TypeORM serialises it); the trap
    // was when raw SQL seeders passed a JS array without
    // JSON.stringify. Kept as a direct array here since save() is
    // safe.
    const item = await this.repo.save({
      destination_id: input.destination_id,
      supplier_types: input.supplier_types?.length ? input.supplier_types : ['transport'],
      name: trimmed,
      // contact_person is NOT NULL in the DB; placeholder matches the
      // Driver quick-create pattern. Ops can edit later.
      contact_person: 'N/A',
      // Phone also NOT NULL in the constraint set — use an empty
      // string so validation passes. Real number added later.
      phone: '',
      is_active: true,
    });
    return { id: item.id, name: item.name, created: true };
  }

  async update(id: string, dto: UpdateSupplierDto) {
    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 updateData: Partial<SupplierEntity> = {};
    const fields = [
      'destination_id',
      'supplier_types',
      'name',
      'contact_person',
      'phone',
      'email',
      'alt_phone',
      'address',
      'linked_transport_services',
      'transport_operational_area',
      'transport_operational_hours',
      'fleet_count',
      'transport_notes',
      'linked_activities',
      'activity_operational_area',
      'activity_operational_hours',
      'capacity',
      'pricing_type',
      'activity_notes',
      'is_active',
    ];
    for (const field of fields) {
      const value = (dto as Record<string, unknown>)[field];
      if (value !== undefined) {
        (updateData as Record<string, unknown>)[field] = value;
      }
    }
    if (dto.photo_url !== undefined) {
      updateData.photo_url = await this.media.prepareSinglePhoto(
        this.repo,
        'suppliers',
        dto.photo_url,
        item.photo_url,
      );
    }

    await this.repo.update(id, updateData);
    if (updateData.photo_url !== undefined && updateData.photo_url !== item.photo_url) {
      await this.media.trashUrl(item.photo_url);
    }
    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 };
  }
}
