/* global React, TABLES */
// Handle-candidate research tool. Lives inside the admin panel.
//
// Rather than guessing at handles, this runs real searches keyed by
// `<name> artist` / `<name> vtuber` against each platform:
//   - X/Twitter  : open search links (no anonymous API)
//   - Instagram  : open Google search restricted to instagram.com
//   - Bluesky    : app.bsky.actor.searchActors — real results shown inline
//
// For X/IG the operator clicks through to search, finds the right account,
// pastes the handle, and hits approve. For Bluesky we resolve and show the
// top ~3 actors up front; one-click approve.

const { useState, useEffect, useMemo, useCallback, useRef } = React;

const REJECT_KEY = 'as:seed:rejected';

function readRejected() {
  try { return new Set(JSON.parse(localStorage.getItem(REJECT_KEY) || '[]')); }
  catch { return new Set(); }
}
function writeRejected(set) {
  try { localStorage.setItem(REJECT_KEY, JSON.stringify([...set])); } catch {}
}
function rejectKey(tableId, platform, handle) {
  return `${tableId}:${platform}:${handle}`;
}

// Two search variants per platform so the operator gets both an
// "artist alley" angle and a "vtuber / streamer" angle. Encoded as URLs to
// open in a new tab.
function searchLinks(name) {
  const q = (s) => encodeURIComponent(s);
  return {
    x: [
      { label: `X: "${name}" artist`,  href: `https://x.com/search?q=${q(`"${name}" artist`)}&src=typed_query&f=user` },
      { label: `X: "${name}" vtuber`,  href: `https://x.com/search?q=${q(`"${name}" vtuber`)}&src=typed_query&f=user` },
      { label: `google → x.com`,       href: `https://www.google.com/search?q=${q(`"${name}" artist OR vtuber site:x.com OR site:twitter.com`)}` },
    ],
    ig: [
      { label: `google → instagram: artist`, href: `https://www.google.com/search?q=${q(`"${name}" artist site:instagram.com`)}` },
      { label: `google → instagram: vtuber`, href: `https://www.google.com/search?q=${q(`"${name}" vtuber site:instagram.com`)}` },
      { label: `instagram tag: #${name.replace(/\W+/g,'').toLowerCase()}`, href: `https://www.instagram.com/explore/tags/${encodeURIComponent(name.replace(/\W+/g,'').toLowerCase())}/` },
    ],
  };
}

// Bluesky actor search. Public endpoint, CORS-enabled. Returns up to 25
// matches ranked by the server.
async function searchBsky(term) {
  try {
    const res = await fetch(
      `https://public.api.bsky.app/xrpc/app.bsky.actor.searchActors?term=${encodeURIComponent(term)}&limit=5`
    );
    if (!res.ok) return [];
    const data = await res.json();
    return Array.isArray(data.actors) ? data.actors : [];
  } catch { return []; }
}

// Merge two search-result lists, dedupe by DID, keep original ranking.
function mergeActors(a, b) {
  const seen = new Set();
  const out = [];
  for (const list of [a, b]) {
    for (const actor of list) {
      if (!actor || !actor.did || seen.has(actor.did)) continue;
      seen.add(actor.did);
      out.push(actor);
    }
  }
  return out.slice(0, 3);
}

function SeedCandidates({ token, directory, onDirectoryChange }) {
  const tables = Array.isArray(window.TABLES) ? window.TABLES : [];
  const api = window.AS_CLAIMS || {};

  const [rejected, setRejected] = useState(() => readRejected());
  const [bskyResults, setBskyResults] = useState({});  // { tableId: Actor[] }
  const [bskyLoading, setBskyLoading] = useState({});
  const [inputs, setInputs] = useState({});            // { "tableId:x": "handle" }
  const [status, setStatus] = useState('');
  const [onlyUnclaimed, setOnlyUnclaimed] = useState(true);
  const [expanded, setExpanded] = useState(() => new Set());

  useEffect(() => { writeRejected(rejected); }, [rejected]);

  const entries = useMemo(() => {
    return tables
      .map(t => {
        const entry = directory?.[t.id] || {};
        const hasAny = !!(entry.x || entry.ig || entry.bsky);
        const missingAny = !(entry.x && entry.ig && entry.bsky);
        return {
          tableId: t.id,
          name: t.artists?.[0]?.name || '(unnamed)',
          seededX: entry.x || null,
          seededIg: entry.ig || null,
          seededBsky: entry.bsky || null,
          hasAny,
          missingAny,
        };
      })
      .filter(e => !onlyUnclaimed || e.missingAny || expanded.has(e.tableId))
      .sort((a, b) => a.tableId.localeCompare(b.tableId, 'en', { numeric: true }));
  }, [tables, directory, onlyUnclaimed, expanded]);

  const toggleExpanded = (id) => {
    setExpanded(prev => {
      const next = new Set(prev);
      if (next.has(id)) next.delete(id); else next.add(id);
      return next;
    });
  };

  const runningRef = useRef(new Set());
  const runBskySearch = useCallback(async (entry) => {
    if (bskyResults[entry.tableId] || runningRef.current.has(entry.tableId)) return;
    runningRef.current.add(entry.tableId);
    setBskyLoading(p => ({ ...p, [entry.tableId]: true }));
    // Two angled queries, merge + dedupe.
    const [a, b] = await Promise.all([
      searchBsky(`${entry.name} artist`),
      searchBsky(`${entry.name} vtuber`),
    ]);
    setBskyResults(p => ({ ...p, [entry.tableId]: mergeActors(a, b) }));
    setBskyLoading(p => ({ ...p, [entry.tableId]: false }));
  }, [bskyResults]);

  useEffect(() => {
    for (const id of expanded) {
      const e = entries.find(x => x.tableId === id);
      if (e) runBskySearch(e);
    }
  }, [expanded, entries, runBskySearch]);

  const isRejected = (tableId, platform, handle) => rejected.has(rejectKey(tableId, platform, handle));
  const reject = (tableId, platform, handle) => {
    setRejected(prev => { const n = new Set(prev); n.add(rejectKey(tableId, platform, handle)); return n; });
  };
  const unreject = (tableId, platform, handle) => {
    setRejected(prev => { const n = new Set(prev); n.delete(rejectKey(tableId, platform, handle)); return n; });
  };

  const approve = async (tableId, platform, handle) => {
    const cleaned = String(handle || '').trim().replace(/^@/, '');
    if (!cleaned) { setStatus('enter a handle first.'); return; }
    if (!token) { setStatus('not signed in as admin.'); return; }
    const nextDir = { ...(directory || {}) };
    nextDir[tableId] = { ...(nextDir[tableId] || {}), [platform]: cleaned };
    try {
      await api.adminPutDirectory(token, nextDir);
      onDirectoryChange(nextDir);
      setStatus(`approved ${platform}: ${cleaned} for ${tableId}.`);
      setInputs(p => ({ ...p, [`${tableId}:${platform}`]: '' }));
    } catch (err) {
      setStatus(err.message || 'approve failed');
    }
  };

  const unseed = async (tableId, platform) => {
    if (!token) { setStatus('not signed in as admin.'); return; }
    const nextDir = { ...(directory || {}) };
    if (nextDir[tableId]) {
      const copy = { ...nextDir[tableId] };
      delete copy[platform];
      if (Object.keys(copy).length === 0) delete nextDir[tableId];
      else nextDir[tableId] = copy;
    }
    try {
      await api.adminPutDirectory(token, nextDir);
      onDirectoryChange(nextDir);
      setStatus(`unseeded ${platform} for ${tableId}.`);
    } catch (err) {
      setStatus(err.message || 'unseed failed');
    }
  };

  const resetRejections = () => {
    if (!confirm('clear all rejections?')) return;
    setRejected(new Set());
  };

  return (
    <div className="seed-root">
      <div className="seed-toolbar">
        <label className="seed-toggle">
          <input
            type="checkbox"
            checked={onlyUnclaimed}
            onChange={(e) => setOnlyUnclaimed(e.target.checked)}
          />
          only show tables still missing a seeded handle
        </label>
        <button type="button" className="link" onClick={resetRejections}>
          reset rejections ({rejected.size})
        </button>
      </div>

      {status && (
        <div className={status.includes('fail') || status.includes('error') || status.includes('enter') ? 'claim-error' : 'claim-ok'}>
          {status}
        </div>
      )}

      <div className="seed-list">
        {entries.length === 0 && (
          <div className="admin-empty">
            {onlyUnclaimed ? 'every table already has seeded handles for the platforms you are reviewing.' : 'no tables loaded.'}
          </div>
        )}
        {entries.map(e => {
          const isOpen = expanded.has(e.tableId);
          return (
            <div className={`seed-row ${isOpen ? 'is-open' : ''}`} key={e.tableId}>
              <button type="button" className="seed-head" onClick={() => toggleExpanded(e.tableId)}>
                <span className="seed-tid">{e.tableId}</span>
                <span className="seed-name">{e.name}</span>
                <span className="seed-chips">
                  {e.seededX    && <span className="seed-chip seed-chip-x">𝕏 @{e.seededX}</span>}
                  {e.seededIg   && <span className="seed-chip seed-chip-ig">◐ @{e.seededIg}</span>}
                  {e.seededBsky && <span className="seed-chip seed-chip-bsky">☁ {e.seededBsky}</span>}
                  {!e.hasAny    && <span className="seed-chip seed-chip-none">no handle yet</span>}
                </span>
                <span className="seed-caret">{isOpen ? '▾' : '▸'}</span>
              </button>

              {isOpen && (
                <div className="seed-body">
                  <SeedSearchRow
                    title="X (Twitter)"
                    platform="x"
                    links={searchLinks(e.name).x}
                    tableId={e.tableId}
                    seeded={e.seededX}
                    value={inputs[`${e.tableId}:x`] || ''}
                    onChange={(v) => setInputs(p => ({ ...p, [`${e.tableId}:x`]: v }))}
                    placeholder="paste handle (no @)"
                    onApprove={(h) => approve(e.tableId, 'x', h)}
                    onUnseed={() => unseed(e.tableId, 'x')}
                  />
                  <SeedSearchRow
                    title="Instagram"
                    platform="ig"
                    links={searchLinks(e.name).ig}
                    tableId={e.tableId}
                    seeded={e.seededIg}
                    value={inputs[`${e.tableId}:ig`] || ''}
                    onChange={(v) => setInputs(p => ({ ...p, [`${e.tableId}:ig`]: v }))}
                    placeholder="paste handle (no @)"
                    onApprove={(h) => approve(e.tableId, 'ig', h)}
                    onUnseed={() => unseed(e.tableId, 'ig')}
                  />
                  <SeedBlueskyRow
                    tableId={e.tableId}
                    name={e.name}
                    seeded={e.seededBsky}
                    loading={bskyLoading[e.tableId]}
                    actors={bskyResults[e.tableId] || []}
                    isRejected={isRejected}
                    onApprove={(h) => approve(e.tableId, 'bsky', h)}
                    onReject={(h) => reject(e.tableId, 'bsky', h)}
                    onUnreject={(h) => unreject(e.tableId, 'bsky', h)}
                    onUnseed={() => unseed(e.tableId, 'bsky')}
                  />
                </div>
              )}
            </div>
          );
        })}
      </div>
    </div>
  );
}

function SeedSearchRow({
  title, platform, links, seeded, value, onChange, placeholder,
  onApprove, onUnseed,
}) {
  return (
    <div className="seed-plat">
      <div className="seed-plat-head">
        <span className="seed-plat-title">{title}</span>
        {seeded && (
          <span className="seed-plat-seeded">
            seeded: <strong>@{seeded}</strong>
            <button type="button" className="link danger" onClick={onUnseed}>unseed</button>
          </span>
        )}
      </div>
      <div className="seed-searchlinks">
        {links.map(l => (
          <a key={l.href} className="seed-searchlink" href={l.href} target="_blank" rel="noopener noreferrer">
            {l.label} ↗
          </a>
        ))}
      </div>
      <form
        className="seed-approve-row"
        onSubmit={(e) => { e.preventDefault(); onApprove(value); }}
      >
        <input
          className="seed-approve-input"
          type="text"
          value={value}
          onChange={(e) => onChange(e.target.value)}
          placeholder={placeholder}
        />
        <button type="submit" className="seed-btn seed-approve">✓ approve</button>
      </form>
    </div>
  );
}

function SeedBlueskyRow({
  tableId, name, seeded, loading, actors, isRejected, onApprove, onReject, onUnreject, onUnseed,
}) {
  return (
    <div className="seed-plat">
      <div className="seed-plat-head">
        <span className="seed-plat-title">
          Bluesky
          <span className="seed-plat-hint"> searching "{name} artist" + "{name} vtuber"</span>
        </span>
        {seeded && (
          <span className="seed-plat-seeded">
            seeded: <strong>{seeded}</strong>
            <button type="button" className="link danger" onClick={onUnseed}>unseed</button>
          </span>
        )}
      </div>
      {loading ? (
        <div className="seed-empty">searching Bluesky…</div>
      ) : actors.length === 0 ? (
        <div className="seed-empty">no Bluesky matches for either query.</div>
      ) : (
        <ul className="seed-cands">
          {actors.map(a => {
            const rej = isRejected(tableId, 'bsky', a.handle);
            return (
              <li key={a.did} className={`seed-cand seed-cand-rich ${rej ? 'is-rej' : ''}`}>
                {a.avatar && <img className="seed-avatar" src={a.avatar} alt="" loading="lazy" />}
                <div className="seed-cand-meta">
                  <a className="seed-cand-link" href={`https://bsky.app/profile/${a.handle}`} target="_blank" rel="noopener noreferrer">
                    <strong>{a.displayName || a.handle}</strong> <span className="seed-cand-sub">@{a.handle} ↗</span>
                  </a>
                  {a.description && <div className="seed-cand-bio">{a.description}</div>}
                </div>
                <div className="seed-cand-actions">
                  {rej ? (
                    <button type="button" className="link" onClick={() => onUnreject(a.handle)}>unreject</button>
                  ) : (
                    <>
                      <button type="button" className="seed-btn seed-approve" onClick={() => onApprove(a.handle)}>✓</button>
                      <button type="button" className="seed-btn seed-reject"  onClick={() => onReject(a.handle)}>✗</button>
                    </>
                  )}
                </div>
              </li>
            );
          })}
        </ul>
      )}
    </div>
  );
}

window.AS = Object.assign(window.AS || {}, { SeedCandidates });
