// ViG Command Center — Sub Cities (svs.info /subs) sync + page
const { useState: useStateSB, useEffect: useEffectSB, useMemo: useMemoSB } = React;

const SUBS_KEY = 'vig2_subs_data';
const SUBS_CFG_KEY = 'vig2_subs_cfg';
const subsDefaultCfg = () => ({
  url: 'https://svs.info/server/428/subs',
  proxy: (LS.get('vig2_sync_cfg', {}) || {}).proxy || 'https://api.allorigins.win/raw?url=',
  auto: true,
});

const SUB_QUALITIES = ['White', 'Green', 'Blue', 'Purple', 'Gold', 'Red'];
const SUB_QUALITY_COLOR = { White: '#a1a1aa', Green: '#22c55e', Blue: '#3b82f6', Purple: '#a855f7', Gold: '#f59e0b', Red: '#ef4444' };
const SUB_CULTURES = ['Europe', 'China', 'Japan', 'Korea', 'America', 'Russia', 'Arabia'];

/* ---------- normalize one record from whatever shape the feed gives ---------- */
function normSub(o) {
  if (!o || typeof o !== 'object') return null;
  const k = (...names) => { for (const n of names) { const key = Object.keys(o).find(x => x.toLowerCase() === n); if (key != null && o[key] !== '' && o[key] != null) return o[key]; } return null; };
  const name = k('name', 'subname', 'sub_name', 'title', 'city', 'cityname');
  const level = +k('level', 'lvl', 'sublevel') || null;
  let quality = k('quality', 'rarity', 'color', 'grade');
  if (typeof quality === 'number') quality = SUB_QUALITIES[Math.max(0, Math.min(5, quality - 1))];
  if (typeof quality === 'string') { const q = SUB_QUALITIES.find(x => quality.toLowerCase().includes(x.toLowerCase())); quality = q || quality; }
  let culture = k('culture', 'civ', 'civilization', 'nation');
  if (typeof culture === 'string') { const c = SUB_CULTURES.find(x => culture.toLowerCase().includes(x.toLowerCase())); culture = c || culture; }
  const owner = k('owner', 'occupiedby', 'occupied_by', 'player', 'holder', 'occupier');
  const x = k('x', 'posx', 'coordx'), y = k('y', 'posy', 'coordy');
  const buff = k('buff', 'buffs', 'effect', 'bonus', 'desc', 'description');
  if (!name && !level && !quality) return null;
  return { name: name || 'Sub City', level, quality: quality || null, culture: culture || null, owner: owner || null, x: x != null ? +x : null, y: y != null ? +y : null, buff: typeof buff === 'string' ? buff : null };
}

function subsFindArray(node, depth) {
  if (!node || depth > 8) return null;
  if (Array.isArray(node)) {
    const normed = node.map(normSub).filter(Boolean);
    if (normed.length >= 5) return normed;
    for (const x of node) { const r = subsFindArray(x, depth + 1); if (r) return r; }
    return null;
  }
  if (typeof node === 'object') { for (const key of Object.keys(node)) { const r = subsFindArray(node[key], depth + 1); if (r) return r; } }
  return null;
}

/* ---------- parse feed body: JSON (worker/api), markdown (r.jina.ai), or HTML ---------- */
function parseSubsMarkdown(text) {
  const lines = text.split('\n').map(l => l.trim()).filter(l => /^\|.*\|$/.test(l));
  if (lines.length < 3) return null;
  const cells = l => l.slice(1, -1).split('|').map(c => c.trim());
  const header = cells(lines[0]).map(h => h.toLowerCase());
  const rows = [];
  for (const line of lines.slice(1)) {
    const c = cells(line);
    if (c.every(x => /^[-: ]*$/.test(x))) continue; // separator
    const rec = {};
    header.forEach((h, i) => rec[h] = c[i] || '');
    // owner: markdown link [Name](url)
    let owner = rec.owner || rec.player || rec.holder || '';
    const lm = /\[([^\]]+)\]/.exec(owner); if (lm) owner = lm[1];
    owner = owner.trim();
    // coords "890, 235"
    let x = null, y = null;
    const coords = rec.coords || rec.position || rec.location || '';
    const cm = /(\d+)\s*[,;: ]\s*(\d+)/.exec(coords); if (cm) { x = +cm[1]; y = +cm[2]; }
    // first column may be labeled culture but hold quality colors
    let quality = null, culture = null;
    for (const h of header) {
      const v = (rec[h] || '').trim();
      if (!v) continue;
      const qm = SUB_QUALITIES.find(q => v.toLowerCase() === q.toLowerCase());
      const cm2 = SUB_CULTURES.find(cu => v.toLowerCase() === cu.toLowerCase());
      if (qm && !quality) quality = qm; else if (cm2 && !culture) culture = cm2;
    }
    const level = +(rec.level || rec.lv || '') || null;
    const name = (rec.name || rec.sub || '').replace(/\[([^\]]+)\][^|]*/, '$1').trim() || null;
    if (!quality && !owner && x == null) continue;
    rows.push({ name: name || (quality ? quality + ' sub' : 'Sub City'), level, quality, culture, owner: owner || null, x, y, buff: null });
  }
  return rows.length >= 3 ? rows : null;
}

function parseSubsBody(body) {
  const trimmed = (body || '').trim();
  if (trimmed[0] === '{' || trimmed[0] === '[') {
    try { const j = JSON.parse(trimmed); const arr = subsFindArray(j, 0); if (arr) return arr; } catch (e) { }
  }
  // markdown table (r.jina.ai rendered output)
  if (/^\|.*\|$/m.test(body)) { const md = parseSubsMarkdown(body); if (md) return md; }
  // HTML: embedded JSON in scripts, then table rows
  const doc = new DOMParser().parseFromString(body, 'text/html');
  for (const s of [...doc.querySelectorAll('script')]) {
    const t = s.textContent || '';
    if (t.length < 100) continue;
    try { const j = JSON.parse(t); const arr = subsFindArray(j, 0); if (arr) return arr; } catch (e) {
      const m = t.match(/\{[\s\S]*\}/); if (m) { try { const arr = subsFindArray(JSON.parse(m[0]), 0); if (arr) return arr; } catch (e2) { } }
    }
  }
  const rows = [];
  [...doc.querySelectorAll('table tr')].forEach(tr => {
    const cells = [...tr.children].map(c => c.textContent.trim());
    if (cells.length < 3) return;
    const lvlIdx = cells.findIndex(c => /^(lv\.?\s?)?\d{1,2}$/i.test(c));
    if (lvlIdx < 0) return;
    const quality = SUB_QUALITIES.find(q => cells.some(c => c.toLowerCase() === q.toLowerCase()));
    const culture = SUB_CULTURES.find(cu => cells.some(c => c.toLowerCase() === cu.toLowerCase()));
    const name = cells.find(c => c.length > 2 && !/^\d+$/.test(c));
    if (!name) return;
    rows.push({ name, level: parseInt(cells[lvlIdx].replace(/\D/g, '')), quality: quality || null, culture: culture || null, owner: null, x: null, y: null, buff: null });
  });
  return rows.length >= 5 ? rows : null;
}

/* ---- resilient fetch: user's proxy first, then public fallbacks ---- */
const SVS_FALLBACK_PROXIES = [
  u => 'https://api.allorigins.win/raw?url=' + encodeURIComponent(u),
  u => 'https://corsproxy.io/?url=' + encodeURIComponent(u),
  u => 'https://api.codetabs.com/v1/proxy?quest=' + encodeURIComponent(u),
];
async function svsFetchText(url, primaryPrefix, minLen) {
  const attempts = [];
  if (primaryPrefix) attempts.push(u => primaryPrefix + encodeURIComponent(u));
  for (const f of SVS_FALLBACK_PROXIES) {
    const probe = f('X');
    if (!primaryPrefix || !probe.startsWith(primaryPrefix.slice(0, 18))) attempts.push(f);
  }
  const errs = [];
  for (const make of attempts) {
    const ctrl = new AbortController();
    const timer = setTimeout(() => ctrl.abort(), 14000);
    try {
      const res = await fetch(make(url), { headers: { 'Accept': 'application/json, text/html' }, signal: ctrl.signal });
      clearTimeout(timer);
      if (!res.ok) { errs.push('HTTP ' + res.status); continue; }
      const body = await res.text();
      if (body.length < (minLen || 300)) { errs.push('empty response'); continue; }
      return body;
    } catch (e) {
      clearTimeout(timer);
      errs.push(e.name === 'AbortError' ? 'timeout' : (e.message || 'failed'));
    }
  }
  throw new Error('all proxies failed (' + errs.join(' · ') + ')');
}
window.svsFetchText = svsFetchText;

async function runSubsSync(cfg) {
  let subs = null, lastErr = null;
  // 1) plain proxies (fast, works if feed is JSON/Worker or page is server-rendered)
  try {
    const body = await svsFetchText(cfg.url, cfg.proxy || null, 300);
    subs = parseSubsBody(body);
  } catch (e) { lastErr = e.message; }
  // 2) rendering proxy — executes the page's JS (needed for svs.info /subs)
  if (!subs || !subs.length) {
    try {
      const ctrl = new AbortController(); const t = setTimeout(() => ctrl.abort(), 30000);
      const r = await fetch('https://r.jina.ai/' + cfg.url, { signal: ctrl.signal });
      clearTimeout(t);
      if (r.ok) { const txt = await r.text(); if (txt.length > 200) subs = parseSubsBody(txt); }
    } catch (e) { lastErr = (lastErr ? lastErr + ' · ' : '') + 'renderer: ' + (e.name === 'AbortError' ? 'timeout' : e.message); }
  }
  if (!subs || !subs.length) throw new Error(lastErr || 'no sub-city rows found');
  const prev = LS.get(SUBS_KEY, null);
  const data = { subs, syncedAt: Date.now(), prevCount: prev ? prev.subs.length : null };
  LS.set(SUBS_KEY, data);
  return data;
}

function subsTimeAgo(ts) {
  if (!ts) return 'never';
  const s = Math.floor((Date.now() - ts) / 1000);
  if (s < 60) return 'just now'; if (s < 3600) return Math.floor(s / 60) + 'm ago';
  if (s < 86400) return Math.floor(s / 3600) + 'h ago'; return Math.floor(s / 86400) + 'd ago';
}

function SubsPage() {
  const [data, setData] = useStateSB(() => LS.get(SUBS_KEY, null));
  const [cfg, setCfg] = useStateSB(() => ({ ...subsDefaultCfg(), ...(LS.get(SUBS_CFG_KEY, {}) || {}) }));
  const [busy, setBusy] = useStateSB(false);
  const [err, setErr] = useStateSB(null);
  const [showCfg, setShowCfg] = useStateSB(false);
  const [q, setQ] = useStateSB('');
  const [quality, setQuality] = useStateSB('all');
  const [culture, setCulture] = useStateSB('all');
  const [occ, setOcc] = useStateSB('all');
  const [lvlMin, setLvlMin] = useStateSB(0);
  useEffectSB(() => { LS.set(SUBS_CFG_KEY, cfg); }, [cfg]);

  const sync = async () => {
    setBusy(true); setErr(null);
    try { setData(await runSubsSync(cfg)); } catch (e) { setErr(e.message); }
    setBusy(false);
  };
  // 24h auto-refresh
  useEffectSB(() => {
    if (!cfg.auto) return;
    const last = (data && data.syncedAt) || 0;
    if (Date.now() - last > 24 * 3600 * 1000) runSubsSync(cfg).then(setData).catch(() => { });
  }, []);

  const subs = (data && data.subs) || [];
  const rows = useMemoSB(() => {
    const ql = q.trim().toLowerCase();
    return subs.filter(s => {
      if (quality !== 'all' && s.quality !== quality) return false;
      if (culture !== 'all' && s.culture !== culture) return false;
      if (occ === 'free' && s.owner) return false;
      if (occ === 'occupied' && !s.owner) return false;
      if (s.level != null && s.level < lvlMin) return false;
      if (ql && !((s.name || '').toLowerCase().includes(ql) || (s.owner || '').toLowerCase().includes(ql) || (s.buff || '').toLowerCase().includes(ql))) return false;
      return true;
    }).sort((a, b) => (b.level || 0) - (a.level || 0));
  }, [subs, q, quality, culture, occ, lvlMin]);

  const stats = useMemoSB(() => ({
    total: subs.length,
    free: subs.filter(s => !s.owner).length,
    gold: subs.filter(s => s.quality === 'Gold' || s.quality === 'Red').length,
    maxLvl: Math.max(0, ...subs.map(s => s.level || 0)),
  }), [subs]);

  return (
    <div className="space-y-4">
      {/* sync bar */}
      <Card className="flex flex-wrap items-center gap-x-4 gap-y-2 px-4 py-2.5">
        <div className="flex items-center gap-2">
          <span className="relative flex h-2 w-2">
            {data && <span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-400 opacity-70"></span>}
            <span className={`relative inline-flex h-2 w-2 rounded-full ${data ? 'bg-emerald-400' : 'bg-zinc-500'}`}></span>
          </span>
          <span className="text-[12px] font-semibold uppercase tracking-[0.12em]" style={{ color: data ? '#34d399' : undefined }}>{data ? 'Live data' : 'Not synced'}</span>
        </div>
        <span className="text-[12px] text-zinc-400">{data ? <>Synced <b className="text-zinc-200">{subsTimeAgo(data.syncedAt)}</b> from the live source · auto-refresh {cfg.auto ? 'every 24h' : 'off'}</> : 'Pull server 428 sub cities from the live source'}</span>
        <div className="ml-auto flex items-center gap-2">
          <Btn variant="outline" size="sm" onClick={() => setShowCfg(s => !s)}><Icon name="grid" size={13} />Source</Btn>
          <Btn variant="brand" size="sm" onClick={sync} disabled={busy}><Icon name="bolt" size={13} className={busy ? 'animate-spin' : ''} />{busy ? 'Syncing…' : 'Sync now'}</Btn>
        </div>
      </Card>
      {err && <div className="rounded-lg border border-rose-500/30 bg-rose-500/10 px-4 py-2.5 text-[12.5px] text-rose-300">Sync failed: {err}. The list is JavaScript-rendered, so a plain relay may return no rows — deploy the included Worker and point the feed at <span className="font-mono">https://your-worker.workers.dev/?url=…/subs</span>, or retry later.</div>}
      {showCfg && (
        <Card className="space-y-3 p-4">
          <div><div className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-zinc-400">Source page / feed</div>
            <input value={cfg.url} onChange={e => setCfg(c => ({ ...c, url: e.target.value }))} className="w-full rounded-lg border border-zinc-200 bg-white px-3 py-2 font-mono text-[12px] outline-none focus:border-brand dark:border-zinc-700 dark:bg-zinc-900" /></div>
          <div><div className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-zinc-400">Relay / feed prefix</div>
            <input value={cfg.proxy} onChange={e => setCfg(c => ({ ...c, proxy: e.target.value }))} placeholder="https://your-worker.workers.dev/?url=" className="w-full rounded-lg border border-zinc-200 bg-white px-3 py-2 font-mono text-[12px] outline-none focus:border-brand dark:border-zinc-700 dark:bg-zinc-900" /></div>
          <label className="flex cursor-pointer items-center justify-between">
            <span className="text-[13px] text-zinc-300">Auto-refresh every 24h</span>
            <button onClick={() => setCfg(c => ({ ...c, auto: !c.auto }))} className="relative h-5 w-9 rounded-full transition-colors" style={{ background: cfg.auto ? 'var(--brand-hex)' : '#3f3f46' }}><span className="absolute top-0.5 h-4 w-4 rounded-full bg-white transition-all" style={{ left: cfg.auto ? 16 : 2 }}></span></button>
          </label>
        </Card>
      )}

      {/* stats */}
      <div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
        <Card className="p-4"><div className="text-[11px] uppercase tracking-wide text-zinc-400">Sub cities</div><div className="mt-1 text-[24px] font-bold tabular-nums text-brand">{stats.total || '—'}</div></Card>
        <Card className="p-4"><div className="text-[11px] uppercase tracking-wide text-zinc-400">Free</div><div className="mt-1 text-[24px] font-bold tabular-nums text-emerald-400">{data ? stats.free : '—'}</div></Card>
        <Card className="p-4"><div className="text-[11px] uppercase tracking-wide text-zinc-400">Gold / Red</div><div className="mt-1 text-[24px] font-bold tabular-nums text-amber-400">{data ? stats.gold : '—'}</div></Card>
        <Card className="p-4"><div className="text-[11px] uppercase tracking-wide text-zinc-400">Top level</div><div className="mt-1 text-[24px] font-bold tabular-nums text-zinc-900 dark:text-white">{stats.maxLvl || '—'}</div></Card>
      </div>

      {/* filters */}
      <div className="flex flex-wrap items-center gap-2.5">
        <SearchInput value={q} onChange={setQ} placeholder="Search name, owner, buff…" className="w-full sm:w-64" />
        <select value={quality} onChange={e => setQuality(e.target.value)} className="h-9 rounded-lg border border-zinc-200 bg-white px-2.5 text-[13px] font-medium outline-none focus:border-brand dark:border-zinc-700 dark:bg-zinc-900">
          <option value="all">All qualities</option>{SUB_QUALITIES.map(x => <option key={x}>{x}</option>)}
        </select>
        <select value={culture} onChange={e => setCulture(e.target.value)} className="h-9 rounded-lg border border-zinc-200 bg-white px-2.5 text-[13px] font-medium outline-none focus:border-brand dark:border-zinc-700 dark:bg-zinc-900">
          <option value="all">All cultures</option>{SUB_CULTURES.map(x => <option key={x}>{x}</option>)}
        </select>
        <Segmented options={[{ value: 'all', label: 'All' }, { value: 'free', label: 'Free' }, { value: 'occupied', label: 'Occupied' }]} value={occ} onChange={setOcc} />
        <label className="flex items-center gap-1.5 text-[12px] text-zinc-400">Min lv <input type="number" min="0" max="50" value={lvlMin || ''} placeholder="0" onChange={e => setLvlMin(+e.target.value || 0)} className="h-9 w-16 rounded-lg border border-zinc-200 bg-white px-2 text-right text-[13px] tabular-nums outline-none focus:border-brand dark:border-zinc-700 dark:bg-zinc-900" /></label>
        <span className="ml-auto text-[12px] tabular-nums text-zinc-400">{rows.length} shown</span>
      </div>

      {/* table */}
      <Card className="overflow-x-auto p-0">
        {subs.length === 0 ? (
          <div className="px-6 py-14 text-center">
            <div className="mx-auto mb-3 flex h-12 w-12 items-center justify-center rounded-full bg-brand/10 text-brand"><Icon name="star" size={22} /></div>
            <div className="text-[14px] font-semibold text-zinc-900 dark:text-white">No sub-city data yet</div>
            <p className="mx-auto mt-1 max-w-md text-[12.5px] text-zinc-400">Hit <b>Sync now</b> to pull server 428's sub cities from the live source. If the public relay can't read the list (it's JavaScript-rendered), deploy the Worker in <span className="font-mono">integration/</span> and paste its URL under <b>Source</b>.</p>
          </div>
        ) : (
          <table className="w-full text-[13px]">
            <thead><tr className="border-b border-zinc-100 text-[10.5px] uppercase tracking-wide text-zinc-400 dark:border-zinc-800">
              <th className="px-4 py-2.5 text-left font-semibold">Sub city</th>
              <th className="px-3 py-2.5 text-left font-semibold">Quality</th>
              <th className="px-3 py-2.5 text-left font-semibold">Culture</th>
              <th className="px-3 py-2.5 text-right font-semibold">Level</th>
              <th className="px-3 py-2.5 text-left font-semibold">Status</th>
              <th className="px-3 py-2.5 text-right font-semibold">Coords</th>
            </tr></thead>
            <tbody>
              {rows.map((s, i) => (
                <tr key={i} className="border-b border-zinc-50 transition-colors hover:bg-zinc-50 dark:border-zinc-800/50 dark:hover:bg-zinc-800/40">
                  <td className="px-4 py-2">
                    <div className="font-medium text-zinc-900 dark:text-white">{s.name}</div>
                    {s.buff && <div className="max-w-xs truncate text-[11px] text-zinc-400">{s.buff}</div>}
                  </td>
                  <td className="px-3 py-2">{s.quality ? <span className="inline-flex items-center gap-1.5 text-[12.5px] font-medium" style={{ color: SUB_QUALITY_COLOR[s.quality] || undefined }}><span className="h-2 w-2 rounded-full" style={{ background: SUB_QUALITY_COLOR[s.quality] || '#777' }}></span>{s.quality}</span> : <span className="text-zinc-500">—</span>}</td>
                  <td className="px-3 py-2 text-zinc-600 dark:text-zinc-300">{s.culture || '—'}</td>
                  <td className="px-3 py-2 text-right font-bold tabular-nums text-zinc-900 dark:text-white">{s.level ?? '—'}</td>
                  <td className="px-3 py-2">{s.owner ? <Pill tone="red">{String(s.owner)}</Pill> : <Pill tone="green">Free</Pill>}</td>
                  <td className="px-3 py-2 text-right font-mono text-[12px] text-zinc-500">{s.x != null && s.y != null ? `${s.x}:${s.y}` : '—'}</td>
                </tr>
              ))}
              {rows.length === 0 && <tr><td colSpan="6" className="py-10 text-center text-zinc-400">No sub cities match your filters.</td></tr>}
            </tbody>
          </table>
        )}
      </Card>
    </div>
  );
}
window.SubsPage = SubsPage;
Object.assign(window, { parseSubsBody, runSubsSync });
