// Editorial layer — v2 "The Luxehotels 100"
// The dataset now carries the editors' real review copy (see properties.jsx);
// this layer slices it for the templates: paragraphs, pull quote, standfirst,
// caveat, plates. No generated prose remains.

const EDITION = '2026';

function getRanked100() { return window.PROPERTIES.filter(p => p.rank <= 100); }
function isRanked(p) { return p.rank <= 100; }

function seedOf(id) {
  let h = 2166136261;
  for (let i = 0; i < id.length; i++) { h ^= id.charCodeAt(i); h = Math.imul(h, 16777619); }
  return Math.abs(h);
}

function sentencesOf(text) { return (text.match(/[^.]+\./g) || [text]).map(s => s.trim()); }

// Split the single review paragraph into 2–3 editorial paragraphs and pick a
// pull quote from the review's own text (a mid-review sentence, not the lead,
// not the critique).
function getReview(p) {
  const sents = sentencesOf(p.review);
  const paras = [];
  if (sents.length <= 4) {
    paras.push(sents.join(' '));
  } else {
    const cut1 = Math.ceil(sents.length * 0.45);
    const cut2 = Math.ceil(sents.length * 0.8);
    paras.push(sents.slice(0, cut1).join(' '));
    if (cut2 > cut1) paras.push(sents.slice(cut1, cut2).join(' '));
    paras.push(sents.slice(cut2).join(' '));
  }
  const candidates = sents.slice(1, -1).filter(s => !/critique/i.test(s) && s.length > 55 && s.length < 190);
  let pullquote = candidates.length
    ? candidates[seedOf(p.id) % candidates.length]
    : sents[sents.length - 1];
  // Rebuttal fragments ("But the design is unforgettable.") stand alone poorly.
  pullquote = pullquote.replace(/^(But|And|Yet) (\w)/, (_, __, c) => c.toUpperCase());
  return { paras: paras.filter(Boolean), pullquote };
}

// Terse critiques ("The critique is little.") carry their follow-on sentence.
function getCaveat(p) {
  const base = p.dontLove || p.caveat;
  if (base.length >= 50) return base;
  const sents = sentencesOf(p.review);
  const i = sents.indexOf(base);
  return i >= 0 && sents[i + 1] ? `${base} ${sents[i + 1]}` : base;
}
function getStandfirst(p) { return p.label; }

// Plates: hero carries the real image (manifest path, tint fallback until the
// file lands in /images); insets remain tint stand-ins. Captions mandatory.
function getPlates(p) {
  const rot = (deg) => p.tint.replace(/\d+deg/, deg + 'deg');
  return [
    { tint: p.tint, src: p.image, alt: p.alt, caption: `${p.name} · ${p.location}`, credit: p.credit || 'Courtesy of the property' },
    { tint: rot(205), caption: `A suite at ${p.name}`, credit: 'Imagery pending' },
    { tint: rot(95), caption: `The table — ${p.standouts.includes('F&B') ? 'a framework standout here' : p.destination}`, credit: 'Imagery pending' },
    { tint: rot(260), caption: `${p.destination}, ${p.region}`, credit: 'Imagery pending' },
  ];
}

// Editor-linked "if you like this, read…": same archetype first, then region.
function getRelated(p, n) {
  const pool = window.PROPERTIES.filter(x => x.id !== p.id);
  const scored = pool.map(x => ({
    x,
    aff: (x.archetype === p.archetype ? 2 : 0) + (x.region === p.region ? 1 : 0),
  }));
  scored.sort((a, b) => b.aff - a.aff || a.x.rank - b.x.rank);
  return scored.slice(0, n || 2).map(s => s.x);
}

Object.assign(window, {
  EDITION, getRanked100, isRanked, getReview, getCaveat, getPlates,
  getStandfirst, getRelated,
});
