/* ============================================================
   WalletTracker — live Hyperliquid wallet tracker
   ============================================================ */
const { useState, useEffect, useCallback } = React;
const D = window.WT_DATA;
const { Icon, StatusBar, AddRow, WalletCard, Leaderboard } = window.WT;

const LEGACY_LS_KEY = 'wallettracker.live.v1';
const PROFILE_ID_KEY = 'wallettracker.profileId.v1';
const WALLET_STORE_PREFIX = 'wallettracker.profile.';
const BACKUP_PARAM = 'watchlist';
const BACKUP_VERSION = 1;

function normalizeWalletRow(row) {
  const address = String(row?.address || '').trim();
  if (!D.isAddress(address)) return null;
  const identity = D.identityForAddress(address);
  return {
    id: address.toLowerCase(),
    name: String(row.name || '').trim() || D.shortAddr(address),
    emoji: row.emoji || identity.emoji,
    color: row.color || identity.color,
    avatar: row.avatar || identity.avatar,
    tag: String(row.tag || '').trim(),
    address,
  };
}

function normalizeWalletRows(value) {
  const rows = Array.isArray(value) ? value : Array.isArray(value?.wallets) ? value.wallets : [];
  return rows.map(normalizeWalletRow).filter(Boolean);
}

function base64UrlEncode(text) {
  const bytes = new TextEncoder().encode(text);
  let binary = '';
  bytes.forEach((byte) => { binary += String.fromCharCode(byte); });
  return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
}

function base64UrlDecode(value) {
  const input = String(value || '').trim().replace(/-/g, '+').replace(/_/g, '/');
  const padded = input + '='.repeat((4 - input.length % 4) % 4);
  const binary = atob(padded);
  const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
  return new TextDecoder().decode(bytes);
}

function slimWallet(wallet) {
  return normalizeWalletRow(wallet);
}

function encodeWalletBackup(wallets) {
  const payload = {
    v: BACKUP_VERSION,
    savedAt: new Date().toISOString(),
    wallets: wallets.map(slimWallet).filter(Boolean),
  };
  return base64UrlEncode(JSON.stringify(payload));
}

function backupCodeFromUrl(urlText) {
  try {
    const url = new URL(urlText, window.location.href);
    const queryCode = url.searchParams.get(BACKUP_PARAM);
    if (queryCode) return queryCode;
    const hash = String(url.hash || '').replace(/^#/, '');
    if (!hash) return '';
    if (hash.startsWith(BACKUP_PARAM + '=')) return new URLSearchParams(hash).get(BACKUP_PARAM) || '';
    if (hash.startsWith(BACKUP_PARAM + ':')) return hash.slice(BACKUP_PARAM.length + 1);
  } catch (e) {}
  return '';
}

function decodeWalletBackup(raw) {
  const input = String(raw || '').trim();
  if (!input) return [];
  const candidate = backupCodeFromUrl(input) || input.replace(/^wallettracker:/i, '');
  let parsed;
  try {
    parsed = JSON.parse(candidate);
  } catch (jsonError) {
    parsed = JSON.parse(base64UrlDecode(candidate));
  }
  return normalizeWalletRows(parsed);
}

function walletBackupFromLocation() {
  const code = backupCodeFromUrl(window.location.href);
  return code ? decodeWalletBackup(code) : [];
}

function buildBackupLink(wallets) {
  const url = new URL(window.location.href);
  url.searchParams.set(BACKUP_PARAM, encodeWalletBackup(wallets));
  url.hash = '';
  return url.toString();
}

function copyText(text) {
  if (navigator.clipboard?.writeText) return navigator.clipboard.writeText(text);
  const area = document.createElement('textarea');
  area.value = text;
  area.setAttribute('readonly', '');
  area.style.position = 'fixed';
  area.style.left = '-9999px';
  document.body.appendChild(area);
  area.select();
  document.execCommand('copy');
  document.body.removeChild(area);
  return Promise.resolve();
}

function createProfileId() {
  if (window.crypto && typeof window.crypto.randomUUID === 'function') return window.crypto.randomUUID();
  return 'local-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2);
}

function localProfileId() {
  try {
    const existing = localStorage.getItem(PROFILE_ID_KEY);
    if (existing) return existing;
    const next = createProfileId();
    localStorage.setItem(PROFILE_ID_KEY, next);
    return next;
  } catch (e) {
    return createProfileId();
  }
}

const LOCAL_PROFILE_ID = localProfileId();
const LS_KEY = WALLET_STORE_PREFIX + LOCAL_PROFILE_ID + '.wallets.v1';

function parseWalletRows(raw) {
  if (!raw) return [];
  return normalizeWalletRows(JSON.parse(raw));
}

function loadState() {
  try {
    const backupRows = walletBackupFromLocation();
    if (backupRows.length) {
      localStorage.setItem(LS_KEY, JSON.stringify(backupRows));
      localStorage.removeItem(LEGACY_LS_KEY);
      return backupRows;
    }

    const profileRows = parseWalletRows(localStorage.getItem(LS_KEY));
    if (profileRows.length) return profileRows;

    const legacyRows = parseWalletRows(localStorage.getItem(LEGACY_LS_KEY));
    if (legacyRows.length) {
      localStorage.setItem(LS_KEY, JSON.stringify(legacyRows));
      localStorage.removeItem(LEGACY_LS_KEY);
    }
    return legacyRows;
  } catch (e) {
    return [];
  }
}

function saveState(wallets) {
  try {
    const slim = wallets.map(slimWallet).filter(Boolean);
    localStorage.setItem(LS_KEY, JSON.stringify(slim));
    localStorage.removeItem(LEGACY_LS_KEY);
  } catch (e) {}
}

function loadingWallet(meta) {
  const identity = D.identityForAddress(meta.address || '');
  return D.fallbackWallet(Object.assign({
    id: (meta.address || String(Date.now())).toLowerCase(),
    name: meta.name || D.shortAddr(meta.address || ''),
    emoji: meta.emoji || identity.emoji,
    color: meta.color || identity.color,
    avatar: meta.avatar || identity.avatar,
    tag: meta.tag || '',
  }, meta), 'loading');
}

function metaFromWallet(wallet) {
  return {
    id: wallet.id || String(wallet.address || '').toLowerCase(),
    address: wallet.address,
    name: wallet.name,
    emoji: wallet.emoji,
    color: wallet.color,
    avatar: wallet.avatar,
    tag: wallet.tag,
  };
}

function App() {
  const [wallets, setWallets] = useState(() => loadState().map(loadingWallet));
  const [expandedId, setExpandedId] = useState(null);
  const [range, setRange] = useState('24h');
  const [secondsAgo, setSecondsAgo] = useState(0);
  const [refreshing, setRefreshing] = useState(false);
  const [toast, setToast] = useState(null);

  useEffect(() => {
    saveState(wallets.filter((wallet) => D.isAddress(wallet.address)));
  }, [wallets]);

  const showToast = useCallback((msg) => {
    setToast(msg);
    setTimeout(() => setToast(null), 2600);
  }, []);

  const upsertWallet = useCallback((wallet) => {
    setWallets((current) => {
      const exists = current.some((row) => row.id === wallet.id);
      if (!exists) return [wallet, ...current];
      return current.map((row) => row.id === wallet.id ? wallet : row);
    });
  }, []);

  const refreshWallet = useCallback(async (meta, options) => {
    const previous = wallets.find((wallet) => wallet.id === meta.id);
    if (!previous && options?.insertLoading) upsertWallet(loadingWallet(meta));
    try {
      const live = await D.fetchWallet(meta);
      upsertWallet(live);
      return live;
    } catch (error) {
      const failed = D.fallbackWallet(Object.assign({}, previous || meta), 'error', error);
      upsertWallet(failed);
      throw error;
    }
  }, [upsertWallet, wallets]);

  const refreshAll = useCallback(async (silent) => {
    const metas = wallets
      .filter((wallet) => D.isAddress(wallet.address))
      .map(metaFromWallet);
    if (!metas.length) return;
    setRefreshing(true);
    try {
      await Promise.allSettled(metas.map((meta) => refreshWallet(meta)));
      setSecondsAgo(0);
      if (!silent) showToast('Live Hyperliquid data refreshed');
    } finally {
      setRefreshing(false);
    }
  }, [refreshWallet, showToast, wallets]);

  useEffect(() => {
    if (!wallets.length) return undefined;
    refreshAll(true);
  // Run once after persisted wallets hydrate. refreshAll depends on wallets by design.
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  useEffect(() => {
    const clock = setInterval(() => setSecondsAgo((seconds) => seconds + 1), 1000);
    const tick = setInterval(() => refreshAll(true), D.REFRESH_MS);
    return () => {
      clearInterval(clock);
      clearInterval(tick);
    };
  }, [refreshAll]);

  const addWallet = useCallback(async (meta) => {
    const address = String(meta.address || '').trim();
    if (!D.isAddress(address)) {
      showToast('Enter a full 42-character wallet address');
      return false;
    }
    const id = address.toLowerCase();
    const existing = wallets.find((wallet) => wallet.id === id);
    if (existing) {
      setExpandedId(id);
      showToast('Already tracking ' + existing.name);
      return false;
    }
    const identity = D.identityForAddress(address);
    const walletMeta = {
      id,
      address,
      name: String(meta.name || '').trim() || D.shortAddr(address),
      emoji: meta.emoji || identity.emoji,
      color: meta.color || identity.color,
      avatar: meta.avatar || identity.avatar,
      tag: meta.tag || '',
    };
    try {
      await refreshWallet(walletMeta, { insertLoading: true });
      setExpandedId(id);
      setSecondsAgo(0);
      showToast('Tracking live wallet ' + walletMeta.name);
      return true;
    } catch (error) {
      showToast(error.message || 'Unable to fetch wallet');
      return false;
    }
  }, [refreshWallet, showToast, wallets]);

  const removeWallet = useCallback((id) => {
    setWallets((current) => current.filter((wallet) => wallet.id !== id));
    setExpandedId((current) => current === id ? null : current);
  }, []);

  const editWallet = useCallback((id, patch) => {
    setWallets((current) => current.map((wallet) => wallet.id === id ? Object.assign({}, wallet, patch) : wallet));
  }, []);

  const walletMetas = useCallback(() => wallets
    .filter((wallet) => D.isAddress(wallet.address))
    .map(metaFromWallet), [wallets]);

  const copyBackupLink = useCallback(async () => {
    const metas = walletMetas();
    if (!metas.length) {
      showToast('Add a wallet before creating a backup link');
      return;
    }
    await copyText(buildBackupLink(metas));
    showToast('Copied permanent wallet backup link');
  }, [showToast, walletMetas]);

  const downloadBackup = useCallback(() => {
    const metas = walletMetas();
    if (!metas.length) {
      showToast('Add a wallet before saving a backup');
      return;
    }
    const payload = {
      v: BACKUP_VERSION,
      savedAt: new Date().toISOString(),
      wallets: metas.map(slimWallet).filter(Boolean),
    };
    const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
    const link = document.createElement('a');
    link.href = URL.createObjectURL(blob);
    link.download = 'hypurrinfo-wallets.json';
    document.body.appendChild(link);
    link.click();
    URL.revokeObjectURL(link.href);
    document.body.removeChild(link);
    showToast('Saved wallet backup file');
  }, [showToast, walletMetas]);

  const restoreBackup = useCallback(async (raw) => {
    let rows = [];
    try {
      rows = decodeWalletBackup(raw);
    } catch (error) {
      showToast('Invalid wallet backup');
      return false;
    }
    if (!rows.length) {
      showToast('No wallets found in backup');
      return false;
    }
    saveState(rows);
    setWallets(rows.map((row) => D.fallbackWallet(row, 'saved')));
    setExpandedId(null);
    setSecondsAgo(0);
    showToast('Restored ' + rows.length + ' tracked wallet' + (rows.length === 1 ? '' : 's'));
    Promise.allSettled(rows.map((row) => refreshWallet(row))).finally(() => setSecondsAgo(0));
    return true;
  }, [refreshWallet, showToast]);

  const toggle = useCallback((id) => {
    setExpandedId((current) => current === id ? null : id);
  }, []);

  const jump = useCallback((id) => {
    setExpandedId(id);
    setTimeout(() => {
      const el = document.getElementById('card-' + id);
      if (el) window.scrollTo({ top: el.getBoundingClientRect().top + window.scrollY - 90, behavior: 'smooth' });
    }, 50);
  }, []);

  const totalEquity = wallets.reduce((sum, wallet) => sum + D.numeric(wallet.equity), 0);
  const lastUpdated = wallets
    .map((wallet) => wallet.lastUpdated ? Date.parse(wallet.lastUpdated) : 0)
    .filter(Boolean)
    .sort((a, b) => b - a)[0];

  return (
    <div className="app">
      <nav className="rail">
        <div className="logo"><img src="assets/brand/hl-symbol-mint.svg" alt="" /></div>
        <button className="ri active" title="Wallet Tracker" aria-label="Wallet Tracker">
          <Icon name="users" />
        </button>
      </nav>

      <div className="main">
        <StatusBar
          secondsAgo={secondsAgo}
          walletCount={wallets.length}
          totalEquity={totalEquity}
          refreshing={refreshing}
          lastUpdated={lastUpdated}
          onRefresh={() => refreshAll(false)}
        />

        <WalletTrackerPage
          wallets={wallets}
          expandedId={expandedId}
          range={range}
          setRange={setRange}
          addWallet={addWallet}
          removeWallet={removeWallet}
          editWallet={editWallet}
          copyBackupLink={copyBackupLink}
          downloadBackup={downloadBackup}
          restoreBackup={restoreBackup}
          toggle={toggle}
          jump={jump}
          refreshWallet={refreshWallet}
        />
      </div>

      {toast && (
        <div className="toast"><Icon name="check" /> {toast}</div>
      )}
    </div>
  );
}

function WalletTrackerPage({ wallets, expandedId, range, setRange, addWallet, removeWallet, editWallet, copyBackupLink, downloadBackup, restoreBackup, toggle, jump, refreshWallet }) {
  const ordered = expandedId
    ? [...wallets].sort((a, b) => (a.id === expandedId ? -1 : b.id === expandedId ? 1 : 0))
    : wallets;

  return (
    <div className="body">
      <section>
        <div className="page-head brand-head">
          <div className="brand-copy">
            <h1>Hypurrinfo <span>· {wallets.length} tracked</span></h1>
            <div className="sub">Live Hyperliquid account state, positions, portfolio PnL, fills, and funding for tracked wallets.</div>
          </div>
          <div className="brand-art" aria-hidden="true">
            <img className="brand-core" src="assets/brand/hypercore-mint.svg" alt="" />
          </div>
        </div>

        <AddRow onAdd={addWallet} />

        <div className="grid">
          {ordered.length === 0 && (
            <div className="empty">
              <img className="empty-mascot" src="assets/hypurr/hypurr.png" alt="" />
              <p>No wallets tracked yet.<br />Paste a full 0x address above and hit <b style={{ color: 'var(--accent)' }}>Track</b> to fetch live Hyperliquid data.</p>
            </div>
          )}
          {ordered.map((wallet) => (
            <div id={'card-' + wallet.id} key={wallet.id} style={expandedId === wallet.id ? { gridColumn: '1 / -1' } : null}>
              <WalletCard
                w={wallet}
                expanded={expandedId === wallet.id}
                onToggle={toggle}
                onEdit={editWallet}
                onRemove={removeWallet}
                onRefresh={() => refreshWallet(wallet)}
              />
            </div>
          ))}
        </div>
      </section>

      <aside className="aside">
        <Leaderboard wallets={wallets} range={range} onRange={setRange} onJump={jump} />
        <BackupPanel walletCount={wallets.length} onCopyLink={copyBackupLink} onDownload={downloadBackup} onRestore={restoreBackup} />
      </aside>
    </div>
  );
}

function BackupPanel({ walletCount, onCopyLink, onDownload, onRestore }) {
  const [raw, setRaw] = useState('');
  const [busy, setBusy] = useState(false);

  async function restore() {
    if (!raw.trim() || busy) return;
    setBusy(true);
    try {
      const restored = await onRestore(raw);
      if (restored) setRaw('');
    } finally {
      setBusy(false);
    }
  }

  return (
    <div className="panel backup-panel">
      <div className="ph"><div className="t"><Icon name="wallet" /> Wallet Backup</div></div>
      <div className="backup-body">
        <div className="backup-actions">
          <button onClick={onCopyLink} disabled={!walletCount}>Copy Link</button>
          <button onClick={onDownload} disabled={!walletCount}>Download</button>
        </div>
        <textarea
          value={raw}
          onChange={(e) => setRaw(e.target.value)}
          placeholder="Paste backup link or code"
          spellCheck="false"
        />
        <button className="restore-btn" onClick={restore} disabled={!raw.trim() || busy}>
          {busy ? 'Restoring...' : 'Restore'}
        </button>
      </div>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<App />);
