import { Injectable, BadRequestException } from '@nestjs/common';
import { DestinationVehicleTypeRepository } from './destination-vehicle-type.repository';

@Injectable()
export class DestinationVehicleTypeService {
  constructor(private readonly repo: DestinationVehicleTypeRepository) {}

  /**
   * List mappings grouped by destination. Returns:
   *   [{ destination_id, destination_name, vehicle_types: [{id, name}] }]
   *
   * Lets the FE render a simple "destination → vehicle types" table without
   * a second roundtrip to resolve names.
   */
  async findAllGrouped() {
    const rows = await this.repo.findAllWithRelations();
    type Grouped = {
      destination_id: string;
      destination_name: string;
      vehicle_types: { id: string; name: string }[];
    };
    const byDest = new Map<string, Grouped>();
    for (const r of rows) {
      if (!byDest.has(r.destination_id)) {
        byDest.set(r.destination_id, {
          destination_id: r.destination_id,
          destination_name: r.destination?.name || '—',
          vehicle_types: [] as { id: string; name: string }[],
        });
      }
      byDest.get(r.destination_id)!.vehicle_types.push({
        id: r.vehicle_type_id,
        name: r.vehicle_type?.name || '—',
      });
    }
    return Array.from(byDest.values()).sort((a, b) =>
      a.destination_name.localeCompare(b.destination_name),
    );
  }

  /**
   * Vehicle types currently mapped to a destination — { id, name } objects
   * sorted by name so the FE can drop the response straight into a select.
   *
   * Used by every vehicle-type dropdown that has a destination in context
   * (QuoteBuilder, BookingItineraryEditor, supplier cost prices, the
   * vehicle list page). Returning `{id, name}[]` rather than just IDs
   * lets the consumer skip a second roundtrip to resolve labels.
   *
   * Empty array when the destination has no mappings configured yet —
   * the FE is expected to render an empty dropdown rather than fall
   * back to the global vehicle-types list (per the agreed UX: "force
   * ops to configure the mapping before transport flows can run").
   */
  async findByDestination(destinationId: string): Promise<{ id: string; name: string }[]> {
    const rows = await this.repo.findByDestination(destinationId);
    return rows
      .map((r) => ({
        id: r.vehicle_type_id,
        name: r.vehicle_type?.name ?? '—',
      }))
      .sort((a, b) => a.name.localeCompare(b.name));
  }

  /**
   * Replace the full set of vehicle-type mappings for a destination.
   * Soft-deletes any rows no longer in the incoming list and inserts
   * new ones. Idempotent — calling with the same payload is a no-op.
   *
   * Side effect: transport_pricing fan-out.
   * Whenever a vehicle type is added or removed from a destination, the
   * change cascades into transport_pricing so the vehicle-type axis on
   * every existing transport_service for that destination stays in
   * sync with the mapping. Specifically:
   *   - Added vehicle type   → INSERT one blank-price PVT pricing row for
   *                            every active transport_service on the
   *                            destination (ON CONFLICT DO NOTHING; safe
   *                            across re-runs).
   *   - Removed vehicle type → SOFT-DELETE every active pricing row for
   *                            (destination, vehicle_type_id) — historic
   *                            rows referenced by old bookings/quotes are
   *                            preserved by the soft-delete model.
   *   - SIC pricing rows (vehicle_type_id IS NULL) are untouched —
   *     they don't have a vehicle-type axis.
   *
   * Body: { destination_id, vehicle_type_ids: string[] }
   */
  async upsertForDestination(payload: { destination_id: string; vehicle_type_ids: string[] }) {
    if (!payload?.destination_id) throw new BadRequestException('destination_id required');
    const target = new Set((payload.vehicle_type_ids || []).filter(Boolean));
    const existing = await this.repo.findByDestination(payload.destination_id);
    const existingMap = new Map(existing.map((r) => [r.vehicle_type_id, r]));

    const addedIds: string[] = [];
    const removedIds: string[] = [];

    // Inserts: in target but not existing
    for (const vtId of target) {
      if (!existingMap.has(vtId)) {
        await this.repo.save({
          destination_id: payload.destination_id,
          vehicle_type_id: vtId,
        });
        addedIds.push(vtId);
      }
    }
    // Soft-deletes: in existing but not target
    for (const [vtId, row] of existingMap) {
      if (!target.has(vtId)) {
        await this.repo.softDelete(row.id);
        removedIds.push(vtId);
      }
    }

    // Fan out into transport_pricing — only if either side actually
    // changed, so a same-payload upsert stays a no-op.
    if (addedIds.length > 0 || removedIds.length > 0) {
      await this.fanOutPricingChanges(payload.destination_id, addedIds, removedIds);
    }

    return {
      destination_id: payload.destination_id,
      vehicle_type_ids: Array.from(target),
      added_count: addedIds.length,
      removed_count: removedIds.length,
    };
  }

  /**
   * Find-or-create a vehicle_type by typed name AND ensure it's
   * mapped to the destination. Called by Movement Chart's inline
   * "type a new cab" flow.
   *
   *   1. Match an existing vehicle_type by case-insensitive name
   *      (tenant-scoped). Reuse the row when found.
   *   2. Otherwise, INSERT a new vehicle_type with sensible defaults
   *      (capacity NULL, ops can fill later via Settings).
   *   3. Ensure destination_vehicle_types has the (destination, type)
   *      mapping — idempotent, won't duplicate. Reuses the existing
   *      upsert path so the pricing cascade fires (blank PVT rows
   *      auto-created for every active transport_service).
   *
   * Returns { id, name } so the FE can store the FK on the row.
   */
  async createPlaceholder(input: {
    name: string;
    destination_id: string;
  }): Promise<{ id: string; name: string; destination_id: string }> {
    const trimmed = (input?.name || '').trim();
    if (!trimmed) throw new BadRequestException('Vehicle type name is required');
    if (!input?.destination_id) throw new BadRequestException('destination_id is required');

    const tenantId = this.repo.currentTenantId();
    const userId = this.repo.currentUserId();
    const manager = this.repo.manager;

    // 1. Find-or-create vehicle_type
    const existing = (await manager.query(
      `SELECT id, name FROM vehicle_types
        WHERE tenant_id = $1
          AND is_deleted = false
          AND LOWER(name) = LOWER($2)
        LIMIT 1`,
      [tenantId, trimmed],
    )) as { id: string; name: string }[];

    let vehicleTypeId: string;
    let vehicleTypeName: string;
    if (existing[0]) {
      vehicleTypeId = existing[0].id;
      vehicleTypeName = existing[0].name;
    } else {
      const created = (await manager.query(
        `INSERT INTO vehicle_types
           (tenant_id, name, is_active, is_deleted, created_by, updated_by, created_at, updated_at)
         VALUES ($1, $2, true, false, $3, $3, NOW(), NOW())
         RETURNING id, name`,
        [tenantId, trimmed, userId],
      )) as { id: string; name: string }[];
      vehicleTypeId = created[0].id;
      vehicleTypeName = created[0].name;
    }

    // 2. Ensure destination_vehicle_types mapping (idempotent via the
    //    existing upsert path so the pricing cascade fires).
    const currentMappings = await this.repo.findByDestination(input.destination_id);
    const currentIds = currentMappings.map((m) => m.vehicle_type_id);
    if (!currentIds.includes(vehicleTypeId)) {
      await this.upsertForDestination({
        destination_id: input.destination_id,
        vehicle_type_ids: [...currentIds, vehicleTypeId],
      });
    }

    return { id: vehicleTypeId, name: vehicleTypeName, destination_id: input.destination_id };
  }

  async removeByDestination(destinationId: string) {
    const rows = await this.repo.findByDestination(destinationId);
    const removedIds = rows.map((r) => r.vehicle_type_id);
    for (const r of rows) {
      await this.repo.softDelete(r.id);
    }
    // Same cascade as upsertForDestination's "removed" branch.
    if (removedIds.length > 0) {
      await this.fanOutPricingChanges(destinationId, [], removedIds);
    }
    return { destination_id: destinationId, removed: rows.length };
  }

  // ──────────────────────────────────────────────────────────────────────
  // Cascade helper — keep transport_pricing in sync with the cab-type
  // mapping for a destination. Called from upsertForDestination and
  // removeByDestination. Uses raw SQL via the underlying manager so we
  // don't pull in TransportPricing-module providers (which would create
  // a module dependency cycle).
  // ──────────────────────────────────────────────────────────────────────
  private async fanOutPricingChanges(
    destinationId: string,
    addedVehicleTypeIds: string[],
    removedVehicleTypeIds: string[],
  ): Promise<void> {
    const tenantId = this.repo.currentTenantId();
    const userId = this.repo.currentUserId();
    const manager = this.repo.manager;

    // ─── Removed vehicle types: soft-delete matching active PVT pricing rows.
    // SIC rows (vehicle_type_id IS NULL) are not affected.
    if (removedVehicleTypeIds.length > 0) {
      await manager.query(
        `UPDATE transport_pricing
            SET is_deleted = TRUE,
                updated_at = NOW(),
                updated_by = $1
          WHERE tenant_id = $2
            AND destination_id = $3
            AND vehicle_type_id = ANY($4::uuid[])
            AND is_deleted = FALSE`,
        [userId, tenantId, destinationId, removedVehicleTypeIds],
      );
    }

    // ─── Added vehicle types: insert one PVT pricing row per
    // (active transport_service × newly added vehicle type), blank prices.
    // ON CONFLICT DO NOTHING against the PVT unique index makes this
    // safe to call repeatedly (any row already active for the same
    // service+vehicle+currency+mode is a no-op).
    if (addedVehicleTypeIds.length > 0) {
      await manager.query(
        `INSERT INTO transport_pricing
           (tenant_id, destination_id, transport_service_id,
            category, currency_id, mode, vehicle_type_id,
            created_by, updated_by)
         SELECT ts.tenant_id,
                ts.destination_id,
                ts.id,
                ts.category,
                d.currency_id,
                'PVT',
                vt_id,
                $1,
                $1
           FROM transport_services ts
           JOIN destinations d ON d.id = ts.destination_id
                              AND d.is_deleted = FALSE
                              AND d.currency_id IS NOT NULL
          CROSS JOIN UNNEST($2::uuid[]) AS vt_id
          WHERE ts.tenant_id = $3
            AND ts.destination_id = $4
            AND ts.is_deleted = FALSE
            AND ts.is_active = TRUE
         -- IDX_transport_pricing_pvt_unique is a PARTIAL unique index
         -- (only over active PVT rows). Postgres requires the same
         -- inference columns + WHERE clause for ON CONFLICT to match.
         ON CONFLICT (tenant_id, transport_service_id, vehicle_type_id, mode, currency_id)
                 WHERE is_deleted = FALSE AND mode = 'PVT'
              DO NOTHING`,
        [userId, addedVehicleTypeIds, tenantId, destinationId],
      );
    }
  }
}
