/* Lodestay — status vocabulary for one guest booking places to stay.
   A value not listed here does not exist. If a screen needs one, it is added
   here first, with a tone grouping, a reason, and at least one fixture.
   Anything computed from other fields is NOT a value — the derived flags live
   further down and take their clock as an argument.

   Booking policy constants live here too: they are part of this product's
   vocabulary (the service fee, the city tax, when the balance falls due), and
   nothing outside this folder may read them. */

const STAY_POLICY = {
  /* The fee Lodestay charges the guest, applied in exactly one place
     (metrics.js quoteFor) so no screen multiplies a price of its own. */
  serviceRate: 0.11,
  /* Lisbon's tourist tax: per guest, per night, capped at seven nights. */
  cityTaxPerGuestNightCents: 200,
  cityTaxNightCap: 7,
  /* Book more than this far ahead and half the total is taken now, the rest
     this many days before check-in. */
  splitPaymentFromDays: 21,
  balanceDueDaysBefore: 14,
  /* A balance falling due inside this window is a job for the guest. One
     number: the trip row, the section count and the nav badge all read it. */
  balanceDueSoonDays: 7,
  /* Arrival time is wanted this close to check-in, and not before. */
  arrivalPlanDays: 3,
  /* How long after checkout a review can still be written. */
  reviewWindowDays: 14,
  /* How long another guest's checkout holds a place's dates. */
  holdMinutes: 15,
  /* Nothing below this many reviews carries a rating: a place with two
     opinions has no average worth printing. */
  ratingMinReviews: 3,
};

/* listing.availability — whether this place can be booked for the dates the
   guest is searching. Grouped by the question the search and saved screens
   ask: "can I book it, and if not, is that temporary?"
     success = bookable now
     warning = temporary and somebody else's: another guest is checking out
     neutral = closed for these dates, by the calendar or by the host
   Deliberately absent because they are derived: `new` (no reviews yet),
   `fits` / `over_budget` (the place against the guest's own scope), and
   `expiring` (a hold against the clock). A place carries one availability and
   any number of derived flags. */
const LISTING_AVAILABILITY = [
  { id: "open", label: "Open", tone: "success" },
  { id: "held", label: "On hold", tone: "warning" },
  { id: "booked", label: "Booked", tone: "neutral" },
  { id: "paused", label: "Not taking bookings", tone: "neutral" },
];
/* ids: "open" | "held" | "booked" | "paused" */

/* booking.status — is this stay happening?
   info    = the question is with the host, not with the guest
   success = it is happening
   neutral = closed, whoever closed it. Three values share neutral on purpose:
             once a stay is not happening, why it is not happening belongs in
             the sentence under it, not in the colour. `danger` is reserved for
             a money failure, which is payment.status' job.
   Deliberately absent because derived: `upcoming`, `staying` and `past`
   (the dates against the clock), `needs_review` (checkout against the review
   window), `balance_due` (a scheduled payment against the clock). */
const BOOKING_STATUS = [
  { id: "requested", label: "Waiting on host", tone: "info" },
  { id: "confirmed", label: "Confirmed", tone: "success" },
  { id: "declined", label: "Declined", tone: "neutral" },
  { id: "cancelled", label: "Cancelled", tone: "neutral" },
  { id: "completed", label: "Completed", tone: "neutral" },
];
/* ids: "requested" | "confirmed" | "declined" | "cancelled" | "completed" */

/* payment.status — where one movement of money stands.
   success = it left the card · info = it is going to · danger = it failed,
   which is the one failure event in the product · neutral = it came back. */
const PAYMENT_STATUS = [
  { id: "paid", label: "Paid", tone: "success" },
  { id: "scheduled", label: "Scheduled", tone: "info" },
  { id: "failed", label: "Failed", tone: "danger" },
  { id: "refunded", label: "Refunded", tone: "neutral" },
];
/* ids: "paid" | "scheduled" | "failed" | "refunded" */

/* ── Classification lists — read through labelOf, never reaching a Badge. ── */

/* What kind of place it is, and the one icon it is drawn with everywhere. */
const PLACE_KINDS = [
  { value: "entire_home", label: "Entire home", icon: "house" },
  { value: "apartment", label: "Entire apartment", icon: "building-2" },
  { value: "private_room", label: "Private room", icon: "door-closed" },
  { value: "studio", label: "Studio", icon: "square" },
  { value: "boat", label: "Boat", icon: "sailboat" },
];

/* Lisbon, as a guest picks it apart. Each carries the point the map opens on
   when it is the only neighbourhood in the filter. */
const NEIGHBOURHOODS = [
  { value: "alfama", label: "Alfama", lat: 38.7118, lng: -9.1302 },
  { value: "mouraria", label: "Mouraria", lat: 38.7152, lng: -9.1352 },
  { value: "graca", label: "Graça", lat: 38.7176, lng: -9.1298 },
  { value: "principe_real", label: "Príncipe Real", lat: 38.7166, lng: -9.1487 },
  { value: "bairro_alto", label: "Bairro Alto", lat: 38.7124, lng: -9.1448 },
  { value: "baixa", label: "Baixa", lat: 38.7104, lng: -9.1392 },
  { value: "estrela", label: "Estrela", lat: 38.7136, lng: -9.1608 },
  { value: "alcantara", label: "Alcântara", lat: 38.7051, lng: -9.1769 },
  { value: "belem", label: "Belém", lat: 38.6976, lng: -9.2033 },
];

/* What a place has. The icon is the one it is drawn with in every list. */
const AMENITIES = [
  { value: "wifi", label: "Fast wi-fi", icon: "wifi" },
  { value: "desk", label: "Desk to work at", icon: "laptop" },
  { value: "kitchen", label: "Kitchen", icon: "cooking-pot" },
  { value: "washer", label: "Washing machine", icon: "washing-machine" },
  { value: "aircon", label: "Air conditioning", icon: "air-vent" },
  { value: "terrace", label: "Terrace", icon: "sun" },
  { value: "view", label: "River view", icon: "waves" },
  { value: "lift", label: "Lift", icon: "move-vertical" },
  { value: "pets", label: "Pets welcome", icon: "paw-print" },
  { value: "parking", label: "Parking", icon: "circle-parking" },
];

/* How a booking is made. This decides the primary verb on a place, product
   wide: instant places say Reserve, the others say Request to book. */
const BOOKING_MODES = [
  { value: "instant", label: "Books instantly" },
  { value: "request", label: "Host confirms within 24 hours" },
];

/* What the guest gets back if they cancel, and until when. `refundShare` is
   what is returned once the free window has passed; one helper reads both, so
   the confirm dialog and the trip screen quote the same number. */
const CANCEL_POLICIES = [
  { value: "flexible", label: "Flexible", freeUntilDays: 1, refundShare: 0.5, note: "Free until the day before check-in, then half back." },
  { value: "moderate", label: "Moderate", freeUntilDays: 7, refundShare: 0.5, note: "Free until a week before check-in, then half back." },
  { value: "firm", label: "Firm", freeUntilDays: 30, refundShare: 0, note: "Free until a month before check-in, nothing back after that." },
];

/* One movement of money. `refund` is a movement, not a status. */
const PAYMENT_KINDS = [
  { value: "full", label: "Full payment" },
  { value: "deposit", label: "Deposit" },
  { value: "balance", label: "Balance" },
  { value: "refund", label: "Refund" },
];

/* How the search results are ordered. The label says what it sorts on, so a
   figure beside the list is never confused with the order of it. */
const SORTS = [
  { value: "recommended", label: "Best match for your dates" },
  { value: "price_low", label: "Nightly price, lowest first" },
  { value: "price_high", label: "Nightly price, highest first" },
  { value: "rating", label: "Guest rating, highest first" },
];

/* Why a guest called off a stay. One of these is required to cancel. */
const CANCEL_REASONS = [
  { value: "dates_changed", label: "My dates changed" },
  { value: "found_other", label: "I found somewhere else" },
  { value: "trip_off", label: "The trip is not happening" },
  { value: "host_asked", label: "The host asked me to" },
];

/* Who a stay is for. Read on the checkout form and quoted back in the trip. */
const TRIP_PURPOSES = [
  { value: "holiday", label: "Holiday" },
  { value: "work", label: "Work" },
  { value: "family", label: "Family visit" },
];

/* ── Derived flags. Clock injected, never read from the module. ─────────── */

const LISTING_FLAGS = {
  /* THE act-ability predicate for a place: can this guest book it for the
     dates in the scope bar? The result count, the map pins and the row's own
     button all call this one function. */
  isBookable: (listing) => listing.availability === "open",
  /* Somebody else is at the checkout screen with it. Bookable in a quarter of
     an hour, or not at all — so the control is disabled with its reason, never
     hidden. */
  isHeld: (listing) => listing.availability === "held",
  holdExpired: (listing, now) => listing.availability === "held" && listing.holdUntil <= now,
  /* No average worth printing yet. Derived from the reviews it actually has,
     so the first three reviews change the row with nothing written on it. */
  isNew: (listing) => listing.reviews.length < STAY_POLICY.ratingMinReviews,
  isInstant: (listing) => listing.bookingMode === "instant",
  fitsGuests: (listing, guests) => listing.sleeps >= guests,
  /* The stay is shorter than the host will take. Derived from the scope, not
     stored on the place. */
  meetsMinimumNights: (listing, nights) => nights >= listing.minNights,
};

const BOOKING_FLAGS = {
  isOpen: (booking) => booking.status === "requested" || booking.status === "confirmed",
  isWaitingOnHost: (booking) => booking.status === "requested",
  isUpcoming: (booking, now) => booking.status === "confirmed" && booking.checkIn > now,
  isStaying: (booking, now) => booking.status === "confirmed" && booking.checkIn <= now && booking.checkOut > now,
  isPast: (booking, now) =>
    booking.status === "completed" || booking.status === "cancelled" || booking.status === "declined" ||
    (booking.status === "confirmed" && booking.checkOut <= now),
  hasFailedPayment: (booking) => booking.payments.some((p) => p.status === "failed"),
  scheduledBalance: (booking) => booking.payments.find((p) => p.kind === "balance" && (p.status === "scheduled" || p.status === "failed")) || null,
  isBalanceDueSoon: (booking, now) => {
    const balance = BOOKING_FLAGS.scheduledBalance(booking);
    return !!balance && balance.status === "scheduled" && balance.dueAt - now <= STAY_POLICY.balanceDueSoonDays * 864e5;
  },
  /* The host wants an arrival time, and only once the stay is close. */
  wantsArrivalPlan: (booking, now) =>
    booking.status === "confirmed" && !booking.arrivalPlan && booking.checkIn > now &&
    booking.checkIn - now <= STAY_POLICY.arrivalPlanDays * 864e5,
  isAwaitingReview: (booking, now) =>
    booking.status === "completed" && !booking.review && now - booking.checkOut <= STAY_POLICY.reviewWindowDays * 864e5,
  /* THE act-ability predicate for a trip: something only the guest can do.
     The nav badge, the "Needs you" section count and the row all call this. */
  needsYou: (booking, now) =>
    BOOKING_FLAGS.hasFailedPayment(booking) ||
    BOOKING_FLAGS.isBalanceDueSoon(booking, now) ||
    BOOKING_FLAGS.wantsArrivalPlan(booking, now) ||
    BOOKING_FLAGS.isAwaitingReview(booking, now),
  /* Why, in the words the row prints. Never two reasons at once: money first,
     because it is the one that ends the booking if it is ignored. */
  reasonNeedsYou: (booking, now) => {
    if (BOOKING_FLAGS.hasFailedPayment(booking)) return "Payment failed";
    if (BOOKING_FLAGS.isBalanceDueSoon(booking, now)) return "Balance due";
    if (BOOKING_FLAGS.wantsArrivalPlan(booking, now)) return "Host is waiting for your arrival time";
    if (BOOKING_FLAGS.isAwaitingReview(booking, now)) return "Review your stay";
    return null;
  },
  canCancel: (booking, now) => BOOKING_FLAGS.isOpen(booking) && booking.checkOut > now,
};

const THREAD_FLAGS = {
  /* Unread is about messages from the other side only — the guest's own
     replies are read by definition. */
  unreadCount: (thread) => thread.messages.filter((m) => m.from === "host" && !m.read).length,
  isUnread: (thread) => THREAD_FLAGS.unreadCount(thread) > 0,
  lastMessage: (thread) => thread.messages[thread.messages.length - 1],
};

/* ── Transitions, named once, derived from the current status. ────────────
   One function per entity, called by the row action, the record header and the
   section alike. A transition that does not apply is omitted, never disabled;
   one that exists but the guest cannot take yet is disabled with its reason.
   `primary` marks the record's normal path — a row shows only that one.
   kind: "nav" moves to another screen · "undo" instant + consequential, raises
   an undo toast · "confirm" irreversible, opens a confirm dialog · "form"
   opens a staged dialog. */

function bookingTransitions(booking, now) {
  if (booking.status === "requested") {
    return [
      { id: "message_host", label: "Message host", kind: "nav", primary: true },
      { id: "cancel_request", label: "Withdraw request", kind: "confirm", destructive: true },
    ];
  }
  if (booking.status === "confirmed") {
    const list = [];
    if (BOOKING_FLAGS.hasFailedPayment(booking)) list.push({ id: "retry_payment", label: "Retry payment", kind: "undo", primary: true });
    else if (BOOKING_FLAGS.wantsArrivalPlan(booking, now)) list.push({ id: "share_arrival", label: "Share arrival time", kind: "form", primary: true });
    else if (BOOKING_FLAGS.scheduledBalance(booking)) list.push({ id: "pay_balance", label: "Pay balance now", kind: "undo", primary: true });
    if (BOOKING_FLAGS.isStaying(booking, now)) list.push({ id: "message_host", label: "Message host", kind: "nav", primary: !list.length });
    else list.push({ id: "message_host", label: "Message host", kind: "nav" });
    if (BOOKING_FLAGS.canCancel(booking, now)) list.push({ id: "cancel_booking", label: "Cancel booking", kind: "confirm", destructive: true });
    return list;
  }
  if (booking.status === "completed") {
    return BOOKING_FLAGS.isAwaitingReview(booking, now)
      ? [{ id: "write_review", label: "Write review", kind: "form", primary: true }, { id: "book_again", label: "Book again", kind: "nav" }]
      : [{ id: "book_again", label: "Book again", kind: "nav", primary: true }];
  }
  /* Declined and cancelled: the place is still there, the stay is not. */
  return [{ id: "find_similar", label: "Find similar places", kind: "nav", primary: true }];
}

/* What a guest can do with a place, from its availability and the scope they
   are searching with. The verb is the booking mode's, product-wide. */
function listingTransitions(listing, scope, now) {
  const verb = LISTING_FLAGS.isInstant(listing) ? { id: "reserve", label: "Reserve" } : { id: "request_book", label: "Request to book" };
  if (listing.availability === "open") {
    if (!LISTING_FLAGS.meetsMinimumNights(listing, scope.nights)) {
      return [Object.assign({}, verb, { kind: "nav", primary: true, disabled: true, disabledReason: listing.minNights + " nights minimum — your dates are " + scope.nights + "." })];
    }
    if (!LISTING_FLAGS.fitsGuests(listing, scope.guests)) {
      return [Object.assign({}, verb, { kind: "nav", primary: true, disabled: true, disabledReason: "Sleeps " + listing.sleeps + ", and you are " + scope.guests + "." })];
    }
    return [Object.assign({}, verb, { kind: "nav", primary: true })];
  }
  if (listing.availability === "held") {
    return [Object.assign({}, verb, {
      kind: "nav", primary: true, disabled: true,
      disabledReason: "Another guest is at the checkout screen with these dates. The hold expires at " + formatHoldClock(listing.holdUntil) + ".",
    })];
  }
  /* Booked or paused for these dates: there is nothing to disable, so nothing
     is offered. The screen says what to do instead. */
  return [];
}

/* The one place a hold timestamp becomes a time of day. */
function formatHoldClock(ts) {
  const d = new Date(ts);
  return String(d.getHours()).padStart(2, "0") + ":" + String(d.getMinutes()).padStart(2, "0");
}

/* ── Auth policy. Every number the auth copy promises lives here, so the
   sentence and the countdown beside it cannot disagree (AGENTS §7.5). */
const AUTH_POLICY = {
  passwordMinLength: 8,
  /* Wrong passwords in a row before the account locks, and for how long. */
  signInAttempts: 3,
  lockMinutes: 15,
  /* How long a reset link lives, and how long a signup link lives. */
  resetLinkMinutes: 60,
  verifyLinkHours: 24,
  /* The wait before a resend button comes back. */
  resendCooldownSeconds: 45,
  supportEmail: "help@lodestay.travel",
};

/* Auth's derived flags. A reset link carries an expiry, never an `expired`
   value: whether the screen behind the token is the form or the dead-link
   state is computed from that expiry against the clock. */
const AUTH_FLAGS = {
  linkExpired: (link, now) => !link || link.expiresAt <= now,
  /* The requirements the checklist prints and the submit reads — one array,
     so a password can never pass one and fail the other. */
  passwordRules: (password, email) => [
    { id: "length", label: "At least " + AUTH_POLICY.passwordMinLength + " characters", ok: password.length >= AUTH_POLICY.passwordMinLength },
    { id: "mix", label: "A number or a symbol in it somewhere", ok: /[^A-Za-z]/.test(password) },
    { id: "email", label: "Not your email address", ok: password.length > 0 && password.trim().toLowerCase() !== String(email || "").trim().toLowerCase() },
  ],
  passwordOk: (password, email) => AUTH_FLAGS.passwordRules(password, email).every((rule) => rule.ok),
};

const STATUS_FIELDS = {
  "listing.availability": { entity: "listing", field: "availability", means: "Whether this place can be booked for the dates being searched", values: LISTING_AVAILABILITY },
  "booking.status": { entity: "booking", field: "status", means: "Whether this stay is happening", values: BOOKING_STATUS },
  "payment.status": { entity: "payment", field: "status", means: "Where one movement of money stands", values: PAYMENT_STATUS },
};

/* Coverage — values with no fixture, so nobody assumes they are unused:
   payment.status `refunded` reaches a screen only after a guest cancels a paid
   booking, and booking.status `cancelled` only after that same cancel. Both
   are one click away in the kit. Every other value of every field appears at
   least once in fixtures.js. */

Object.assign(window, {
  STAY_POLICY, AUTH_POLICY, AUTH_FLAGS,
  LISTING_AVAILABILITY, BOOKING_STATUS, PAYMENT_STATUS,
  PLACE_KINDS, NEIGHBOURHOODS, AMENITIES, BOOKING_MODES, CANCEL_POLICIES, PAYMENT_KINDS, SORTS, CANCEL_REASONS, TRIP_PURPOSES,
  LISTING_FLAGS, BOOKING_FLAGS, THREAD_FLAGS,
  bookingTransitions, listingTransitions, formatHoldClock,
  STATUS_FIELDS,
});
