/**
 * Booking approval-trigger detectors — shared between:
 *   - ChangeRequestService.create()  (label upgrade for Sales-submitted CRs)
 *   - BookingItineraryService.update()  (auto-CR on approver saves)
 *
 * Three rules per stakeholder spec (2026-07-02):
 *   1. Same-day booking   — start_date == today in destination tz
 *   2. Next-day booking   — start_date == today + 1 day
 *   3. Post-confirm edit  — line-item present on update DTO AND booking
 *                           status ∈ {confirmed, ongoing, completed}
 *
 * Users with `booking_approval_bypass:view` (Admin by default) skip
 * all three. The bypass check lives at the call site — the helpers
 * here are pure detection.
 */

import { ChangeRequestKind } from '../../entities/booking-change-request.entity';
import { BookingStatus } from '../../entities/booking.entity';

export interface ApprovalTrigger {
  kind: ChangeRequestKind;
  detail: string;
}

/**
 * Return the destination-local calendar date in YYYY-MM-DD as a
 * fresh Date object. Uses Intl.DateTimeFormat with the tz so DST
 * shifts don't drift the day boundary.
 *
 * Falls back to UTC when tz is null/empty/invalid — better than
 * throwing, since a tenant with a mis-configured destination
 * shouldn't crash the save.
 */
function todayInTz(tz: string | null | undefined): string {
  try {
    const parts = new Intl.DateTimeFormat('en-CA', {
      timeZone: tz || 'UTC',
      year: 'numeric',
      month: '2-digit',
      day: '2-digit',
    }).formatToParts(new Date());
    const y = parts.find((p) => p.type === 'year')?.value ?? '1970';
    const m = parts.find((p) => p.type === 'month')?.value ?? '01';
    const d = parts.find((p) => p.type === 'day')?.value ?? '01';
    return `${y}-${m}-${d}`;
  } catch {
    return new Date().toISOString().slice(0, 10);
  }
}

function addDaysIso(iso: string, days: number): string {
  // iso = YYYY-MM-DD; convert to UTC midnight then add days.
  const [y, m, d] = iso.split('-').map(Number);
  const dt = new Date(Date.UTC(y, (m || 1) - 1, d || 1));
  dt.setUTCDate(dt.getUTCDate() + days);
  return dt.toISOString().slice(0, 10);
}

/** True when booking.start_date is today in the destination's tz. */
export function detectSameDayBooking(
  startDate: string | null | undefined,
  destTz: string | null | undefined,
): ApprovalTrigger | null {
  if (!startDate) return null;
  const start = startDate.slice(0, 10);
  const today = todayInTz(destTz);
  if (start === today) {
    return {
      kind: ChangeRequestKind.SAME_DAY_BOOKING,
      detail: `Trip starts today (${today}) in the destination's timezone.`,
    };
  }
  return null;
}

/** True when booking.start_date is exactly one day after today in dest tz. */
export function detectNextDayBooking(
  startDate: string | null | undefined,
  destTz: string | null | undefined,
): ApprovalTrigger | null {
  if (!startDate) return null;
  const start = startDate.slice(0, 10);
  const today = todayInTz(destTz);
  const tomorrow = addDaysIso(today, 1);
  if (start === tomorrow) {
    return {
      kind: ChangeRequestKind.NEXT_DAY_BOOKING,
      detail: `Trip starts tomorrow (${tomorrow}) in the destination's timezone.`,
    };
  }
  return null;
}

// "Post-confirm" in this codebase means "post-createFromQuery" —
// every persisted booking. There's no CONFIRMED status in the
// lifecycle (upcoming → ongoing → completed / cancelled). Firing
// on all non-cancelled statuses matches the stakeholder rule of
// "any change to an existing booking quotation".
const POST_CONFIRM_STATUSES: BookingStatus[] = [
  BookingStatus.UPCOMING,
  BookingStatus.ONGOING,
  BookingStatus.COMPLETED,
];

interface UpdateDtoLike {
  transport_items?: unknown;
  activity_items?: unknown;
  hotel_items?: unknown;
  special_items?: unknown;
}

/**
 * Fires when a POST-confirmation booking (status confirmed/ongoing/
 * completed) receives a payload containing ANY of the four item
 * arrays. Since the itinerary editor always sends the full item
 * list on save, this is a proxy for "user pressed Save on the
 * itinerary editor" — matches stakeholder intent ("any change to
 * line items pricing anything").
 *
 * Does NOT compare before/after — every save counts. If ops needs
 * finer granularity later (e.g., only fire on price drift), swap
 * this for a hashed-line-items comparison.
 */
export function detectPostConfirmEdit(
  currentStatus: BookingStatus | string | null | undefined,
  dto: UpdateDtoLike,
): ApprovalTrigger | null {
  if (!currentStatus) return null;
  if (!POST_CONFIRM_STATUSES.includes(currentStatus as BookingStatus)) return null;
  const hasItems =
    Array.isArray(dto.transport_items) ||
    Array.isArray(dto.activity_items) ||
    Array.isArray(dto.hotel_items) ||
    Array.isArray(dto.special_items);
  if (!hasItems) return null;
  return {
    kind: ChangeRequestKind.POST_CONFIRM_EDIT,
    detail: `Booking is already ${String(currentStatus)} — line-item edit needs manager approval.`,
  };
}

/**
 * Run all three detectors and return every match. Empty array means
 * no triggers fired. Precedence for kind promotion sits in
 * change-request.service.ts (KIND_PRIORITY).
 */
export function detectBookingApprovalTriggers(input: {
  startDate: string | null | undefined;
  destTz: string | null | undefined;
  currentStatus?: BookingStatus | string | null;
  dto?: UpdateDtoLike;
  isUpdate: boolean;
}): ApprovalTrigger[] {
  const out: ApprovalTrigger[] = [];
  const same = detectSameDayBooking(input.startDate, input.destTz);
  if (same) out.push(same);
  const next = detectNextDayBooking(input.startDate, input.destTz);
  if (next) out.push(next);
  if (input.isUpdate) {
    const post = detectPostConfirmEdit(input.currentStatus, input.dto || {});
    if (post) out.push(post);
  }
  return out;
}
