// ReviewReplies.jsx — "Review Replies" page (Admin Portal).
//
// Drop / paste / pick review screenshots → the `review-reply` Edge Function
// (Claude vision) reads each one (a screenshot may hold several stacked
// reviews), extracts platform / name / rating / text, and writes an on-brand
// reply draft. Rows persist in `review_replies` and arrive here via the
// useSupaList realtime subscription. Staff can edit the draft inline (autosave
// on blur), Copy, Regenerate (history-aware), Delete, and mark Posted. Posted
// cards collapse to their header row (chevron re-opens them).
//
// Reviews are searchable + platform-filterable. Repeat customers (same legit
// name) get a "Repeat customer" badge — DoorDash/Uber Eats let the same person
// review multiple times, so we don't want to greet a regular as a first-timer.
// A same-name AND same-platform match additionally raises a "Possible
// duplicate" badge + ingest toast (likely a re-uploaded screenshot — the
// content-hash dedupe only lasts one session).

const RR_ORANGE = '#EA580C';
const RR_ORANGE_BG = '#FFF4ED';
const RR_ORANGE_BORDER = '#FBD3B4';

const RR_PLATFORMS = {
  google:    { label: 'Google',    bg: '#E8F0FE', fg: '#1A56DB' },
  doordash:  { label: 'DoorDash',  bg: '#FBE6E3', fg: '#C1121F' },
  uber_eats: { label: 'Uber Eats', bg: '#E5F4E9', fg: '#1E7A34' },
  other:     { label: 'Other',     bg: '#F0F0F2', fg: '#6E6E73' },
};
const RR_DOORDASH = { Loved: '❤️ Loved', Liked: '👍 Liked', Disliked: '👎 Disliked' };

const rrFromRow = (r) => ({
  id: r.id,
  platform: r.platform || 'other',
  customerName: r.customer_name || '',
  nameIsLegit: !!r.name_is_legit,
  ratingType: r.rating_type || null,
  ratingStars: r.rating_stars ?? null,
  ratingDoordash: r.rating_doordash || null,
  subRatings: r.sub_ratings || null,
  reviewText: r.review_text || '',
  orderCount: r.order_count || '',
  replyDraft: r.reply_draft || '',
  isNegative: !!r.is_negative,
  needsHumanReview: !!r.needs_human_review,
  status: r.status || 'draft',
  createdAt: r.created_at,
});
// Only the columns the portal edits are sent back (reply text + status).
const rrToRow = (o) => ({
  id: o.id,
  platform: o.platform,
  customer_name: o.customerName || '',
  name_is_legit: !!o.nameIsLegit,
  rating_type: o.ratingType || null,
  rating_stars: o.ratingStars ?? null,
  rating_doordash: o.ratingDoordash || null,
  sub_ratings: o.subRatings || null,
  review_text: o.reviewText || '',
  order_count: o.orderCount || null,
  reply_draft: o.replyDraft || '',
  is_negative: !!o.isNegative,
  needs_human_review: !!o.needsHumanReview,
  status: o.status || 'draft',
});

// Manager-editable reply rules (review_reply_rules). These drive every drafted
// reply — the `review-reply` Edge Function reads the enabled rows in sort order.
const rrRuleFromRow = (r) => ({
  id: r.id,
  body: r.body || '',
  sourceInput: r.source_input || '',
  enabled: r.enabled !== false,
  sortOrder: r.sort_order ?? 0,
  createdAt: r.created_at,
});
const rrRuleToRow = (o) => ({
  id: o.id,
  body: o.body || '',
  source_input: o.sourceInput || null,
  enabled: o.enabled !== false,
  sort_order: o.sortOrder ?? 0,
});

const rrReadDataUrl = (file) => new Promise((resolve, reject) => {
  const fr = new FileReader();
  fr.onload = () => resolve(fr.result);
  fr.onerror = reject;
  fr.readAsDataURL(file);
});

// Content hash for de-duping re-pasted/re-uploaded screenshots within a session.
const rrSha256 = async (file) => {
  const buf = await file.arrayBuffer();
  const h = await crypto.subtle.digest('SHA-256', buf);
  return Array.from(new Uint8Array(h)).map(b => b.toString(16).padStart(2, '0')).join('');
};

const RrStars = ({ n }) => {
  const v = Number(n) || 0;
  return (
    <span style={{ letterSpacing: 1, fontSize: 14 }} title={`${v} of 5`}>
      <span style={{ color: '#F5A623' }}>{'★'.repeat(v)}</span>
      <span style={{ color: 'var(--border-1)' }}>{'★'.repeat(Math.max(0, 5 - v))}</span>
    </span>
  );
};

const ReviewReplies = () => {
  const [items, setItems] = window.useSupaList('review_replies', {
    fromRow: rrFromRow, toRow: rrToRow, initial: () => [],
  });
  const [search, setSearch] = useState('');
  const [platformFilter, setPlatformFilter] = useState('all');
  const [pending, setPending] = useState([]);   // [{ pid, name, status, error }]
  const [edits, setEdits] = useState({});        // { id: draftText } in-progress
  const [busy, setBusy] = useState({});          // { id: 'regen' }
  const [dragOver, setDragOver] = useState(false);
  const [toast, setToast] = useState('');
  const [expandedPosted, setExpandedPosted] = useState({}); // { id: true } posted rows temporarily re-opened
  const fileRef = useRef(null);
  const pidRef = useRef(0);
  const seenRef = useRef(new Set()); // image content hashes processed this session (dedupe)
  const itemsRef = useRef(items);   // latest items for use inside ingest callbacks
  useEffect(() => { itemsRef.current = items; }, [items]);

  // Reply guidelines (the rules the AI follows when drafting each reply).
  const [rules, setRules] = window.useSupaList('review_reply_rules', {
    fromRow: rrRuleFromRow, toRow: rrRuleToRow, initial: () => [],
  });
  const [rulesOpen, setRulesOpen] = useState(false);
  const [ruleEdits, setRuleEdits] = useState({}); // { id: bodyText } in-progress
  const [newRule, setNewRule] = useState('');
  const [refining, setRefining] = useState(false);

  const sortedRules = useMemo(
    () => [...rules].sort((a, b) => (a.sortOrder - b.sortOrder) || (a.createdAt || '').localeCompare(b.createdAt || '')),
    [rules]
  );

  const saveRuleBody = (id) => {
    const body = ruleEdits[id];
    if (body === undefined) return;
    const cur = rules.find(r => r.id === id);
    const trimmed = body.trim();
    if (cur && trimmed && trimmed !== cur.body) {
      setRules(prev => prev.map(r => r.id === id ? { ...r, body: trimmed } : r));
    }
    setRuleEdits(e => { const n = { ...e }; delete n[id]; return n; });
  };
  const toggleRule = (id) =>
    setRules(prev => prev.map(r => r.id === id ? { ...r, enabled: !r.enabled } : r));
  const deleteRule = (id) => {
    setRuleEdits(e => { const n = { ...e }; delete n[id]; return n; });
    setRules(prev => prev.filter(r => r.id !== id));
  };
  const moveRule = (id, dir) => {
    const arr = sortedRules;
    const idx = arr.findIndex(r => r.id === id);
    const swap = idx + dir;
    if (idx < 0 || swap < 0 || swap >= arr.length) return;
    const a = arr[idx], b = arr[swap];
    setRules(prev => prev.map(r => {
      if (r.id === a.id) return { ...r, sortOrder: b.sortOrder };
      if (r.id === b.id) return { ...r, sortOrder: a.sortOrder };
      return r;
    }));
  };
  const addRule = (body, sourceInput) => {
    const trimmed = (body || '').trim();
    if (!trimmed) return;
    const maxOrder = rules.reduce((m, r) => Math.max(m, r.sortOrder || 0), 0);
    setRules(prev => [...prev, {
      id: crypto.randomUUID(),
      body: trimmed,
      sourceInput: sourceInput || '',
      enabled: true,
      sortOrder: maxOrder + 1,
      createdAt: new Date().toISOString(),
    }]);
  };
  const addAsIs = () => { addRule(newRule); setNewRule(''); };
  const refineAndAdd = async () => {
    const raw = newRule.trim();
    if (!raw) return;
    setRefining(true);
    try {
      const res = await window.callEdge('review-reply', { mode: 'refine_rule', text: raw });
      if (!res.ok || res.data?.error || !res.data?.rule_text) {
        throw new Error(res.data?.error || ('HTTP ' + res.status));
      }
      addRule(res.data.rule_text, raw);
      setNewRule('');
      setToast('Rule added.');
    } catch (e) {
      setToast('Couldn’t refine that — added as-is. (' + (e.message || 'error') + ')');
      addRule(raw);
      setNewRule('');
    } finally {
      setRefining(false);
    }
  };

  const removePending = (pid) => setPending(p => {
    const hit = p.find(x => x.pid === pid);
    if (hit?.thumb) { try { URL.revokeObjectURL(hit.thumb); } catch (e) { /* noop */ } }
    return p.filter(x => x.pid !== pid);
  });

  const isImage = (f) => f && /^image\/(png|jpe?g|webp)$/.test(f.type);

  const ingestFile = useCallback(async (file) => {
    let hash = null;
    try { hash = await rrSha256(file); } catch (e) { /* dedupe best-effort */ }
    if (hash && seenRef.current.has(hash)) {
      setToast('Already added that screenshot — skipped the duplicate.');
      return;
    }
    if (hash) seenRef.current.add(hash);
    const pid = ++pidRef.current;
    const thumb = URL.createObjectURL(file);
    setPending(p => [...p, { pid, thumb, status: 'processing' }]);
    try {
      const dataUrl = await rrReadDataUrl(file);
      const base64 = String(dataUrl).split(',')[1];
      const res = await window.callEdge('review-reply', {
        mode: 'ingest',
        restaurant_id: window.RESTAURANT_ID,
        image_base64: base64,
        media_type: file.type || 'image/png',
      });
      if (!res.ok || res.data?.error) throw new Error(res.data?.error || ('HTTP ' + res.status));
      const n = res.data?.detected ?? (res.data?.reviews || []).length;
      // Show a clear done/empty state on the thumbnail, then auto-clear. Rows
      // themselves arrive in the list via the useSupaList realtime subscription.
      setPending(p => p.map(x => x.pid === pid ? { ...x, status: n > 0 ? 'done' : 'empty', count: n } : x));
      setTimeout(() => removePending(pid), n > 0 ? 4500 : 3500);
      if (n === 0) setToast('Couldn’t detect a review in that screenshot.');
      // Duplicate alert: a just-ingested review whose exact name + platform
      // already exist in the list is probably a re-uploaded screenshot (the
      // hash dedupe above only covers this session). The matching cards also
      // get a persistent "Possible duplicate" badge.
      const inserted = res.data?.reviews || [];
      const insertedIds = new Set(inserted.map(r => r.id));
      const dupNames = inserted
        .filter(r => r.name_is_legit && (r.customer_name || '').trim())
        .filter(r => itemsRef.current.some(it =>
          !insertedIds.has(it.id) && it.nameIsLegit
          && it.platform === (r.platform || 'other')
          && it.customerName.trim().toLowerCase() === r.customer_name.trim().toLowerCase()))
        .map(r => r.customer_name.trim());
      if (dupNames.length) {
        setToast('⚠️ Possible duplicate — ' + [...new Set(dupNames)].join(', ') + ' already has a review on the same platform.');
      }
    } catch (e) {
      if (hash) seenRef.current.delete(hash); // a failed image can be retried
      setPending(p => p.map(x => x.pid === pid ? { ...x, status: 'error', error: e.message || 'Failed' } : x));
    }
  }, []);

  const ingestFiles = useCallback((files) => {
    const imgs = Array.from(files || []).filter(isImage);
    if (!imgs.length) { setToast('Only PNG / JPG / WebP images are supported.'); return; }
    imgs.forEach(ingestFile);
  }, [ingestFile]);

  // Clipboard paste anywhere on the page.
  useEffect(() => {
    const onPaste = (e) => {
      const files = Array.from(e.clipboardData?.items || [])
        .filter(it => it.kind === 'file')
        .map(it => it.getAsFile())
        .filter(Boolean);
      if (files.length) { e.preventDefault(); ingestFiles(files); }
    };
    window.addEventListener('paste', onPaste);
    return () => window.removeEventListener('paste', onPaste);
  }, [ingestFiles]);

  const nameCounts = useMemo(() => {
    const m = {};
    items.forEach(it => {
      if (it.nameIsLegit && it.customerName) {
        const k = it.customerName.trim().toLowerCase();
        m[k] = (m[k] || 0) + 1;
      }
    });
    return m;
  }, [items]);

  // Ids of reviews whose exact name + platform match an EARLIER review — the
  // newer one(s) get a "Possible duplicate" badge (the original stays clean).
  const dupIds = useMemo(() => {
    const groups = {};
    items.forEach(it => {
      const name = (it.customerName || '').trim().toLowerCase();
      if (!it.nameIsLegit || !name) return;
      const k = it.platform + '|' + name;
      (groups[k] = groups[k] || []).push(it);
    });
    const s = new Set();
    Object.values(groups).forEach(arr => {
      if (arr.length < 2) return;
      arr.sort((a, b) => (a.createdAt || '').localeCompare(b.createdAt || ''));
      arr.slice(1).forEach(it => s.add(it.id));
    });
    return s;
  }, [items]);

  const filtered = useMemo(() => {
    const q = search.trim().toLowerCase();
    return items
      .filter(it => platformFilter === 'all' || it.platform === platformFilter)
      .filter(it => !q || (it.customerName || '').toLowerCase().includes(q)
        || (it.reviewText || '').toLowerCase().includes(q)
        || (it.replyDraft || '').toLowerCase().includes(q))
      .sort((a, b) => (b.createdAt || '').localeCompare(a.createdAt || ''));
  }, [items, platformFilter, search]);

  const platformCounts = useMemo(() => {
    const m = { all: items.length, google: 0, doordash: 0, uber_eats: 0, other: 0 };
    items.forEach(it => { m[it.platform] = (m[it.platform] || 0) + 1; });
    return m;
  }, [items]);

  const saveDraft = (id) => {
    const draft = edits[id];
    if (draft === undefined) return;
    const cur = items.find(it => it.id === id);
    if (cur && draft !== cur.replyDraft) {
      setItems(prev => prev.map(it => it.id === id ? { ...it, replyDraft: draft } : it));
    }
    setEdits(e => { const n = { ...e }; delete n[id]; return n; });
  };

  const regenerate = async (id) => {
    setBusy(b => ({ ...b, [id]: 'regen' }));
    setEdits(e => { const n = { ...e }; delete n[id]; return n; }); // drop local edit so the new draft shows
    try {
      const res = await window.callEdge('review-reply', { mode: 'regenerate', id });
      if (!res.ok || res.data?.error) throw new Error(res.data?.error || ('HTTP ' + res.status));
      // updated row arrives via realtime; also patch locally for instant feedback
      if (res.data?.reply_draft) {
        setItemsLocal(id, { replyDraft: res.data.reply_draft });
      }
    } catch (e) {
      setToast('Regenerate failed: ' + (e.message || 'error'));
    } finally {
      setBusy(b => { const n = { ...b }; delete n[id]; return n; });
    }
  };

  // Patch a row in local state WITHOUT triggering a write-back (the edge
  // function already persisted it; useSupaList's realtime echo will reconcile).
  const setItemsLocal = (id, patch) =>
    setItems(prev => prev.map(it => it.id === id ? { ...it, ...patch } : it));

  const togglePosted = (id) => {
    // Reset any temporary re-open so a freshly posted card starts collapsed.
    setExpandedPosted(e => { const n = { ...e }; delete n[id]; return n; });
    setItems(prev => prev.map(it => it.id === id ? { ...it, status: it.status === 'posted' ? 'draft' : 'posted' } : it));
  };

  const del = (id) => setItems(prev => prev.filter(it => it.id !== id));

  const copyReply = async (text) => {
    try { await navigator.clipboard.writeText(text || ''); setToast('Reply copied'); }
    catch { setToast('Copy failed — select and copy manually.'); }
  };

  return (
    <div className="portal-page-wide" style={{ maxWidth: 1100 }}>
      <div className="portal-page-header">
        <div>
          <h1 className="portal-page-title">Review Replies</h1>
          <div className="portal-page-subtitle">Drop, paste, or upload review screenshots — we read each one and draft an on-brand reply.</div>
        </div>
        <PBtn variant="secondary" icon="book" onClick={() => setRulesOpen(true)}>
          Reply guidelines{rules.length > 0 && <span style={{ opacity: 0.6, fontFamily: 'var(--font-num)', marginLeft: 2 }}>{rules.filter(r => r.enabled).length}</span>}
        </PBtn>
      </div>

      {/* Upload zone */}
      <div
        onDragOver={e => { e.preventDefault(); setDragOver(true); }}
        onDragLeave={() => setDragOver(false)}
        onDrop={e => { e.preventDefault(); setDragOver(false); ingestFiles(e.dataTransfer.files); }}
        onClick={() => fileRef.current?.click()}
        style={{
          border: `2px dashed ${dragOver ? RR_ORANGE : RR_ORANGE_BORDER}`,
          background: dragOver ? RR_ORANGE_BG : 'var(--bg-surface)',
          borderRadius: 14, padding: '26px 20px', textAlign: 'center', cursor: 'pointer',
          transition: 'background 120ms, border-color 120ms', marginBottom: 16,
        }}
      >
        <div style={{ width: 40, height: 40, borderRadius: 10, background: RR_ORANGE_BG, color: RR_ORANGE, margin: '0 auto 10px', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
          <PIcon name="plus" size={20} color={RR_ORANGE} />
        </div>
        <div style={{ fontSize: 14, fontWeight: 600, color: 'var(--fg-1)' }}>Drop screenshots here, paste (⌘V), or click to upload</div>
        <div style={{ fontSize: 12, color: 'var(--fg-3)', marginTop: 4 }}>Google, DoorDash, Uber Eats · multiple at once · stacked reviews are split automatically</div>
        <input ref={fileRef} type="file" accept="image/png,image/jpeg,image/webp" multiple style={{ display: 'none' }}
          onChange={e => { ingestFiles(e.target.files); e.target.value = ''; }} />
      </div>

      {/* Upload progress — thumbnail tiles so it's obvious each screenshot
          was received + is being read (no more guessing whether it worked). */}
      {pending.length > 0 && (
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: 10, marginBottom: 16 }}>
          {pending.map(p => (
            <div key={p.pid} title={p.status === 'error' ? p.error : undefined} style={{
              position: 'relative', width: 92, height: 92, borderRadius: 10, overflow: 'hidden', flexShrink: 0,
              background: 'var(--bg-sunken)',
              border: '1px solid ' + (p.status === 'error' ? 'var(--danger)' : p.status === 'done' ? '#BBE0A1' : RR_ORANGE_BORDER),
            }}>
              <img src={p.thumb} alt="review screenshot" style={{ width: '100%', height: '100%', objectFit: 'cover', opacity: p.status === 'processing' ? 0.45 : 0.85 }} />
              <div style={{
                position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 3, textAlign: 'center', padding: 4,
                background: p.status === 'error' ? 'rgba(139,42,26,0.62)' : p.status === 'processing' ? 'rgba(255,255,255,0.28)' : p.status === 'empty' ? 'rgba(20,20,20,0.5)' : 'rgba(45,106,42,0.6)',
                color: '#fff',
              }}>
                {p.status === 'processing' && (
                  <>
                    <span className="rr-spin" style={{ width: 18, height: 18, border: '2.5px solid ' + RR_ORANGE_BORDER, borderTopColor: RR_ORANGE, borderRadius: 999, display: 'inline-block' }} />
                    <span style={{ fontSize: 10, fontWeight: 700, color: 'var(--fg-1)' }}>Reading…</span>
                  </>
                )}
                {p.status === 'done' && (
                  <>
                    <PIcon name="check" size={22} color="#fff" />
                    <span style={{ fontSize: 10.5, fontWeight: 700 }}>{p.count} repl{p.count === 1 ? 'y' : 'ies'}</span>
                  </>
                )}
                {p.status === 'empty' && <span style={{ fontSize: 10, fontWeight: 700 }}>No review found</span>}
                {p.status === 'error' && (
                  <>
                    <span style={{ fontSize: 18, fontWeight: 800, lineHeight: 1 }}>!</span>
                    <span style={{ fontSize: 10, fontWeight: 700 }}>Failed</span>
                    <button onClick={() => removePending(p.pid)} style={{ fontSize: 9.5, fontWeight: 600, color: '#fff', textDecoration: 'underline', background: 'transparent', cursor: 'pointer' }}>dismiss</button>
                  </>
                )}
              </div>
            </div>
          ))}
        </div>
      )}

      {/* Controls */}
      <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 16, flexWrap: 'wrap' }}>
        <div style={{ position: 'relative', flex: 1, minWidth: 220 }}>
          <PIcon name="search" size={14} color="var(--fg-3)" style={{ position: 'absolute', left: 11, top: '50%', transform: 'translateY(-50%)' }} />
          <input className="portal-input" placeholder="Search name, review, reply…" value={search}
            onChange={e => setSearch(e.target.value)} style={{ paddingLeft: 32, width: '100%' }} />
        </div>
        {['all', 'google', 'doordash', 'uber_eats', 'other'].map(p => {
          const on = platformFilter === p;
          const label = p === 'all' ? 'All' : RR_PLATFORMS[p].label;
          return (
            <button key={p} onClick={() => setPlatformFilter(p)} style={{
              padding: '7px 12px', borderRadius: 999, fontSize: 12.5, fontWeight: 500, cursor: 'pointer',
              background: on ? 'var(--fg-1)' : 'var(--bg-sunken)', color: on ? '#fff' : 'var(--fg-2)',
              border: '1px solid ' + (on ? 'var(--fg-1)' : 'transparent'),
            }}>{label} <span style={{ opacity: 0.65, fontFamily: 'var(--font-num)' }}>{platformCounts[p] || 0}</span></button>
          );
        })}
      </div>

      <div style={{ fontSize: 12, color: 'var(--fg-3)', marginBottom: 10 }}>{filtered.length} repl{filtered.length === 1 ? 'y' : 'ies'}</div>

      {/* List */}
      {filtered.length === 0 ? (
        <div className="portal-empty">
          <div style={{ width: 48, height: 48, borderRadius: 999, background: RR_ORANGE_BG, margin: '0 auto 14px', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
            <PIcon name="msg" size={22} color={RR_ORANGE} />
          </div>
          <div style={{ fontSize: 14, fontWeight: 500, color: 'var(--fg-1)', marginBottom: 6 }}>{items.length === 0 ? 'No reviews yet' : 'No matches'}</div>
          <div style={{ fontSize: 12.5, color: 'var(--fg-3)' }}>{items.length === 0 ? 'Drop a review screenshot above to get started.' : 'Try a different search or filter.'}</div>
        </div>
      ) : (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
          {filtered.map(it => {
            const plat = RR_PLATFORMS[it.platform] || RR_PLATFORMS.other;
            const repeatCount = it.nameIsLegit && it.customerName ? (nameCounts[it.customerName.trim().toLowerCase()] || 0) : 0;
            const draftVal = edits[it.id] !== undefined ? edits[it.id] : it.replyDraft;
            const regen = busy[it.id] === 'regen';
            const collapsed = it.status === 'posted' && !expandedPosted[it.id];
            return (
              <div key={it.id} className="portal-card" style={{ padding: collapsed ? '10px 16px' : 16, opacity: it.status === 'posted' ? 0.72 : 1 }}>
                {/* Header */}
                <div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap', marginBottom: collapsed ? 0 : 10 }}>
                  <span style={{ fontSize: 11, fontWeight: 700, padding: '2px 9px', borderRadius: 999, background: plat.bg, color: plat.fg, letterSpacing: '0.02em' }}>{plat.label}</span>
                  {it.nameIsLegit && it.customerName
                    ? <span style={{ fontSize: 14, fontWeight: 600, color: 'var(--fg-1)' }}>{it.customerName}</span>
                    : <span style={{ fontSize: 13, fontStyle: 'italic', color: 'var(--fg-3)' }}>no clear name</span>}
                  {it.ratingType === 'stars' && it.ratingStars != null && <RrStars n={it.ratingStars} />}
                  {it.ratingType === 'doordash' && it.ratingDoordash && (
                    <span style={{ fontSize: 13, fontWeight: 600, color: 'var(--fg-1)' }}>{RR_DOORDASH[it.ratingDoordash] || it.ratingDoordash}</span>
                  )}
                  {it.orderCount && <span style={{ fontSize: 11.5, color: 'var(--fg-3)' }}>· {it.orderCount}</span>}
                  {repeatCount > 1 && (
                    <span title="This customer has reviewed before — reply mindful of a returning guest." style={{ fontSize: 10.5, fontWeight: 700, padding: '2px 8px', borderRadius: 999, background: RR_ORANGE_BG, color: RR_ORANGE, border: '1px solid ' + RR_ORANGE_BORDER }}>↻ Repeat customer · {repeatCount}</span>
                  )}
                  {dupIds.has(it.id) && (
                    <span title="Same name and same platform as an earlier review — could be a re-uploaded screenshot (or a genuine repeat review). Double-check before replying." style={{ fontSize: 10.5, fontWeight: 700, padding: '2px 8px', borderRadius: 999, background: 'var(--danger-bg)', color: 'var(--danger)' }}>⚠ Possible duplicate</span>
                  )}
                  {it.needsHumanReview && (
                    <span style={{ fontSize: 10.5, fontWeight: 700, padding: '2px 8px', borderRadius: 999, background: 'var(--danger-bg)', color: 'var(--danger)' }}>Needs review</span>
                  )}
                  <div style={{ flex: 1 }} />
                  <label style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 11.5, color: 'var(--fg-2)', cursor: 'pointer' }}>
                    <input type="checkbox" checked={it.status === 'posted'} onChange={() => togglePosted(it.id)} /> Posted
                  </label>
                  {it.status === 'posted' && (
                    <button
                      onClick={() => setExpandedPosted(e => ({ ...e, [it.id]: !e[it.id] }))}
                      title={collapsed ? 'Show review & reply' : 'Collapse'}
                      style={{ cursor: 'pointer', lineHeight: 0, color: 'var(--fg-3)' }}
                    >
                      <PIcon name={collapsed ? 'chevronD' : 'chevronU'} size={16} />
                    </button>
                  )}
                </div>

                {!collapsed && <>
                {/* Sub-ratings */}
                {it.subRatings && Object.keys(it.subRatings).length > 0 && (
                  <div style={{ fontSize: 11.5, color: 'var(--fg-3)', marginBottom: 8 }}>
                    {Object.entries(it.subRatings).map(([k, v]) => `${k[0].toUpperCase()}${k.slice(1)} ${v}`).join(' · ')}
                  </div>
                )}

                {/* Review text */}
                <div style={{ fontSize: 13.5, color: it.reviewText ? 'var(--fg-1)' : 'var(--fg-3)', fontStyle: it.reviewText ? 'normal' : 'italic', lineHeight: 1.5, marginBottom: 12 }}>
                  {it.reviewText || 'No written review — rating only'}
                </div>

                {/* Reply box */}
                <div style={{ background: RR_ORANGE_BG, border: '1px solid ' + RR_ORANGE_BORDER, borderRadius: 10, padding: 12 }}>
                  <div style={{ fontSize: 10.5, fontWeight: 700, color: RR_ORANGE, textTransform: 'uppercase', letterSpacing: '0.06em', marginBottom: 6 }}>Reply draft</div>
                  <textarea
                    value={regen ? 'Regenerating…' : draftVal}
                    disabled={regen}
                    onChange={e => setEdits(ed => ({ ...ed, [it.id]: e.target.value }))}
                    onBlur={() => saveDraft(it.id)}
                    rows={Math.max(2, Math.ceil((draftVal || '').length / 80))}
                    className="portal-input"
                    style={{ width: '100%', background: '#fff', resize: 'vertical', lineHeight: 1.5, fontSize: 13.5 }}
                  />
                  <div style={{ display: 'flex', gap: 8, marginTop: 10 }}>
                    <PBtn variant="primary" size="sm" icon="copy" onClick={() => copyReply(draftVal)} style={{ background: RR_ORANGE, borderColor: RR_ORANGE }}>Copy</PBtn>
                    <PBtn variant="secondary" size="sm" icon="refresh" onClick={() => regenerate(it.id)} disabled={regen}>{regen ? 'Regenerating…' : 'Regenerate'}</PBtn>
                    <div style={{ flex: 1 }} />
                    <PBtn variant="ghost" size="sm" icon="trash" onClick={() => del(it.id)} style={{ color: 'var(--danger)' }}>Delete</PBtn>
                  </div>
                </div>
                </>}
              </div>
            );
          })}
        </div>
      )}

      {/* Reply guidelines editor */}
      <PModal
        open={rulesOpen}
        onClose={() => setRulesOpen(false)}
        width={660}
        title="Reply guidelines"
        footer={<PBtn variant="primary" onClick={() => setRulesOpen(false)} style={{ background: RR_ORANGE, borderColor: RR_ORANGE }}>Done</PBtn>}
      >
        <div style={{ padding: 18 }}>
          <div style={{ fontSize: 12.5, color: 'var(--fg-3)', lineHeight: 1.55, marginBottom: 16 }}>
            These rules guide every reply we draft. Edit any rule, turn it on or off, reorder, or add your own — just describe what you want in plain words below and we’ll tidy up the wording. Changes apply to the next reply you generate.
          </div>

          {/* Existing rules */}
          <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
            {sortedRules.length === 0 && (
              <div style={{ fontSize: 12.5, color: 'var(--fg-3)', fontStyle: 'italic', padding: '8px 0' }}>
                No rules yet — add your first one below.
              </div>
            )}
            {sortedRules.map((r, i) => {
              const val = ruleEdits[r.id] !== undefined ? ruleEdits[r.id] : r.body;
              return (
                <div key={r.id} style={{
                  display: 'flex', gap: 10, alignItems: 'flex-start', padding: 10, borderRadius: 10,
                  border: '1px solid var(--border-2)',
                  background: r.enabled ? 'var(--bg-surface)' : 'var(--bg-sunken)',
                  opacity: r.enabled ? 1 : 0.62,
                }}>
                  {/* Order + reorder controls */}
                  <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 1, paddingTop: 2 }}>
                    <button onClick={() => moveRule(r.id, -1)} disabled={i === 0} title="Move up"
                      style={{ cursor: i === 0 ? 'default' : 'pointer', opacity: i === 0 ? 0.3 : 1, color: 'var(--fg-3)', lineHeight: 0 }}>
                      <PIcon name="chevronU" size={16} />
                    </button>
                    <span style={{ fontSize: 11, fontWeight: 700, color: 'var(--fg-3)', fontFamily: 'var(--font-num)' }}>{i + 1}</span>
                    <button onClick={() => moveRule(r.id, 1)} disabled={i === sortedRules.length - 1} title="Move down"
                      style={{ cursor: i === sortedRules.length - 1 ? 'default' : 'pointer', opacity: i === sortedRules.length - 1 ? 0.3 : 1, color: 'var(--fg-3)', lineHeight: 0 }}>
                      <PIcon name="chevronD" size={16} />
                    </button>
                  </div>
                  {/* Rule body */}
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <textarea
                      value={val}
                      onChange={e => setRuleEdits(s => ({ ...s, [r.id]: e.target.value }))}
                      onBlur={() => saveRuleBody(r.id)}
                      rows={Math.max(1, Math.ceil((val || '').length / 58))}
                      className="portal-input"
                      style={{ width: '100%', resize: 'vertical', fontSize: 13, lineHeight: 1.45 }}
                    />
                    {r.sourceInput && r.sourceInput !== r.body && (
                      <div style={{ fontSize: 11, color: 'var(--fg-3)', marginTop: 4, fontStyle: 'italic' }}>You said: “{r.sourceInput}”</div>
                    )}
                  </div>
                  {/* Actions */}
                  <div style={{ display: 'flex', flexDirection: 'column', gap: 8, alignItems: 'center', paddingTop: 2 }}>
                    <label title={r.enabled ? 'Rule is on — tap to turn off' : 'Rule is off — tap to turn on'} style={{ cursor: 'pointer', display: 'inline-flex' }}>
                      <input type="checkbox" checked={r.enabled} onChange={() => toggleRule(r.id)} />
                    </label>
                    <button onClick={() => deleteRule(r.id)} title="Delete rule" style={{ cursor: 'pointer', lineHeight: 0 }}>
                      <PIcon name="trash" size={15} color="var(--danger)" />
                    </button>
                  </div>
                </div>
              );
            })}
          </div>

          {/* Add a rule */}
          <div style={{ marginTop: 16, padding: 12, borderRadius: 10, background: RR_ORANGE_BG, border: '1px solid ' + RR_ORANGE_BORDER }}>
            <div style={{ fontSize: 10.5, fontWeight: 700, color: RR_ORANGE, textTransform: 'uppercase', letterSpacing: '0.06em', marginBottom: 8 }}>Add a rule</div>
            <textarea
              value={newRule}
              onChange={e => setNewRule(e.target.value)}
              placeholder="Describe what you want in your own words — e.g. “don’t sound robotic; if they liked the drinks, tell them to try the brown sugar boba next time”"
              rows={2}
              className="portal-input"
              style={{ width: '100%', resize: 'vertical', fontSize: 13, lineHeight: 1.45, background: '#fff' }}
            />
            <div style={{ display: 'flex', gap: 8, marginTop: 10, alignItems: 'center' }}>
              <PBtn variant="primary" size="sm" icon="sparkle" onClick={refineAndAdd} disabled={refining || !newRule.trim()} style={{ background: RR_ORANGE, borderColor: RR_ORANGE }}>
                {refining ? 'Refining…' : 'Refine & add'}
              </PBtn>
              <PBtn variant="secondary" size="sm" icon="plus" onClick={addAsIs} disabled={refining || !newRule.trim()}>Add as-is</PBtn>
              <span style={{ fontSize: 11, color: 'var(--fg-3)' }}>“Refine &amp; add” rewrites your note into a clean rule.</span>
            </div>
          </div>
        </div>
      </PModal>

      <style>{`@keyframes rr-spin { to { transform: rotate(360deg); } } .rr-spin { animation: rr-spin 0.7s linear infinite; }`}</style>
      <PToast message={toast} open={!!toast} onClose={() => setToast('')} />
    </div>
  );
};

window.ReviewReplies = ReviewReplies;
