// data.jsx — production data layer.
//
// Replaces the prototype's mock APARTMENTS/RESERVATION with a live fetch of the
// `getReservationAndRoomDetails` Cloud Function and a transform that shapes the
// Firestore payload into the `stay` view-model the UI consumes.
//
// Payload shape (see functions/getReservationAndRoomDetails.js + getRoomData.js):
//   {
//     reservationDetails: { hotelId, room:[...], roomsData:[{room, roomCode}], <guest/date fields> },
//     roomsData: [ { roomNumber, success, data: { id,name,floor,building,roomType,
//                    property:{...,name_i18n,lat,lon,wifi,password,buildingCode,
//                              googleMaps,waze,roomTypes,languages,services,information},
//                    items:{ Category:{ Type:[ {id,name,name_i18n,iconSvg,category,type,
//                              questions:[{question,answer,question_i18n,answer_i18n}]} ] } } } } ]
//   }
// A reservation references ONE OR MORE rooms → we build one `stay` per room.

const CF_URL = 'https://europe-west1-master-guide-56f7e.cloudfunctions.net/getReservationAndRoomDetails';

// ── i18n field picker ────────────────────────────────────────────────
// Every translatable Firestore field is mirrored as `<field>_i18n: { en, he, ... }`
// with the scalar `<field>` holding the English/default value. Pick the requested
// language, falling back to the scalar (English) when a translation is missing.
function pickField(obj, field, lang) {
  if (!obj) return '';
  const map = obj[field + '_i18n'];
  if (map && typeof map[lang] === 'string' && map[lang].trim()) return map[lang];
  return typeof obj[field] === 'string' ? obj[field] : '';
}

// ── small utilities ──────────────────────────────────────────────────
function stripHtml(html) {
  if (!html) return '';
  const d = document.createElement('div');
  d.innerHTML = html;
  return (d.textContent || '').replace(/\s+/g, ' ').trim();
}

function truncate(s, n) {
  s = s || '';
  return s.length > n ? s.slice(0, n - 1).trimEnd() + '…' : s;
}

// Substitute {{token}} or {token} placeholders in CMS content with reservation
// values. Supported tokens: res, id (alias of res), firstName, pnr, room,
// building. Only known tokens are replaced; anything else is left as-is. So
// admin content like `.../plaka/{id}` or `.../voucher?res={{res}}` renders live.
function applyVars(html, vars) {
  if (!html || !vars) return html || '';
  const sub = (m, key) => (vars[key] != null ? String(vars[key]) : m);
  return html
    .replace(/\{\{\s*(\w+)\s*\}\}/g, sub)   // {{token}}
    .replace(/\{\s*(\w+)\s*\}/g, sub);      // {token} — known tokens only, else unchanged
}

// "master Linzergasse" → "LINZERGASSE" (the large light lockup in the sidebar).
function shortName(name) {
  if (!name) return '';
  return name.replace(/^master\s+/i, '').toUpperCase();
}

// Deterministic brand gradient so a room with no room-type image still gets a
// distinct hero. Picks from a small on-brand palette by hashing the room id.
const GRADIENTS = [
  'linear-gradient(135deg,#2f4a3a 0%,#6f7d5e 55%,#caa86a 100%)',
  'linear-gradient(135deg,#26402f 0%,#8a8a63 55%,#d8b27a 100%)',
  'linear-gradient(135deg,#1f3a2c 0%,#587056 55%,#b79a6a 100%)',
  'linear-gradient(135deg,#33402c 0%,#7d8a5e 55%,#cbb079 100%)',
];
function gradientFor(seed) {
  const s = String(seed || '');
  let h = 0;
  for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) >>> 0;
  return GRADIENTS[h % GRADIENTS.length];
}

// ── Weather (Open-Meteo, no key required) ────────────────────────────
// WMO weather code → { kind (drives the emoji), en/he label }.
const WMO = {
  0: ['clear', 'Clear sky', 'שמיים בהירים'],
  1: ['sunny', 'Mainly clear', 'בהיר בעיקר'],
  2: ['partly', 'Partly cloudy', 'מעונן חלקית'],
  3: ['cloudy', 'Overcast', 'מעונן'],
  45: ['cloudy', 'Fog', 'ערפל'],
  48: ['cloudy', 'Rime fog', 'ערפל מקפיא'],
  51: ['rain', 'Light drizzle', 'טפטוף קל'],
  53: ['rain', 'Drizzle', 'טפטוף'],
  55: ['rain', 'Dense drizzle', 'טפטוף סמיך'],
  61: ['rain', 'Light rain', 'גשם קל'],
  63: ['rain', 'Rain', 'גשם'],
  65: ['rain', 'Heavy rain', 'גשם כבד'],
  71: ['snow', 'Light snow', 'שלג קל'],
  73: ['snow', 'Snow', 'שלג'],
  75: ['snow', 'Heavy snow', 'שלג כבד'],
  80: ['rain', 'Rain showers', 'ממטרים'],
  81: ['rain', 'Rain showers', 'ממטרים'],
  82: ['rain', 'Violent showers', 'ממטרים עזים'],
  95: ['rain', 'Thunderstorm', 'סופת רעמים'],
};

function weatherInfo(code, lang) {
  const e = WMO[code] || WMO[2];
  return { kind: e[0], label: lang === 'he' ? e[2] : e[1] };
}

// Fetch current weather for a coordinate. Returns { tempC, code } or null.
async function fetchWeather(lat, lon) {
  if (lat == null || lon == null || lat === '' || lon === '') return null;
  try {
    const url = `https://api.open-meteo.com/v1/forecast?latitude=${encodeURIComponent(lat)}&longitude=${encodeURIComponent(lon)}&current=temperature_2m,weather_code`;
    const r = await fetch(url);
    if (!r.ok) return null;
    const j = await r.json();
    const c = j.current || {};
    if (c.temperature_2m == null) return null;
    return { tempC: Math.round(c.temperature_2m), code: c.weather_code };
  } catch (e) {
    return null;
  }
}

// ── Reservation-level mapping ────────────────────────────────────────
// ⚠️ FIELD NAMES PENDING A REAL SAMPLE of `reservationDetails` (the check-in API
// response). This reads a broad set of common field names and degrades
// gracefully — any field that isn't found is simply hidden in the UI. Once a
// real sample is available, tighten the field names below.
function num(v) {
  if (v == null || v === '') return null;   // Number(null)/Number('') are 0 — guard them
  const n = Number(v);
  return Number.isFinite(n) ? n : null;
}

function firstDefined(obj, keys) {
  for (const k of keys) {
    if (obj[k] != null && obj[k] !== '') return obj[k];
  }
  return null;
}

function parseDate(v) {
  if (!v) return null;
  const d = new Date(v);
  return isNaN(d.getTime()) ? null : d;
}

function fmtDate(d, lang) {
  if (!d) return '';
  try {
    return new Intl.DateTimeFormat(lang === 'he' ? 'he-IL' : 'en-GB', { month: 'short', day: 'numeric' }).format(d);
  } catch (e) {
    return d.toDateString();
  }
}

// Extract a "HH:MM" clock from a time or datetime string ("15:00",
// "2026-06-29 12:15:00"). Treats midnight ("00:00:00") as "no real time".
function hhmm(v) {
  if (!v || typeof v !== 'string') return '';
  const m = v.match(/(\d{1,2}):(\d{2})/);
  if (!m) return '';
  const t = m[1].padStart(2, '0') + ':' + m[2];
  return t === '00:00' ? '' : t;
}

// Today's date ("YYYY-MM-DD") in the given IANA timezone (falls back to the
// browser's local zone when none is provided).
function todayInTz(timeZone) {
  try {
    return new Intl.DateTimeFormat('en-CA', timeZone ? { timeZone } : {}).format(new Date());
  } catch (e) {
    return new Intl.DateTimeFormat('en-CA').format(new Date());
  }
}

function sameDay(a, b) {
  return a && b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
}

// Normalize a Latin first name to title case ("MERVE"/"merve" → "Merve").
// Hebrew (and other caseless scripts) are left exactly as-is.
function formatFirstName(name) {
  if (!name) return '';
  if (/[֐-׿]/.test(name)) return name; // Hebrew — no case, leave alone
  return name.toLowerCase().replace(/(^|[\s'’-])([a-zà-ɏ])/g, (_, sep, ch) => sep + ch.toUpperCase());
}

function mapReservation(rd, resId, lang) {
  rd = rd || {};

  // Guest name — try several shapes, then split a single full-name string.
  let first = firstDefined(rd, ['firstName', 'guestFirstName', 'givenName']);
  let last = firstDefined(rd, ['lastName', 'guestLastName', 'surname', 'familyName']);
  if (!first) {
    const full = firstDefined(rd, ['guestName', 'guestFullName', 'fullName', 'name']) || '';
    const parts = String(full).trim().split(/\s+/).filter(Boolean);
    if (parts.length) {
      first = parts[0];
      last = last || parts.slice(1).join(' ');
    }
  }

  const arrival = parseDate(firstDefined(rd, ['arrivalDate', 'checkInDate', 'arrival', 'startDate', 'fromDate', 'checkin']));
  const departureRaw = firstDefined(rd, ['departureDate', 'checkOutDate', 'departure', 'endDate', 'toDate', 'checkout']);
  const departure = parseDate(departureRaw);
  const departureDateISO = departureRaw ? String(departureRaw).slice(0, 10) : ''; // "YYYY-MM-DD"

  let nights = num(firstDefined(rd, ['nights', 'numberOfNights', 'los', 'lengthOfStay']));
  if (nights == null && arrival && departure) {
    nights = Math.max(1, Math.round((departure - arrival) / 86400000));
  }

  // Guests = adults + children when given as separate counts; else a single field.
  let guests = num(firstDefined(rd, ['guests', 'numberOfGuests', 'pax', 'occupancy', 'totalGuests']));
  if (guests == null) {
    const a = num(rd.adults);
    if (a != null) guests = a + (num(rd.children) || 0);
  }

  // Check-in time is fixed at 15:00 (the "Available now" case is handled per-room
  // from the clean status, not here). Check-out time comes from the per-room
  // `forcedCheckOut` ("YYYY-MM-DD HH:MM:SS").
  const rr = (Array.isArray(rd.roomsData) && rd.roomsData[0]) || {};
  const checkInTime = '15:00';
  const checkOutTime = hhmm(rr.forcedCheckOut) || hhmm(firstDefined(rd, ['checkOutTime', 'departureTime', 'checkout_time'])) || '11:00';
  const method = firstDefined(rd, ['checkInMethod', 'accessMethod', 'entryMethod']) || '';

  // Confirmation is the PNR.
  const code = rd.pnr || rd.mainConfirmationNo || (Array.isArray(rd.confirmationNo) && rd.confirmationNo[0]) ||
    firstDefined(rd, ['confirmationNumber', 'confirmationNo', 'confirmation', 'reservationId', 'bookingId']) || resId || '';

  return {
    code,
    resId: resId || '',                 // URL `res` param — used for check-out webhook
    breakfast: !!rd.breakfast,          // rate code included breakfast (from the function)
    guest: { first: formatFirstName(first), last: last || '' },
    nights,
    guests,
    departureDateISO,                   // "YYYY-MM-DD" — drives check-out-day gating
    today: arrival ? sameDay(arrival, new Date()) : false,
    checkIn: { date: fmtDate(arrival, lang), time: checkInTime, method, _date: arrival },
    checkOut: { date: fmtDate(departure, lang), time: checkOutTime, _date: departure },
    raw: rd,
  };
}

// ── Content mapping (per room) ───────────────────────────────────────
// Build the "living here" sections from the room's grouped items, plus the
// property's services as a trailing section. Each card carries a Q&A list
// (rich-HTML answers) shown in a modal.
// Fixed order for the "living here" item categories; unknown categories keep
// their relative order after the known ones (services are always appended last).
const CATEGORY_ORDER = ['essential', 'applianc', 'kitchen', 'laundr', 'bathroom', 'bedroom'];
function catRank(name) {
  const s = String(name || '').toLowerCase();
  const i = CATEGORY_ORDER.findIndex((k) => s.includes(k));
  return i === -1 ? 500 : i;
}

function buildSections(items, services, lang, vars) {
  const catSections = [];

  for (const [catName, types] of Object.entries(items || {})) {
    const cards = [];
    let title = catName;
    for (const arr of Object.values(types || {})) {
      for (const item of arr) {
        // All items in a category share its (localized) display name.
        title = pickField(item.category, 'name', lang) || catName;
        const qa = [...(item.questions || [])]
          .sort((a, b) => (a.order ?? 999) - (b.order ?? 999))
          .map((q) => ({ q: pickField(q, 'question', lang), a: applyVars(pickField(q, 'answer', lang), vars) }))
          .filter((x) => (x.q && x.q.trim()) || (x.a && x.a.trim()));
        cards.push({
          id: item.id,
          // The type ("AC", "Coffee Machine") reads cleaner as the card label than
          // the property-prefixed item name ("Plaka AC"); fall back to item name.
          label: pickField(item.type, 'name', lang) || pickField(item, 'name', lang) || item.name || '',
          icon: item.iconSvg || null,
          video: item.video || null,
          qa,
        });
      }
    }
    if (cards.length) catSections.push({ key: catName, title, cards });
  }

  // Order categories (Essentials, Appliances, …); services appended last below.
  catSections.sort((a, b) => catRank(a.key) - catRank(b.key));
  const sections = [...catSections];

  if (services && services.length) {
    const cards = services
      .map((s) => {
        const label = pickField(s, 'displayName', lang) || pickField(s, 'name', lang) || s.name || '';
        const html = applyVars(pickField(s, 'description', lang), vars);
        return { id: 's-' + s.id, label, icon: s.iconSvg || null, qa: html ? [{ q: '', a: html }] : [] };
      })
      .filter((c) => c.label);
    if (cards.length) sections.push({ key: '__services', title: lang === 'he' ? 'שירותים וכללי הבית' : 'Services & house rules', cards, isServices: true });
  }

  return sections;
}

// Local guide ("Around the corner") from the property's `information` content.
function buildGuide(information, lang, vars) {
  return (information || [])
    .map((info) => {
      const name = pickField(info, 'displayName', lang) || pickField(info, 'name', lang) || info.name || '';
      const html = applyVars(pickField(info, 'description', lang), vars);
      // Prefer the explicit (translatable) subheader; fall back to a snippet.
      const subheader = applyVars(pickField(info, 'subheader', lang), vars);
      const meta = subheader || truncate(stripHtml(html), 92);
      return { name, meta, html, tag: 'see' };
    })
    .filter((g) => g.name);
}

// ── Build one `stay` from one room entry ─────────────────────────────
function buildStay(entry, resRooms, reservation, lang) {
  const d = entry.data || {};
  const p = d.property || {};

  const pinEntry = (resRooms || []).find((r) => String(r.room) === String(entry.roomNumber)) || {};
  const doorPin = pinEntry.roomCode || '';

  const roomType = (p.roomTypes || []).find((rt) => rt.code === d.roomType);
  const unitName = (roomType && pickField(roomType, 'name', lang)) || d.roomType || d.name || '';
  const photo = roomType && roomType.imageUrl ? `center/cover no-repeat url("${roomType.imageUrl}")` : gradientFor(d.id);

  const hasCoords = p.lat != null && p.lat !== '' && p.lon != null && p.lon !== '';
  const buildingName = pickField(p, 'name', lang) || p.name || '';

  // Housekeeping status (from the clean-status webhook). When the room has been
  // "Inspected" it's ready now → the UI shows "Available now" instead of 15:00.
  const cleanStatus = entry.cleanStatus || 'Unknown';
  const availableNow = String(cleanStatus).trim().toLowerCase() === 'inspected';

  // Check-out day: is today (property-local, else browser-local) the departure
  // date? Drives the check-out section's visibility.
  const timezone = p.timezone || '';
  const isCheckoutDay = !!reservation.departureDateISO && todayInTz(timezone) === reservation.departureDateISO;

  // Template variables for CMS content (e.g. `.../voucher?res={{res}}`).
  const vars = {
    res: reservation.resId || '',
    id: reservation.resId || '',   // alias of res (for `.../plaka/{id}` links)
    firstName: reservation.guest.first || '',
    pnr: reservation.code || '',
    room: d.name || '',
    building: buildingName,
  };

  // Breakfast: shown as its own section (before "living here") only when the
  // reservation includes breakfast (rate code) AND the property has a service
  // marked as the breakfast section in admin (`isBreakfast`). That service is
  // used as the breakfast content and excluded from the generic services list.
  // Fallback for content authored before the toggle existed: a service whose
  // name contains "breakfast" and hasn't been explicitly unflagged.
  const allServices = Array.isArray(p.services) ? p.services : [];
  const isBreakfastSvc = (s) => s.isAntiBreakfast !== true && (s.isBreakfast === true || (s.isBreakfast == null && /breakfast/i.test(s.name || '')));
  const breakfastSvc = allServices.find(isBreakfastSvc);
  // Services list ("Services & house rules"): exclude the breakfast service; and
  // "anti-breakfast" services (isAntiBreakfast) only appear when the reservation
  // does NOT include breakfast.
  // Luggage storage: a service flagged `isLuggageStorage` in admin becomes its
  // own box on the departure (check-out) day, and is excluded from the generic
  // services list. No flagged service → no box (handled in the component).
  const isLuggageSvc = (s) => s.isLuggageStorage === true;
  const luggageSvc = allServices.find(isLuggageSvc);
  const otherServices = allServices.filter((s) => {
    if (isBreakfastSvc(s)) return false;
    if (isLuggageSvc(s)) return false;
    if (s.isAntiBreakfast === true && reservation.breakfast) return false;
    return true;
  });
  const breakfast = reservation.breakfast && breakfastSvc ? {
    label: pickField(breakfastSvc, 'displayName', lang) || pickField(breakfastSvc, 'name', lang) || 'Breakfast',
    html: applyVars(pickField(breakfastSvc, 'description', lang), vars),
  } : null;
  // Content is pulled from the CMS; the title + sub are fixed (i18n). Only `html`
  // when a luggage service exists, else null → the box is hidden entirely.
  const luggageHtml = luggageSvc ? applyVars(pickField(luggageSvc, 'description', lang), vars) : '';
  const luggage = luggageHtml ? { html: luggageHtml } : null;

  return {
    id: d.id || entry.roomNumber,
    building: buildingName,
    buildingShort: shortName(buildingName),
    unitName,
    room: d.name || entry.roomNumber || '',
    floor: d.floor || '',
    buildingLabel: d.building || '',
    address: p.address || '',
    lat: hasCoords ? p.lat : null,
    lng: hasCoords ? p.lon : null,
    mapHint: '',
    googleMaps: (p.googleMaps || '').trim(),
    waze: (p.waze || '').trim(),
    photo,
    wifi: { network: p.wifi || '', password: p.password || '' },
    buildingCode: p.buildingCode || '',
    doorPin,
    pinSuffix: (p.pinSuffix && String(p.pinSuffix).trim()) || '#',
    weather: null,           // filled asynchronously, keyed by _wkey
    _wkey: hasCoords ? `${p.lat},${p.lon}` : null,
    _coords: hasCoords ? { lat: p.lat, lon: p.lon } : null,
    cleanStatus,
    availableNow,
    timezone,
    isCheckoutDay,
    sectionIcons: p.sectionIcons || {}, // custom per-section icons (admin), else doodle fallback
    breakfast,               // { label, html } when included, else null
    luggage,                 // { html } from a luggage-flagged service, else null
    city: p.city || '',      // not in the property schema → omitted in UI when empty
    country: p.country || '',
    sections: buildSections(d.items, otherServices, lang, vars),
    guide: buildGuide(p.information, lang, vars),
    reservation,
    code: reservation.code,
    checkIn: reservation.checkIn,
    checkOut: reservation.checkOut,
    languages: Array.isArray(p.languages) ? p.languages : [],
  };
}

// Build all stays (one per successful room) for the chosen language.
function buildStays(payload, lang) {
  if (!payload) return [];
  const rooms = (payload.roomsData || []).filter((r) => r && r.success && r.data);
  const resDetails = payload.reservationDetails || {};
  const resRooms = Array.isArray(resDetails.roomsData) ? resDetails.roomsData : [];
  const reservation = mapReservation(resDetails, payload.__resId, lang);
  return rooms.map((entry) => buildStay(entry, resRooms, reservation, lang));
}

// Admin-managed UI-string overrides ({ lang: { key: value } }). Global — travels
// on the property; read from the first successful room.
function extractUiStrings(payload) {
  const r = ((payload && payload.roomsData) || []).find((x) => x && x.success && x.data);
  return (r && r.data.property && r.data.property.uiStrings) || {};
}

// Union of all languages declared across the reservation's properties (+ en).
function availableLanguages(payload) {
  const set = new Set(['en']);
  for (const r of (payload && payload.roomsData) || []) {
    const langs = r && r.data && r.data.property && r.data.property.languages;
    if (Array.isArray(langs)) langs.forEach((l) => l && set.add(l));
  }
  return Array.from(set);
}

// ── Fetch ────────────────────────────────────────────────────────────
// `id` is the authorization token (must match the reservation's operaId server
// side). A 403 throws an error tagged `code: 'UNAUTHORIZED'` so the UI can show
// a distinct "not authorized" screen.
async function loadReservation(resId, id, token) {
  // Guard against a hung/slow endpoint so the loading screen can never spin
  // forever — abort after 25s and surface a clear, retryable error.
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), 25000);
  let r;
  try {
    const q = `resId=${encodeURIComponent(resId)}&id=${encodeURIComponent(id || '')}` +
      (token ? `&token=${encodeURIComponent(token)}` : '');
    r = await fetch(`${CF_URL}?${q}`, { signal: controller.signal });
  } catch (e) {
    throw new Error(controller.signal.aborted
      ? 'The reservation service took too long to respond. Please try again.'
      : 'Could not reach the reservation service. Please check your connection and try again.');
  } finally {
    clearTimeout(timer);
  }
  if (r.status === 401 || r.status === 403) {
    let body = {};
    try { body = await r.json(); } catch (e) {}
    if (body && body.error === 'OUTSIDE_WINDOW') {
      const e = new Error('outside-window');
      e.code = 'OUTSIDE_WINDOW';
      e.reason = body.reason;                 // 'before' | 'after'
      e.availableFrom = body.availableFrom;    // ISO instant
      e.availableUntil = body.availableUntil;  // ISO instant
      e.timeZone = body.timeZone;
      e.past = body.past;                      // departure before today (stay ended)
      e.arrival = body.arrival;                // arrival is today
      e.departure = body.departure;            // departure is today
      throw e;
    }
    const e = new Error("This link isn't valid, please contact our team");
    e.code = 'UNAUTHORIZED';
    throw e;
  }
  if (!r.ok) throw new Error(`Server returned ${r.status}: ${r.statusText}`);
  const payload = await r.json();
  if (!payload.roomsData || !Array.isArray(payload.roomsData) || !payload.roomsData.some((x) => x.success && x.data)) {
    throw new Error('No room content found for this reservation.');
  }
  payload.__resId = resId;
  return payload;
}

Object.assign(window, {
  loadReservation,
  buildStays,
  availableLanguages,
  extractUiStrings,
  fetchWeather,
  weatherInfo,
  pickField,
});
