import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ClsService } from 'nestjs-cls';
import { BookingTransportItemEntity } from '../../../entities/booking-transport-item.entity';
import { BookingActivityItemEntity } from '../../../entities/booking-activity-item.entity';
import { BookingSpecialItemEntity } from '../../../entities/booking-special-item.entity';
import { UserEntity } from '../../../entities/user.entity';
import { ServicePaymentCollectionEntity } from '../../../entities/service-payment-collection.entity';

/**
 * Builds the data bag for WHATSAPP/MOVEMENT_CUSTOMER templates.
 *
 * Mirrors MovementSupplierContextResolver in structure — same subject
 * lookup (transport → activity → special), same date/pax/route/time
 * tokens — but emits `service_body` from the master's *customer*
 * WhatsApp column (activities.customer_whatsapp_body,
 * transport_services.customer_whatsapp_body). The two audiences
 * usually want different content ("Wear comfy shoes for dune bashing"
 * → guest; "Waiting time chargeable after 15 min" → supplier), so
 * keeping the columns separate lets ops tune each independently.
 *
 * When the master's customer body is empty, the wrapper template's
 * {{#if service_body}} block collapses. So new services are
 * immediately usable — the wrapper still renders the greeting +
 * pickup line + closer without a broken middle.
 */
@Injectable()
export class MovementCustomerContextResolver {
  constructor(
    @InjectRepository(BookingTransportItemEntity)
    private readonly transports: Repository<BookingTransportItemEntity>,
    @InjectRepository(BookingActivityItemEntity)
    private readonly activities: Repository<BookingActivityItemEntity>,
    @InjectRepository(BookingSpecialItemEntity)
    private readonly specials: Repository<BookingSpecialItemEntity>,
    @InjectRepository(UserEntity)
    private readonly users: Repository<UserEntity>,
    @InjectRepository(ServicePaymentCollectionEntity)
    private readonly paymentCollections: Repository<ServicePaymentCollectionEntity>,
    private readonly cls: ClsService,
  ) {}

  /** Sibling of MovementSupplierContextResolver.lookupPaymentCollection —
   *  same shape so wrapper templates can share the {{#if payment_collection_required}}
   *  block across both audiences. */
  private async lookupPaymentCollection(
    serviceType: 'TRANSPORT' | 'ACTIVITY' | 'SPECIAL',
    itemId: string,
    tenantId: string,
  ): Promise<{
    payment_collection_required: boolean;
    payment_collection_pending: boolean;
    payment_collection_collected: boolean;
    payment_collection_amount: string;
    payment_collection_actual_amount: string;
    payment_collection_currency: string;
    payment_collection_status: string;
  }> {
    const where: Record<string, unknown> = {
      tenant_id: tenantId,
      is_deleted: false,
    };
    if (serviceType === 'TRANSPORT') where.transport_item_id = itemId;
    else if (serviceType === 'ACTIVITY') where.activity_item_id = itemId;
    else where.special_item_id = itemId;

    const rows = await this.paymentCollections.find({
      where: where as any,
      relations: { currency: true, confirmations: true } as any,
      order: { created_at: 'ASC' } as any,
    });
    const live = rows.filter((r) => r.status !== 'CANCELLED');
    const primaryId = await this.primaryJunctionId(serviceType, itemId, tenantId);
    const row =
      primaryId &&
      live.find(
        (r) =>
          (r.transport_item_supplier_id ||
            r.activity_item_supplier_id ||
            r.special_item_supplier_id) === primaryId,
      );
    if (!row) {
      return {
        payment_collection_required: false,
        payment_collection_pending: false,
        payment_collection_collected: false,
        payment_collection_amount: '',
        payment_collection_actual_amount: '',
        payment_collection_currency: '',
        payment_collection_status: '',
      };
    }
    // See supplier resolver for the state-machine rationale — same
    // shape here so the customer wrapper can switch between "please
    // have X ready" and "✅ X received" via nested {{#if}} blocks.
    const latestLive = (row.confirmations || [])
      .filter((c) => !c.voided_at && !c.is_deleted)
      .sort((a, b) => new Date(b.collected_at).getTime() - new Date(a.collected_at).getTime())[0];
    return {
      payment_collection_required: true,
      payment_collection_pending: row.status === 'PENDING',
      payment_collection_collected: row.status === 'COLLECTED',
      payment_collection_amount: formatAmount(row.expected_amount),
      payment_collection_actual_amount: latestLive
        ? formatAmount(latestLive.actual_amount)
        : '',
      payment_collection_currency: (row as any).currency?.code ?? '',
      payment_collection_status: row.status,
    };
  }

  private async primaryJunctionId(
    serviceType: 'TRANSPORT' | 'ACTIVITY' | 'SPECIAL',
    itemId: string,
    tenantId: string,
  ): Promise<string | null> {
    const table =
      serviceType === 'TRANSPORT'
        ? 'booking_transport_item_suppliers'
        : serviceType === 'ACTIVITY'
          ? 'booking_activity_item_suppliers'
          : 'booking_special_item_suppliers';
    const itemCol =
      serviceType === 'TRANSPORT'
        ? 'transport_item_id'
        : serviceType === 'ACTIVITY'
          ? 'activity_item_id'
          : 'special_item_id';
    const rows = (await this.transports.manager.query(
      `SELECT id FROM ${table}
        WHERE "${itemCol}" = $1 AND tenant_id = $2 AND is_deleted = false
        ORDER BY sort_order ASC LIMIT 1`,
      [itemId, tenantId],
    )) as Array<{ id: string }>;
    return rows[0]?.id ?? null;
  }

  private async lookupCollectionTotal(
    serviceType: 'TRANSPORT' | 'ACTIVITY' | 'SPECIAL',
    itemId: string,
    tenantId: string,
  ): Promise<{ collection_total: string }> {
    const col =
      serviceType === 'TRANSPORT'
        ? 'transport_item_id'
        : serviceType === 'ACTIVITY'
          ? 'activity_item_id'
          : 'special_item_id';
    const rows = (await this.transports.manager.query(
      `SELECT
          CASE
            WHEN c.expense_status IS NOT NULL AND c.expense_status <> 'CANCELLED'
              THEN COALESCE(c.expense_expected_amount, 0)
            ELSE 0
          END AS expense,
          COALESCE(c.cash_expected_amount, 0) AS cash,
          cur.code AS currency_code
         FROM booking_cash_collections c
         LEFT JOIN currencies cur ON cur.id = c.currency_id
        WHERE c.tenant_id = $1 AND c.is_deleted = false AND c.${col} = $2
        LIMIT 1`,
      [tenantId, itemId],
    )) as Array<{ expense: string | number; cash: string | number; currency_code: string | null }>;
    const row = rows[0];
    if (!row) return { collection_total: '' };
    const total = Number(row.expense) + Number(row.cash);
    if (!Number.isFinite(total) || total <= 0.005) return { collection_total: '' };
    const code = row.currency_code ? ` ${row.currency_code}` : '';
    return { collection_total: `${total.toFixed(2)}${code}` };
  }

  async build(subjectId: string): Promise<Record<string, unknown>> {
    const tenantId = this.cls.get<string>('tenant_id');

    const transport = await this.transports.findOne({
      where: { id: subjectId, tenant_id: tenantId, is_deleted: false } as any,
      relations: {
        transport_service: true,
        vehicle_type: true,
        supplier: true,
        driver: true,
        // Destination lives on booking.query, not booking directly —
        // same shape as MovementSupplierContextResolver.
        booking: { query: { destination: true } },
      } as any,
    });
    if (transport) return this.buildFromTransport(transport, tenantId);

    const activity = await this.activities.findOne({
      where: { id: subjectId, tenant_id: tenantId, is_deleted: false } as any,
      relations: {
        activity: true,
        ticket: true,
        // Destination lives on booking.query, not booking directly —
        // same shape as MovementSupplierContextResolver.
        booking: { query: { destination: true } },
      } as any,
    });
    if (activity) return this.buildFromActivity(activity, tenantId);

    const special = await this.specials.findOne({
      where: { id: subjectId, tenant_id: tenantId, is_deleted: false } as any,
      relations: {
        // Destination lives on booking.query, not booking directly —
        // same shape as MovementSupplierContextResolver.
        booking: { query: { destination: true } },
      } as any,
    });
    if (special) return this.buildFromSpecial(special, tenantId);

    throw new NotFoundException('Movement dashboard row not found');
  }

  private async buildFromTransport(
    t: BookingTransportItemEntity,
    tenantId: string,
  ): Promise<Record<string, unknown>> {
    const b = (t as any).booking;
    const q = b?.query ?? {};
    const ts = (t as any).transport_service;
    const vt = (t as any).vehicle_type;
    const supplier = (t as any).supplier;
    const driver = (t as any).driver;
    const coordinator = await this.resolveCoordinator(
      q.sales_team,
      b?.created_by ?? q.created_by,
      tenantId,
    );
    // Supplier / driver — prefer the linked row's fields, fall back
    // to the free-text override columns (used when ops types a
    // supplier name inline without a master row).
    const supplierName = supplier?.name ?? (t as any).supplier_name_text ?? '';
    const supplierPhone = supplier?.phone ?? '';
    const driverName = driver?.name ?? (t as any).driver_name_text ?? '';
    const driverPhone = driver?.phone ?? (t as any).driver_phone_text ?? '';
    const pc = await this.lookupPaymentCollection('TRANSPORT', t.id, tenantId);
    const collectionTotal = await this.lookupCollectionTotal('TRANSPORT', t.id, tenantId);
    return {
      date_upper: dateUpper((t as any).date),
      guest_name_upper: upper(q.guest_name),
      guest_name_titled: titled(q.guest_name),
      service_upper: composeService(ts, (t as any).category ?? ts?.category ?? ''),
      route_upper: ts
        ? `${(ts.from_city || '').toUpperCase()} → ${(ts.to_city || '').toUpperCase()}`
        : '',
      from_city_upper: upper(ts?.from_city),
      to_city_upper: upper(ts?.to_city),
      category_label: prettyCategory((t as any).category ?? ts?.category ?? ''),
      pax_line: paxLine(q.adults ?? 0, childrenCount(q.children_ages), q.infants ?? 0),
      pickup_location: (t as any).pickup_address || '',
      drop_location: (t as any).dropoff_address || '',
      pickup_time_12h: to12h((t as any).pickup_time) || '-',
      return_time_12h: to12h((t as any).dropoff_time) || '-',
      trip_coordinator: coordinator,
      transfer_type: (t as any).is_shared ? 'SIC' : 'PVT',
      vehicle_type: vt?.name ?? '',
      supplier_name: supplierName,
      supplier_phone: supplierPhone,
      driver_name: driverName,
      driver_phone: driverPhone,
      remarks: (t as any).notes || '',
      booking_number: b?.booking_number ?? '',
      item_kind: 'TRANSPORT',
      service_body: (ts as any)?.customer_whatsapp_body ?? '',
      greeting_time_of_day: timeOfDay(q?.destination?.timezone),
      footer_line: 'This is an automated message',
      ...pc,
      ...collectionTotal,
    };
  }

  private async buildFromActivity(
    a: BookingActivityItemEntity,
    tenantId: string,
  ): Promise<Record<string, unknown>> {
    const b = (a as any).booking;
    const q = b?.query ?? {};
    const coordinator = await this.resolveCoordinator(
      q.sales_team,
      b?.created_by ?? q.created_by,
      tenantId,
    );
    const pc = await this.lookupPaymentCollection('ACTIVITY', a.id, tenantId);
    const collectionTotal = await this.lookupCollectionTotal('ACTIVITY', a.id, tenantId);
    return {
      date_upper: dateUpper((a as any).date),
      guest_name_upper: upper(q.guest_name),
      guest_name_titled: titled(q.guest_name),
      service_upper: upper((a as any).activity?.name),
      pax_line: paxLine(q.adults ?? 0, childrenCount(q.children_ages), q.infants ?? 0),
      pickup_time_12h: (a as any).slot || '-',
      trip_coordinator: coordinator,
      supplier_name: '',
      supplier_phone: '',
      driver_name: '',
      driver_phone: '',
      remarks: (a as any).notes || '',
      booking_number: b?.booking_number ?? '',
      item_kind: 'ACTIVITY',
      service_body: (a as any).activity?.customer_whatsapp_body ?? '',
      greeting_time_of_day: timeOfDay(q?.destination?.timezone),
      footer_line: 'This is an automated message',
      ...pc,
      ...collectionTotal,
    };
  }

  private async buildFromSpecial(
    s: BookingSpecialItemEntity,
    tenantId: string,
  ): Promise<Record<string, unknown>> {
    const b = (s as any).booking;
    const q = b?.query ?? {};
    const coordinator = await this.resolveCoordinator(
      q.sales_team,
      b?.created_by ?? q.created_by,
      tenantId,
    );
    const pc = await this.lookupPaymentCollection('SPECIAL', s.id, tenantId);
    const collectionTotal = await this.lookupCollectionTotal('SPECIAL', s.id, tenantId);
    return {
      date_upper: dateUpper((s as any).date),
      guest_name_upper: upper(q.guest_name),
      guest_name_titled: titled(q.guest_name),
      service_upper: upper((s as any).service_name),
      pax_line: paxLine(q.adults ?? 0, childrenCount(q.children_ages), q.infants ?? 0),
      pickup_time_12h: '-',
      trip_coordinator: coordinator,
      supplier_name: '',
      supplier_phone: '',
      driver_name: '',
      driver_phone: '',
      remarks: (s as any).comments || '',
      booking_number: b?.booking_number ?? '',
      item_kind: 'SPECIAL',
      service_body: '',
      greeting_time_of_day: timeOfDay(q?.destination?.timezone),
      footer_line: 'This is an automated message',
      ...pc,
      ...collectionTotal,
    };
  }

  private async resolveCoordinator(
    salesTeam: unknown,
    fallbackUserId: string | null,
    tenantId: string,
  ): Promise<{ name: string; phone: string; label: string }> {
    const uid =
      (Array.isArray(salesTeam) && (salesTeam[0] as string | undefined)) ||
      fallbackUserId ||
      null;
    if (!uid) return { name: '', phone: '', label: '' };
    const u = await this.users.findOne({
      where: { id: uid, tenant_id: tenantId } as any,
      // TypeORM v1 dropped the string-array select syntax — use the
      // object form: { field: true }.
      select: { id: true, name: true, phone: true } as any,
    });
    const name = u?.name ?? '';
    const phone = u?.phone ?? '';
    const label =
      name && phone ? `${name} - ${phone}` : name || (phone ? phone : '');
    return { name, phone, label };
  }
}

// ── Helpers (shared shape with the supplier resolver) ────────────
function upper(s: unknown): string {
  return typeof s === 'string' ? s.toUpperCase() : '';
}
function titled(s: unknown): string {
  if (typeof s !== 'string' || !s.trim()) return '';
  const parts = s.trim().toLowerCase().split(/\s+/);
  return parts.map((p) => (p ? p[0].toUpperCase() + p.slice(1) : p)).join(' ');
}
function pad2(n: number): string {
  return n.toString().padStart(2, '0');
}
function childrenCount(arr: unknown): number {
  return Array.isArray(arr) ? arr.length : 0;
}
function paxLine(adults: number, children: number, infants: number): string {
  const parts: string[] = [];
  parts.push(`${pad2(adults)} ${adults === 1 ? 'Adult' : 'Adults'}`);
  if (children > 0) parts.push(`${pad2(children)} ${children === 1 ? 'Child' : 'Children'}`);
  if (infants > 0) parts.push(`${pad2(infants)} ${infants === 1 ? 'Infant' : 'Infants'}`);
  if (parts.length === 3) return `${parts[0]}, ${parts[1]} & ${parts[2]}`;
  return parts.join(', ');
}
function composeService(ts: any, category: string): string {
  if (!ts) return '';
  const from = upper(ts.from_city);
  const to = upper(ts.to_city);
  const cat = prettyCategory(category).toUpperCase();
  if (from && to && cat) return `${from} → ${to} ${cat}`;
  if (from && to) return `${from} → ${to}`;
  return `${from}${to ? ' ' + to : ''}`.trim();
}
function prettyCategory(cat: string): string {
  if (!cat) return '';
  const map: Record<string, string> = {
    PICKUP: 'Arrival',
    DROPOFF: 'Departure',
    ARRIVAL: 'Arrival',
    DEPARTURE: 'Departure',
    TRANSFER: 'Transfer',
    ROUND_TRIP: 'Round Trip',
    ONE_WAY: 'One Way',
    TWO_WAY: 'Two Way',
    DISPOSAL: 'Disposal',
    HALF_DAY: 'Half Day',
    FULL_DAY: 'Full Day',
  };
  return map[cat.toUpperCase()] ?? cat;
}
function dateUpper(d: string | null | undefined): string {
  if (!d) return '';
  const [y, m, day] = d.split('T')[0].split('-');
  const months = [
    'JANUARY','FEBRUARY','MARCH','APRIL','MAY','JUNE',
    'JULY','AUGUST','SEPTEMBER','OCTOBER','NOVEMBER','DECEMBER',
  ];
  return `${months[parseInt(m) - 1] ?? m}, ${pad2(parseInt(day))} ${y}`;
}
/**
 * Time-of-day greeting in the destination's local timezone.
 * Falls back to server time when the destination has no timezone
 * (or the tz string is invalid — Intl throws). Buckets:
 *   05–11 → Morning · 12–17 → Afternoon · 18–21 → Evening ·
 *   22–04 → Night. Ops uses this in the wrapper's greeting line
 *   ("Good {{greeting_time_of_day}}, Sir/Ma'am") so the message
 *   reads correctly regardless of when Copy WA is clicked.
 */
function timeOfDay(tz: string | null | undefined): string {
  let hour: number;
  try {
    const hhmm = new Intl.DateTimeFormat('en-US', {
      timeZone: tz || 'UTC',
      hour: 'numeric',
      hour12: false,
    }).format(new Date());
    hour = parseInt(hhmm, 10);
  } catch {
    hour = new Date().getUTCHours();
  }
  if (hour >= 5 && hour < 12) return 'Morning';
  if (hour >= 12 && hour < 18) return 'Afternoon';
  if (hour >= 18 && hour < 22) return 'Evening';
  return 'Night';
}

function formatAmount(n: unknown): string {
  const v = Number(n);
  if (!Number.isFinite(v)) return '';
  return v.toFixed(2);
}
function to12h(hhmm: string | null | undefined): string {
  if (!hhmm) return '';
  const m = /^(\d{1,2}):(\d{2})/.exec(hhmm);
  if (!m) return hhmm;
  let h = Number(m[1]);
  const mm = m[2];
  const period = h >= 12 ? 'PM' : 'AM';
  h = h % 12 || 12;
  return `${pad2(h)}:${mm} ${period}`;
}
