// OmniSuggest — dropdown that surfaces matches across customers, existing
// bookings, venues, operators, and product types as the agent types.

// ── Fuzzy matching ─────────────────────────────────────────────────────────
// Operators search the bar with loose, typo-prone terms ("notingham brunch",
// "stonegage", "manc rooftop"). A plain .includes() misses all of those, so we
// score each venue across several fields with a tolerant matcher: exact > word
// prefix > substring > bounded edit-distance (typos) > in-order subsequence.

// Coerce a value to a flat list of strings. Tolerates the several shapes a
// venue field can arrive in across data sources: arrays, object maps (e.g.
// productNames is keyed by productId), bare strings, or missing/empty.
function ppValues(x) {
  if (Array.isArray(x)) return x;
  if (x && typeof x === 'object') return Object.values(x);
  return (x != null && x !== '') ? [x] : [];
}

// Lowercase, strip accents, drop punctuation, collapse whitespace.
function ppNorm(s) {
  return (s || '').toString().toLowerCase()
    .normalize('NFD').replace(/[̀-ͯ]/g, '')
    .replace(/[^a-z0-9\s]/g, ' ')
    .replace(/\s+/g, ' ').trim();
}

// Bounded Levenshtein — returns the edit distance, short-circuiting once it is
// guaranteed to exceed `max` (keeps the per-keystroke cost tiny).
function ppLev(a, b, max) {
  const la = a.length, lb = b.length;
  if (Math.abs(la - lb) > max) return max + 1;
  let prev = Array.from({ length: lb + 1 }, (_, i) => i);
  for (let i = 1; i <= la; i++) {
    let best = i;
    const cur = [i];
    for (let j = 1; j <= lb; j++) {
      const cost = a[i - 1] === b[j - 1] ? 0 : 1;
      cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + cost);
      if (cur[j] < best) best = cur[j];
    }
    if (best > max) return max + 1; // whole row already over budget
    prev = cur;
  }
  return prev[lb];
}

// Does every char of `needle` appear, in order, within `word`? Catches dropped
// letters ("ntngham" → "nottingham") that edit-distance alone would reject.
function ppSubseq(needle, word) {
  let i = 0;
  for (let j = 0; j < word.length && i < needle.length; j++) {
    if (word[j] === needle[i]) i++;
  }
  return i === needle.length;
}

// Best match score (0..1) for one query token against one field's text.
function ppFieldScore(token, fieldText) {
  const norm = ppNorm(fieldText);
  if (!norm || !token) return 0;
  let best = 0;
  for (const word of norm.split(' ')) {
    if (!word) continue;
    if (word === token) return 1;                       // exact word — can't beat it
    if (word.startsWith(token)) { best = Math.max(best, 0.9); continue; }
    if (word.includes(token))   { best = Math.max(best, 0.72); continue; }
    // Typo tolerance scales with token length: 1 edit for short, 2 for longer.
    const max = token.length >= 7 ? 2 : token.length >= 4 ? 1 : 0;
    if (max && ppLev(token, word, max) <= max) {
      best = Math.max(best, token.length >= 7 ? 0.62 : 0.58);
      continue;
    }
    if (token.length >= 4 && ppSubseq(token, word)) best = Math.max(best, 0.5);
  }
  // Also try the token against the whole field (handles multi-word names).
  if (best < 0.72 && norm.includes(token)) best = Math.max(best, 0.66);
  return best;
}

// Weighted relevance of a venue to the full query. Every query token must land
// on at least one field (AND semantics) or the venue is dropped (returns 0).
function ppVenueScore(venue, tokens) {
  const op = (typeof operatorById === 'function' && operatorById(venue.operator)) || null;
  const fields = [
    [venue.name, 3],
    [venue.area, 2],
    [venue.city, 2],
    [op && op.name, 2],
    [ppValues(venue.productNames).join(' '), 1.5],
    [venue.description, 1.3],
    [venue.postCode, 1.2],
  ];
  let total = 0;
  for (const token of tokens) {
    let tokenBest = 0;
    for (const [text, weight] of fields) {
      if (!text) continue;
      tokenBest = Math.max(tokenBest, weight * ppFieldScore(token, text));
    }
    if (tokenBest === 0) return 0; // this token matched nothing → not a hit
    total += tokenBest;
  }
  return total;
}

// Rank venues for a raw query string. Returns matches sorted best-first.
function ppFuzzyVenues(rawText, limit) {
  const tokens = ppNorm(rawText).split(' ').filter(Boolean);
  if (!tokens.length) return [];
  const scored = [];
  for (const v of VENUES) {
    const score = ppVenueScore(v, tokens);
    if (score > 0) scored.push({ v, score });
  }
  scored.sort((a, b) => b.score - a.score || a.v.name.localeCompare(b.v.name, 'en-GB'));
  return (typeof limit === 'number' ? scored.slice(0, limit) : scored).map(s => s.v);
}

function buildSuggestions(rawText) {
  const text = (rawText || '').trim().toLowerCase();

  if (!text) {
    // Empty state — surface venues, example natural-language searches, and a
    // light touch of recent bookings to anchor the dropdown.
    const sections = [];
    if (VENUES.length) {
      sections.push({
        kind: 'venues',
        label: 'Venues',
        items: VENUES.slice(0, 4).map(v => ({ kind: 'venue', venue: v })),
      });
    }
    sections.push({
      kind: 'example-searches',
      label: 'Try searching for',
      items: [
        { kind: 'run-search', text: 'best brunches in nottingham',                  label: 'best brunches in nottingham',                  sub: 'Natural-language venue search' },
        { kind: 'run-search', text: 'pubs in central london with a garden',         label: 'pubs in central london with a garden',         sub: 'Natural-language venue search' },
        { kind: 'run-search', text: 'late-night cocktail bars in shoreditch',       label: 'late-night cocktail bars in shoreditch',       sub: 'Natural-language venue search' },
      ],
    });
    sections.push({
      kind: 'recent-bookings',
      label: 'Recent bookings',
      items: SEED_BOOKINGS.slice(0, 2).map(b => ({ kind: 'booking', booking: b })),
    });
    sections.push({
      kind: 'jump-to',
      label: 'Quick jump',
      items: [
        { kind: 'screen', screen: 'bookings', label: 'All bookings', sub: 'Manage existing inventory' },
        { kind: 'screen', screen: 'search',   label: 'New search',   sub: 'Find availability across operators' },
      ],
    });
    return sections;
  }

  const out = [];

  // Customers / bookings — match customer name or booking ID.
  const bookingMatches = SEED_BOOKINGS.filter(b => {
    return b.customer.toLowerCase().includes(text)
        || b.id.toLowerCase().includes(text)
        || b.venue.toLowerCase().includes(text);
  });
  if (bookingMatches.length) {
    out.push({
      kind: 'customer-bookings',
      label: 'Customer bookings',
      hint: bookingMatches.length + ' match' + (bookingMatches.length === 1 ? '' : 'es'),
      items: bookingMatches.slice(0, 4).map(b => ({ kind: 'booking', booking: b })),
      footer: bookingMatches.length > 4 ? { label: 'See all ' + bookingMatches.length + ' bookings', action: { kind: 'bookings-search', text: rawText } } : null,
    });
  }

  // Venues — fuzzy ranked across name, area, city, operator, product, postcode
  // so loose/typo'd terms still surface the right venue.
  const venueMatches = ppFuzzyVenues(rawText, 6);
  if (venueMatches.length) {
    out.push({
      kind: 'venues',
      label: 'Venues',
      hint: venueMatches.length + ' match' + (venueMatches.length === 1 ? '' : 'es'),
      items: venueMatches.map(v => ({ kind: 'venue', venue: v })),
    });
  }

  // Operators
  const operatorMatches = OPERATORS.filter(o => o.name.toLowerCase().includes(text));
  if (operatorMatches.length) {
    out.push({
      kind: 'operators',
      label: 'Operators',
      items: operatorMatches.slice(0, 4).map(o => ({ kind: 'operator', operator: o })),
    });
  }

  // Product types
  const productMatches = PRODUCT_TYPES.filter(p => p.name.toLowerCase().includes(text));
  if (productMatches.length) {
    out.push({
      kind: 'products',
      label: 'Product types',
      items: productMatches.slice(0, 4).map(p => ({ kind: 'product', product: p })),
    });
  }

  // Fallback — always offer a "search inventory for X" affordance even if
  // nothing matched, so the user has a way forward.
  out.push({
    kind: 'actions',
    label: null,
    items: [
      { kind: 'run-search',    label: 'Search inventory for “' + rawText + '”',     sub: 'Find availability matching this term' },
      { kind: 'search-bookings', label: 'Find existing bookings matching “' + rawText + '”', sub: 'Open the bookings list filtered by this term' },
    ],
  });

  return out;
}

function OmniSuggest({ text, onPick, onClose, anchorRef }) {
  const ref = React.useRef(null);
  React.useEffect(() => {
    const onDoc = (e) => {
      if (ref.current && !ref.current.contains(e.target) && anchorRef && !anchorRef.current.contains(e.target)) {
        onClose();
      }
    };
    document.addEventListener('mousedown', onDoc);
    return () => document.removeEventListener('mousedown', onDoc);
  }, [onClose, anchorRef]);

  const sections = buildSuggestions(text);

  return (
    <div className="pp-omni" ref={ref} role="listbox">
      {sections.map((sec, sIdx) => (
        <div key={sec.kind} className="pp-omni-section">
          {sec.label && (
            <div className="pp-omni-section-head">
              <span>{sec.label}</span>
              {sec.hint && <span className="pp-omni-section-hint">{sec.hint}</span>}
            </div>
          )}
          <div className="pp-omni-items">
            {sec.items.map((it, i) => (
              <OmniRow key={sec.kind + ':' + i} item={it} onPick={onPick}/>
            ))}
          </div>
          {sec.footer && (
            <button className="pp-omni-footer" onClick={() => onPick(sec.footer.action)}>
              {sec.footer.label} <IconArrowR size={12}/>
            </button>
          )}
        </div>
      ))}
    </div>
  );
}

function OmniRow({ item, onPick }) {
  if (item.kind === 'booking') {
    const b = item.booking;
    const product = productById(b.product);
    return (
      <button className="pp-omni-row" onClick={() => onPick(item)}>
        <span className="pp-omni-row-glyph pp-omni-glyph--customer">
          <span>{b.customer.split(' ').filter(Boolean).slice(0,2).map(w => w[0]).join('').toUpperCase()}</span>
        </span>
        <span className="pp-omni-row-main">
          <span className="pp-omni-row-title">{b.customer}</span>
          <span className="pp-omni-row-sub">
            <span>{b.venue}</span>
            <span className="pp-omni-row-dot">·</span>
            <span>{product.name}</span>
            <span className="pp-omni-row-dot">·</span>
            <span>{fmtDateShort(b.date)}</span>
          </span>
        </span>
        <span className="pp-omni-row-aside">
          <StatusDot status={b.status}/>
        </span>
      </button>
    );
  }

  if (item.kind === 'venue') {
    const v = item.venue;
    const photo = v.photos && v.photos[0];
    return (
      <button className="pp-omni-row" onClick={() => onPick(item)}>
        {photo ? (
          <img className="pp-omni-row-glyph pp-omni-glyph--photo"
               src={photo}
               alt=""
               loading="lazy"
               onError={(e) => {
                 e.currentTarget.style.display = 'none';
                 const fb = e.currentTarget.nextElementSibling;
                 if (fb) fb.style.display = '';
               }}/>
        ) : null}
        <span className={"pp-omni-row-glyph pp-omni-glyph--cover pp-cover--" + (v.tone || 'sage')}
              style={photo ? { display: 'none' } : null}>
          <span>{v.name.replace(/[^A-Za-z\s]/g, '').split(/\s+/).filter(Boolean).slice(0,2).map(w => w[0]).join('').toUpperCase()}</span>
        </span>
        <span className="pp-omni-row-main">
          <span className="pp-omni-row-title">{v.name}</span>
          <span className="pp-omni-row-sub">
            <span><IconPin size={10}/> {v.city}</span>
            <span className="pp-omni-row-dot">·</span>
            <OperatorTag id={v.operator} size="sm"/>
          </span>
        </span>
        <span className="pp-omni-row-aside"><IconArrowR size={13}/></span>
      </button>
    );
  }

  if (item.kind === 'operator') {
    const o = item.operator;
    return (
      <button className="pp-omni-row" onClick={() => onPick(item)}>
        <span className="pp-omni-row-glyph pp-omni-glyph--operator">
          <IconTag size={14}/>
        </span>
        <span className="pp-omni-row-main">
          <span className="pp-omni-row-title">{o.name}</span>
          <span className="pp-omni-row-sub">{o.venueCount} venues</span>
        </span>
        <span className="pp-omni-row-aside"><IconArrowR size={13}/></span>
      </button>
    );
  }

  if (item.kind === 'product') {
    const p = item.product;
    const G = PRODUCT_GLYPHS[p.id];
    return (
      <button className="pp-omni-row" onClick={() => onPick(item)}>
        <span className="pp-omni-row-glyph pp-omni-glyph--product" style={{ background: 'color-mix(in oklch, ' + p.accent + ' 32%, #fff)' }}>
          <G size={14}/>
        </span>
        <span className="pp-omni-row-main">
          <span className="pp-omni-row-title">{p.name}</span>
          <span className="pp-omni-row-sub">{p.blurb}</span>
        </span>
        <span className="pp-omni-row-aside"><IconArrowR size={13}/></span>
      </button>
    );
  }

  // Generic actions (run-search, search-bookings, screen jumps)
  if (item.kind === 'run-search' || item.kind === 'search-bookings' || item.kind === 'screen') {
    const Ico = item.kind === 'search-bookings' ? IconCal
              : item.kind === 'screen' ? (item.screen === 'bookings' ? IconCal : IconSearch)
              : IconSearch;
    return (
      <button className="pp-omni-row pp-omni-row--action" onClick={() => onPick(item)}>
        <span className="pp-omni-row-glyph pp-omni-glyph--action">
          <Ico size={14}/>
        </span>
        <span className="pp-omni-row-main">
          <span className="pp-omni-row-title">{item.label}</span>
          {item.sub && <span className="pp-omni-row-sub">{item.sub}</span>}
        </span>
        <span className="pp-omni-row-aside">
          <span className="pp-omni-kbd">⏎</span>
        </span>
      </button>
    );
  }

  return null;
}

Object.assign(window, {
  OmniSuggest, buildSuggestions,
  ppValues, ppNorm, ppLev, ppSubseq, ppFieldScore, ppVenueScore, ppFuzzyVenues,
});
