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_SUPPLIER templates.
 *
 * `subjectId` is a Movement-Dashboard row id — a booking_transport_
 * item OR booking_activity_item OR booking_special_item primary
 * key. The resolver probes each table in order (transport is the
 * most common) so the FE doesn't need to tell us the item type up
 * front. Empty per-row optional fields (return time, remarks, etc.)
 * fall back to '-' so the seeded supplier template renders cleanly
 * even when the row is partially filled.
 *
 * Formatting choices match the stakeholder sample 2026-07-01:
 *  - `date_upper` — "JULY, 02 2026" (uppercase month + zero-padded)
 *  - `guest_name_upper` — uppercase guest name; salutation ("MR.")
 *     stays in the template so ops can drop it easily
 *  - `pax_line` — "03 Adults, 02 Child & Infant" with zero padding
 *  - `pickup_time_12h` / `return_time_12h` — "09:00 AM" / "-"
 *  - `transfer_type` — "PVT" or "SIC" from is_shared
 *  - `trip_coordinator` — first user id in query.sales_team, name +
 *     phone. Empty when unassigned.
 */
@Injectable()
export class MovementSupplierContextResolver {
  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,
  ) {}

  /**
   * Look up the LIVE cash-at-service collection for a given
   * (service_type, item_id) tuple. Read-only — tenant scoped
   * manually since we're injecting the raw repo, not going through
   * TenantAwareRepository (this resolver lives outside the SPC
   * module boundary).
   *
   * Returns the shape the template needs: presence flag, amount and
   * currency (alphabetic code for display), and current status —
   * suitable for {{#if payment_collection_required}} blocks in the
   * seeded supplier / customer templates.
   */
  private async lookupPaymentCollection(
    serviceType: 'TRANSPORT' | 'ACTIVITY' | 'SPECIAL',
    itemId: string,
    tenantId: string,
    itemSupplierId?: string | null,
  ): 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 empty = {
      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: '',
    };
    const itemFk =
      serviceType === 'TRANSPORT'
        ? 'transport_item_id'
        : serviceType === 'ACTIVITY'
          ? 'activity_item_id'
          : 'special_item_id';
    const junctionFk =
      serviceType === 'TRANSPORT'
        ? 'transport_item_supplier_id'
        : serviceType === 'ACTIVITY'
          ? 'activity_item_supplier_id'
          : 'special_item_supplier_id';

    const rows = await this.paymentCollections.find({
      where: {
        tenant_id: tenantId,
        is_deleted: false,
        [itemFk]: itemId,
      } 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 = itemSupplierId
      ? live.find((r) => (r as any)[junctionFk] === itemSupplierId) || null
      : primaryId
        ? live.find((r) => (r as any)[junctionFk] === primaryId) || null
        : null;
    if (!row) return empty;
    // Latest live confirmation drives the "cash collected" branch of
    // the WhatsApp template. Pre-collection ops sends the "please
    // collect X" version; after Confirm, the same button sends the
    // "✅ X received" receipt version — one template, two states.
    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 fromJunction = await this.buildIfJunctionSubject(subjectId, tenantId);
    if (fromJunction) return fromJunction;

    // Try transport → activity → special. Each carries the parent
    // booking + query relations so we can compose the guest/service
    // fields uniformly.
    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 —
        // the join through query is enough for the resolver's needs
        // (we only reference q.destination.name below).
        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 —
        // the join through query is enough for the resolver's needs
        // (we only reference q.destination.name below).
        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 —
        // the join through query is enough for the resolver's needs
        // (we only reference q.destination.name below).
        booking: { query: { destination: true } },
      } as any,
    });
    if (special) return this.buildFromSpecial(special, tenantId);

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

  /** Extra-supplier WhatsApp uses the junction row id as subject so
   *  cash-collection tokens match THAT supplier, not the primary. */
  private async buildIfJunctionSubject(
    subjectId: string,
    tenantId: string,
  ): Promise<Record<string, unknown> | null> {
    const transportJ = (await this.transports.manager.query(
      `SELECT j.id, j.transport_item_id AS item_id,
              COALESCE(m.name, j.supplier_name_text, '') AS supplier_name,
              COALESCE(m.phone, j.supplier_phone_text, '') AS supplier_phone
         FROM booking_transport_item_suppliers j
         LEFT JOIN suppliers m ON m.id = j.supplier_id
        WHERE j.id = $1 AND j.tenant_id = $2 AND j.is_deleted = false`,
      [subjectId, tenantId],
    )) as Array<{ id: string; item_id: string; supplier_name: string; supplier_phone: string }>;
    if (transportJ[0]) {
      const item = await this.transports.findOne({
        where: { id: transportJ[0].item_id, tenant_id: tenantId, is_deleted: false } as any,
        relations: {
          transport_service: true,
          vehicle_type: true,
          supplier: true,
          driver: true,
          booking: { query: { destination: true } },
        } as any,
      });
      if (!item) return null;
      return {
        ...(await this.buildFromTransport(item, tenantId, transportJ[0].id)),
        supplier_name: transportJ[0].supplier_name,
        supplier_phone: transportJ[0].supplier_phone,
      };
    }

    const activityJ = (await this.transports.manager.query(
      `SELECT j.id, j.activity_item_id AS item_id,
              COALESCE(m.name, j.supplier_name_text, '') AS supplier_name,
              COALESCE(m.phone, j.supplier_phone_text, '') AS supplier_phone
         FROM booking_activity_item_suppliers j
         LEFT JOIN suppliers m ON m.id = j.supplier_id
        WHERE j.id = $1 AND j.tenant_id = $2 AND j.is_deleted = false`,
      [subjectId, tenantId],
    )) as Array<{ id: string; item_id: string; supplier_name: string; supplier_phone: string }>;
    if (activityJ[0]) {
      const item = await this.activities.findOne({
        where: { id: activityJ[0].item_id, tenant_id: tenantId, is_deleted: false } as any,
        relations: {
          activity: true,
          ticket: true,
          booking: { query: { destination: true } },
        } as any,
      });
      if (!item) return null;
      const bag = await this.buildFromActivity(item, tenantId, activityJ[0].id);
      return {
        ...bag,
        supplier_name: activityJ[0].supplier_name,
        supplier_phone: activityJ[0].supplier_phone,
      };
    }

    const specialJ = (await this.transports.manager.query(
      `SELECT j.id, j.special_item_id AS item_id,
              COALESCE(m.name, j.supplier_name_text, '') AS supplier_name,
              COALESCE(m.phone, j.supplier_phone_text, '') AS supplier_phone
         FROM booking_special_item_suppliers j
         LEFT JOIN suppliers m ON m.id = j.supplier_id
        WHERE j.id = $1 AND j.tenant_id = $2 AND j.is_deleted = false`,
      [subjectId, tenantId],
    )) as Array<{ id: string; item_id: string; supplier_name: string; supplier_phone: string }>;
    if (specialJ[0]) {
      const item = await this.specials.findOne({
        where: { id: specialJ[0].item_id, tenant_id: tenantId, is_deleted: false } as any,
        relations: { booking: { query: { destination: true } } } as any,
      });
      if (!item) return null;
      const bag = await this.buildFromSpecial(item, tenantId, specialJ[0].id);
      return {
        ...bag,
        supplier_name: specialJ[0].supplier_name,
        supplier_phone: specialJ[0].supplier_phone,
      };
    }
    return null;
  }

  private async buildFromTransport(
    t: BookingTransportItemEntity,
    tenantId: string,
    itemSupplierId?: string | null,
  ): 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,
    );
    const pax = paxLine(q.adults ?? 0, childrenCount(q.children_ages), q.infants ?? 0);
    const route = ts
      ? `${(ts.from_city || '').toUpperCase()} → ${(ts.to_city || '').toUpperCase()}`
      : '';
    const category = (t as any).category ?? ts?.category ?? '';

    // Per-service supplier body lives on the transport_services
    // master. Empty → the wrapper's {{#if service_body}} block
    // collapses cleanly. Same axis on the activity + special
    // branches below.
    const serviceBody = (ts as any)?.supplier_whatsapp_body ?? '';

    const pc = await this.lookupPaymentCollection('TRANSPORT', t.id, tenantId, itemSupplierId);
    const collectionTotal = await this.lookupCollectionTotal('TRANSPORT', t.id, tenantId);

    return {
      // Header
      date_upper: dateUpper((t as any).date),
      // Guest
      guest_name_upper: upper(q.guest_name),
      // Service — an ARRIVAL / DEPARTURE / TRANSFER line. Templates
      // may prefer the raw from_city / to_city / category if they
      // want to hand-format.
      service_upper: composeService(ts, category),
      route_upper: route,
      from_city_upper: upper(ts?.from_city),
      to_city_upper: upper(ts?.to_city),
      category_label: prettyCategory(category),
      // Pax
      pax_line: pax,
      adults_padded: pad2(q.adults ?? 0),
      children_padded: pad2(childrenCount(q.children_ages)),
      infants_padded: pad2(q.infants ?? 0),
      has_children: childrenCount(q.children_ages) > 0,
      has_infants: (q.infants ?? 0) > 0,
      // Locations + timing
      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) || '-',
      // Supplier + driver + trip coordinator — prefer linked row,
      // fall back to the free-text override columns (used when ops
      // types an inline name without a master row).
      supplier_name: supplier?.name ?? (t as any).supplier_name_text ?? '',
      supplier_phone: supplier?.phone ?? '',
      driver_name: driver?.name ?? (t as any).driver_name_text ?? '',
      driver_phone: driver?.phone ?? (t as any).driver_phone_text ?? '',
      trip_coordinator: coordinator,
      // Transfer type
      transfer_type: (t as any).is_shared ? 'SIC' : 'PVT',
      // Vehicle + remarks
      vehicle_type: vt?.name ?? '',
      remarks: (t as any).notes || '-',
      // Booking cross-reference — handy for templates that want to
      // reference the parent booking number.
      booking_number: b?.booking_number ?? '',
      item_kind: 'TRANSPORT',
      service_body: serviceBody,
      greeting_time_of_day: timeOfDay(q?.destination?.timezone),
      footer_line: 'This is an automated message',
      ...pc,
      ...collectionTotal,
    };
  }

  private async buildFromActivity(
    a: BookingActivityItemEntity,
    tenantId: string,
    itemSupplierId?: string | null,
  ): 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, itemSupplierId);
    const collectionTotal = await this.lookupCollectionTotal('ACTIVITY', a.id, tenantId);
    return {
      date_upper: dateUpper((a as any).date),
      guest_name_upper: upper(q.guest_name),
      service_upper: upper((a as any).activity?.name),
      route_upper: '',
      from_city_upper: '',
      to_city_upper: '',
      category_label: 'Activity',
      pax_line: paxLine(q.adults ?? 0, childrenCount(q.children_ages), q.infants ?? 0),
      adults_padded: pad2(q.adults ?? 0),
      children_padded: pad2(childrenCount(q.children_ages)),
      infants_padded: pad2(q.infants ?? 0),
      has_children: childrenCount(q.children_ages) > 0,
      has_infants: (q.infants ?? 0) > 0,
      pickup_location: '',
      drop_location: '',
      pickup_time_12h: (a as any).slot || '-',
      return_time_12h: '-',
      supplier_name: '',
      supplier_phone: '',
      driver_name: '',
      driver_phone: '',
      trip_coordinator: coordinator,
      transfer_type: 'ACTIVITY',
      vehicle_type: '',
      remarks: (a as any).notes || '-',
      booking_number: b?.booking_number ?? '',
      item_kind: 'ACTIVITY',
      service_body: (a as any).activity?.supplier_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,
    itemSupplierId?: string | null,
  ): 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, itemSupplierId);
    const collectionTotal = await this.lookupCollectionTotal('SPECIAL', s.id, tenantId);
    return {
      date_upper: dateUpper((s as any).date),
      guest_name_upper: upper(q.guest_name),
      service_upper: upper((s as any).service_name),
      route_upper: '',
      from_city_upper: '',
      to_city_upper: '',
      category_label: 'Special',
      pax_line: paxLine(q.adults ?? 0, childrenCount(q.children_ages), q.infants ?? 0),
      adults_padded: pad2(q.adults ?? 0),
      children_padded: pad2(childrenCount(q.children_ages)),
      infants_padded: pad2(q.infants ?? 0),
      has_children: childrenCount(q.children_ages) > 0,
      has_infants: (q.infants ?? 0) > 0,
      pickup_location: '',
      drop_location: '',
      pickup_time_12h: '-',
      return_time_12h: '-',
      supplier_name: '',
      supplier_phone: '',
      driver_name: '',
      driver_phone: '',
      trip_coordinator: coordinator,
      transfer_type: 'SPECIAL',
      vehicle_type: '',
      remarks: (s as any).comments || '-',
      booking_number: b?.booking_number ?? '',
      item_kind: 'SPECIAL',
      // Special items have no service master — the free-text
      // service_name IS the whole "master". No dedicated body field
      // yet; ops can hand-edit the copied message before sending
      // for one-off specials.
      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?.toUpperCase() ?? '';
    const phone = u?.phone ?? '';
    // "ANISHAF KHAN - NO. +971 56 406 5031"
    const label =
      name && phone ? `${name} - NO. ${phone}` : name || (phone ? `NO. ${phone}` : '');
    return { name, phone, label };
  }
}

// ── Helpers ──────────────────────────────────────────────────────
function upper(s: unknown): string {
  return typeof s === 'string' ? s.toUpperCase() : '';
}
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'}`);
  // Match the sample format's use of & before the last non-Adult
  // segment when there are 3+ segments: "03 Adults, 02 Child & Infant".
  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 '';
  // Match the FE constants where possible. Camel-case fallback.
  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.
 * See movement-customer-context.resolver.ts for full rationale — the
 * two resolvers share the bucketing (05–11 Morning · 12–17 Afternoon
 * · 18–21 Evening · 22–04 Night) so the wrapper reads the same
 * regardless of which button ops 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}`;
}
