// Banks section on the Connections page. Renders the persistent list of
// connected Plaid Items + their Accounts (loaded from /api/plaid/list-items),
// the Connect-a-bank flow (Plaid Link → exchange-token), and the per-account
// mapping dropdown that writes through /api/plaid/map-account.
//
// House slugs are the app-facing identifier; UUIDs only round-trip through
// the API. The dropdown value is either a slug, '__management__', or '' (unmapped).
//
// Internal components (BankItemCard, AccountRow) live in this file so we
// don't have to thread script-tag load order in index.html.

const MGMT_VALUE = '__management__';

function BanksSection({ store }) {
  const [items, setItems]               = React.useState(null); // null = initial loading
  const [connectStatus, setStatus]      = React.useState('idle');
  const [connectError, setConnectError] = React.useState(null);
  const [syncing, setSyncing]           = React.useState(() => new Set()); // item ids currently syncing
  const [syncError, setSyncError]       = React.useState(null);
  const [confirmDisconnect, setConfirmDisconnect] = React.useState(null); // item or null
  const [disconnecting, setDisconnecting] = React.useState(false);

  const refresh = React.useCallback(async () => {
    try {
      const r = await fetch('/api/plaid/list-items');
      if (!r.ok) throw new Error('list-items HTTP ' + r.status);
      const { items } = await r.json();
      setItems(items);
    } catch (e) {
      console.error('list-items failed:', e);
      setItems([]);
    }
  }, []);

  React.useEffect(() => { refresh(); }, [refresh]);

  const onConnect = async () => {
    setConnectError(null);
    setStatus('loading');
    try {
      const r = await fetch('/api/plaid/create-link-token', { method: 'POST' });
      if (!r.ok) throw new Error('link-token HTTP ' + r.status);
      const { link_token } = await r.json();

      setStatus('connecting');
      const handler = window.Plaid.create({
        token: link_token,
        onSuccess: async (public_token, metadata) => {
          setStatus('exchanging');
          const ex = await fetch('/api/plaid/exchange-token', {
            method: 'POST',
            headers: { 'content-type': 'application/json' },
            body: JSON.stringify({ public_token, institution: metadata.institution }),
          });
          if (!ex.ok) {
            setStatus('error');
            setConnectError('Exchange failed: ' + (await ex.text()));
            return;
          }
          setStatus('idle');
          refresh();
        },
        onExit: (err) => {
          setStatus(s => (s === 'connecting' ? 'idle' : s));
          if (err) {
            setStatus('error');
            setConnectError(err.display_message || err.error_message || err.error_code || 'Plaid error');
          }
        },
      });
      handler.open();
    } catch (e) {
      setStatus('error');
      setConnectError(e.message);
    }
  };

  // Optimistic mapping update. value is '' | '__management__' | houseSlug.
  const onMap = async (plaid_account_id, value) => {
    let mapped_house_id, is_management;
    if (value === '')                       { mapped_house_id = null;                    is_management = false; }
    else if (value === MGMT_VALUE)          { mapped_house_id = null;                    is_management = true;  }
    else                                    { mapped_house_id = houseUuid(value) ?? null; is_management = false; }

    setItems(prev => prev.map(it => ({
      ...it,
      accounts: it.accounts.map(a =>
        a.plaid_account_id === plaid_account_id
          ? { ...a, mapped_house_id, is_management }
          : a
      ),
    })));

    const r = await fetch('/api/plaid/map-account', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ plaid_account_id, mapped_house_id, is_management }),
    });
    if (!r.ok) {
      console.error('map-account failed:', await r.text());
      refresh();
    }
  };

  const onReconnect = async (item_id) => {
    setSyncing(prev => new Set(prev).add(item_id));
    try {
      const tr = await fetch('/api/plaid/create-link-token', {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ item_id }),
      });
      if (!tr.ok) throw new Error('link-token HTTP ' + tr.status);
      const { link_token } = await tr.json();

      const handler = window.Plaid.create({
        token: link_token,
        onSuccess: async () => {
          // Update mode — no public_token to exchange. Run a sync so the
          // success path also clears requires_reauth → 'good' on the item.
          await fetch('/api/plaid/sync-transactions', {
            method: 'POST',
            headers: { 'content-type': 'application/json' },
            body: JSON.stringify({ item_id }),
          });
          await Promise.all([refresh(), store.reloadTransactions()]);
          setSyncing(prev => { const n = new Set(prev); n.delete(item_id); return n; });
        },
        onExit: () => {
          setSyncing(prev => { const n = new Set(prev); n.delete(item_id); return n; });
        },
      });
      handler.open();
    } catch (e) {
      console.error('reconnect failed:', e);
      setSyncing(prev => { const n = new Set(prev); n.delete(item_id); return n; });
    }
  };

  const onDisconnect = async () => {
    if (!confirmDisconnect) return;
    setDisconnecting(true);
    try {
      const r = await fetch('/api/plaid/remove-item', {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ item_id: confirmDisconnect.id }),
      });
      if (!r.ok) throw new Error('disconnect HTTP ' + r.status);
      setConfirmDisconnect(null);
      await Promise.all([refresh(), store.reloadTransactions()]);
    } catch (e) {
      console.error('disconnect failed:', e);
      setSyncError('Disconnect failed: ' + e.message);
      setConfirmDisconnect(null);
    } finally {
      setDisconnecting(false);
    }
  };

  const onSync = async (item_id) => {
    setSyncError(null);
    setSyncing(prev => new Set(prev).add(item_id));
    try {
      const r = await fetch('/api/plaid/sync-transactions', {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ item_id }),
      });
      if (!r.ok) throw new Error('sync HTTP ' + r.status);
      const { results } = await r.json();
      const failed = results.find(x => x.error);
      if (failed) throw new Error(failed.error.message || 'Plaid sync error');
      await Promise.all([refresh(), store.reloadTransactions()]);
    } catch (e) {
      console.error('sync failed:', e);
      setSyncError(e.message);
    } finally {
      setSyncing(prev => { const n = new Set(prev); n.delete(item_id); return n; });
    }
  };

  const busy = connectStatus === 'loading' || connectStatus === 'connecting' || connectStatus === 'exchanging';
  const buttonLabel =
      connectStatus === 'loading'    ? 'Loading…'
    : connectStatus === 'exchanging' ? 'Saving…'
    : items && items.length > 0      ? 'Connect another bank'
    :                                  'Connect a bank';

  return (
    <div className="card card-pad-lg">
      <div className="row between" style={{ alignItems: 'flex-start', gap: 16, marginBottom: items && items.length ? 16 : 0 }}>
        <div style={{ minWidth: 0 }}>
          <h3 style={{ margin: 0, fontSize: 17, fontWeight: 600 }}>Banks</h3>
          <div className="small muted" style={{ marginTop: 4, lineHeight: 1.5 }}>
            Map each account to a property so transactions auto-categorize.
            Use <strong>Management</strong> for shared expenses — those go to a review queue.
          </div>
        </div>
        <button className="btn accent" onClick={onConnect} disabled={busy}>
          {Icon.link} {buttonLabel}
        </button>
      </div>

      {items === null ? (
        <div className="small muted" style={{ padding: '20px 4px' }}>Loading banks…</div>
      ) : items.length === 0 ? (
        <div className="small muted" style={{ padding: '24px 4px', textAlign: 'center' }}>
          No banks connected yet.
        </div>
      ) : (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
          {items.map(item => (
            <BankItemCard
              key={item.id}
              item={item}
              houses={store.houses}
              onMap={onMap}
              onSync={() => onSync(item.id)}
              onReconnect={() => onReconnect(item.id)}
              onDisconnect={() => setConfirmDisconnect(item)}
              syncing={syncing.has(item.id)}
            />
          ))}
        </div>
      )}

      {syncError && (
        <div style={{
          marginTop: 14, padding: '10px 12px', borderRadius: 6, fontSize: 13,
          background: 'var(--bg-sunken)', color: 'var(--expense)',
        }}>
          Sync failed: {syncError}
        </div>
      )}

      {connectStatus === 'error' && (
        <div style={{
          marginTop: 14, padding: '10px 12px', borderRadius: 6, fontSize: 13,
          background: 'var(--bg-sunken)', color: 'var(--expense)',
        }}>
          {connectError}
        </div>
      )}

      {confirmDisconnect && (
        <ConfirmDisconnectModal
          item={confirmDisconnect}
          working={disconnecting}
          onCancel={() => setConfirmDisconnect(null)}
          onConfirm={onDisconnect}
        />
      )}
    </div>
  );
}

const STATUS_INFO = {
  good:            { label: 'Good',           color: 'var(--accent)',  bg: 'color-mix(in oklab, var(--accent) 14%, transparent)' },
  requires_reauth: { label: 'Reauth required', color: '#B8860B',        bg: 'color-mix(in oklab, #B8860B 14%, transparent)' },
  error:           { label: 'Error',          color: 'var(--expense)', bg: 'color-mix(in oklab, var(--expense) 14%, transparent)' },
};

function BankItemCard({ item, houses, onMap, onSync, onReconnect, onDisconnect, syncing }) {
  const info = STATUS_INFO[item.status] || STATUS_INFO.error;
  const lastSync = item.last_synced_at ? `Last synced ${relativeTime(item.last_synced_at)}` : 'Not synced yet';
  const errMsg = item.last_error?.error_message || item.last_error?.message;
  const tooltip = item.status === 'good'
    ? lastSync
    : `${info.label} · ${lastSync}${errMsg ? '\n\n' + errMsg : ''}`;

  return (
    <div style={{ border: '1px solid var(--border)', borderRadius: 10, overflow: 'hidden' }}>
      <div className="row between" style={{ padding: '12px 16px', background: 'var(--bg-sunken)', borderBottom: '1px solid var(--border)' }}>
        <div className="row" style={{ gap: 12, alignItems: 'center', minWidth: 0 }}>
          <div style={{
            width: 32, height: 32, borderRadius: 8,
            background: 'var(--text-tertiary)', color: 'white',
            display: 'grid', placeItems: 'center',
            fontFamily: 'var(--font-serif)', fontSize: 16, fontWeight: 500, flexShrink: 0,
          }}>
            {item.institution_name[0]}
          </div>
          <div style={{ minWidth: 0 }}>
            <div style={{ fontWeight: 600, fontSize: 14 }}>{item.institution_name}</div>
            <div className="xs muted">
              {item.last_synced_at ? `Last synced ${relativeTime(item.last_synced_at)}` : 'Not synced yet'}
            </div>
          </div>
        </div>
        <div className="row" style={{ gap: 8, alignItems: 'center', flexShrink: 0 }}>
          <button className="btn" onClick={onSync} disabled={syncing}>
            <span style={{ display: 'inline-flex', animation: syncing ? 'spin 0.8s linear infinite' : 'none' }}>
              {React.cloneElement(Icon.sync, { style: { width: 13, height: 13 } })}
            </span>
            {syncing ? 'Syncing…' : 'Sync now'}
          </button>
          <span className="pill" title={tooltip}
                style={{ background: info.bg, color: info.color, borderColor: 'transparent', cursor: 'help' }}>
            <span className="dot" style={{ background: info.color }}/>
            {info.label}
          </span>
          <BankActionsMenu item={item} onReconnect={onReconnect} onDisconnect={onDisconnect}/>
        </div>
      </div>

      <div>
        {item.accounts.map(a => <AccountRow key={a.id} account={a} houses={houses} onMap={onMap}/>)}
      </div>
    </div>
  );
}

function AccountRow({ account, houses, onMap }) {
  const value = account.is_management
              ? MGMT_VALUE
              : (account.mapped_house_id ? (houseSlug(account.mapped_house_id) ?? '') : '');
  const mappedHouse = !account.is_management && value ? houses.find(h => h.id === value) : null;

  const dotColor = account.is_management ? 'var(--text-tertiary)'
                 : mappedHouse ? mappedHouse.accentColor
                 : 'var(--text-quaternary)';

  return (
    <div className="row between" style={{
      padding: '10px 16px', gap: 12,
      borderTop: '0',
    }}>
      <div className="row" style={{ gap: 10, minWidth: 0, flex: 1, alignItems: 'center' }}>
        <span className="dot" style={{ background: dotColor, width: 8, height: 8, flexShrink: 0 }}/>
        <span className="tabular muted" style={{ fontSize: 12.5, fontFamily: 'var(--font-mono, ui-monospace, monospace)', flexShrink: 0 }}>
          ••{account.mask || '----'}
        </span>
        <span style={{
          fontSize: 13, fontWeight: 500,
          whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
        }}>
          {account.name}
        </span>
      </div>
      <select
        value={value}
        onChange={e => onMap(account.plaid_account_id, e.target.value)}
        style={{
          padding: '6px 10px',
          borderRadius: 6,
          border: '1px solid var(--border)',
          background: 'var(--bg-elev)',
          color: 'var(--text)',
          fontSize: 13,
          cursor: 'pointer',
          minWidth: 200,
          flexShrink: 0,
        }}
      >
        <option value="">— Unmapped —</option>
        <optgroup label="── Properties ──">
          {houses.map(h => <option key={h.id} value={h.id}>{h.name}</option>)}
        </optgroup>
        <optgroup label="── Other ──">
          <option value={MGMT_VALUE}>Management (Needs Review)</option>
        </optgroup>
      </select>
    </div>
  );
}

function BankActionsMenu({ item, onReconnect, onDisconnect }) {
  const [open, setOpen] = React.useState(false);
  const ref = React.useRef(null);

  React.useEffect(() => {
    if (!open) return;
    const handler = (e) => { if (!ref.current?.contains(e.target)) setOpen(false); };
    document.addEventListener('mousedown', handler);
    return () => document.removeEventListener('mousedown', handler);
  }, [open]);

  const itemBtn = {
    display: 'block', width: '100%',
    padding: '8px 12px', textAlign: 'left',
    border: 0, background: 'transparent',
    fontSize: 13, cursor: 'pointer', borderRadius: 4,
  };

  return (
    <div ref={ref} style={{ position: 'relative' }}>
      <button
        onClick={() => setOpen(o => !o)}
        title="More actions"
        style={{
          width: 28, height: 28, padding: 0,
          border: '1px solid var(--border)',
          borderRadius: 6, background: 'var(--bg-elev)',
          color: 'var(--text-secondary)',
          display: 'grid', placeItems: 'center', cursor: 'pointer',
        }}
      >
        <svg viewBox="0 0 24 24" width="14" height="14">
          <circle cx="5"  cy="12" r="1.8" fill="currentColor"/>
          <circle cx="12" cy="12" r="1.8" fill="currentColor"/>
          <circle cx="19" cy="12" r="1.8" fill="currentColor"/>
        </svg>
      </button>
      {open && (
        <div style={{
          position: 'absolute', top: 'calc(100% + 4px)', right: 0,
          background: 'var(--bg-elev)', border: '1px solid var(--border)',
          borderRadius: 8, boxShadow: '0 8px 24px rgba(0,0,0,0.12)',
          minWidth: 168, zIndex: 10, padding: 4,
        }}>
          <button
            onClick={() => { setOpen(false); onReconnect(); }}
            style={itemBtn}
            onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-sunken)'}
            onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
          >
            Reconnect{item.status === 'requires_reauth' ? ' (required)' : ''}
          </button>
          <div style={{ height: 1, background: 'var(--border)', margin: '4px 0' }}/>
          <button
            onClick={() => { setOpen(false); onDisconnect(); }}
            style={{ ...itemBtn, color: 'var(--expense)' }}
            onMouseEnter={e => e.currentTarget.style.background = 'color-mix(in oklab, var(--expense) 8%, transparent)'}
            onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
          >
            Disconnect…
          </button>
        </div>
      )}
    </div>
  );
}

function ConfirmDisconnectModal({ item, working, onCancel, onConfirm }) {
  return (
    <>
      <div onClick={working ? undefined : onCancel} style={{
        position: 'fixed', inset: 0, background: 'rgba(10,10,8,0.35)', zIndex: 100,
      }}/>
      <div role="dialog" aria-modal="true" style={{
        position: 'fixed', top: '50%', left: '50%',
        transform: 'translate(-50%, -50%)',
        background: 'var(--bg-elev)', border: '1px solid var(--border)',
        borderRadius: 12, padding: 24, width: 480, maxWidth: '92vw', zIndex: 101,
        boxShadow: '0 24px 60px rgba(0,0,0,0.18)',
      }}>
        <h3 style={{ margin: 0, fontSize: 17, fontWeight: 600 }}>
          Disconnect {item.institution_name}?
        </h3>
        <div className="small muted" style={{ marginTop: 12, lineHeight: 1.55 }}>
          Disconnecting <strong style={{ color: 'var(--text)' }}>{item.institution_name}</strong> will
          stop syncing new transactions. Existing imported transactions
          will <strong style={{ color: 'var(--text)' }}>remain in your records</strong>.
          You can reconnect any time to resume.
        </div>
        <div className="row" style={{ gap: 8, justifyContent: 'flex-end', marginTop: 22 }}>
          <button className="btn ghost" onClick={onCancel} disabled={working}>Cancel</button>
          <button
            className="btn"
            onClick={onConfirm}
            disabled={working}
            style={{
              background: 'var(--expense)', color: 'white',
              borderColor: 'var(--expense)',
            }}
          >
            {working ? 'Disconnecting…' : 'Disconnect'}
          </button>
        </div>
      </div>
    </>
  );
}

Object.assign(window, { BanksSection });
