// DbWorkspaceOverview.jsx · R47.1 (2026-05-28)
//
// Live-DB workspace overview · top-level "domain catalog" view.
// Self-mounts on ?screen=db-workspace&workspace_id=<id>. Reads from
// /api/v1/projects?workspace_id=... and renders each project as a clickable
// tile representing one MB-P domain.
//
//   ┌──────────────────────────────────────────────────────────────────────┐
//   │  ← cockpit                              workspace: Xlooop Internal   │
//   │  org_3EIO8YYcUTtjOQ9d1i7eh8mvBFv · 8 domains · 15 events             │
//   │                                                                      │
//   │  ┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────┐│
//   │  │ MB-P governance     │ │ Unified intake      │ │ Private areas ││
//   │  │ Operate · on-track  │ │ Validate · on-track │ │ Validate · ...  ││
//   │  │ scope bound         │ │ no binding          │ │ no binding      ││
//   │  │ 7 matched events    │ │ 1 matched event     │ │ 1 matched event ││
//   │  │ [open ›]            │ │ [open ›]            │ │ [open ›]        ││
//   │  └─────────────────────┘ └─────────────────────┘ └─────────────────┘│
//   │  …                                                                   │
//   └──────────────────────────────────────────────────────────────────────┘
//
// Clicking a tile navigates to ?screen=db-project&project_id=<id> which
// opens the R47 single-project detail view.

(function () {
  'use strict';

  if (typeof window === 'undefined') return;
  if (window.__DbWorkspaceOverviewMounted) return;
  window.__DbWorkspaceOverviewMounted = true;

  var React = window.React;
  var ReactDOM = window.ReactDOM;
  if (!React || !ReactDOM) {
    window.addEventListener('xcp:data-ready', mount, { once: true });
    return;
  }
  mount();

  function mount() {
    var container = document.createElement('div');
    container.id = 'xcp-db-workspace-overview-portal';
    container.style.cssText = 'position:fixed;inset:0;z-index:99989;pointer-events:none;';
    document.body.appendChild(container);
    var root = ReactDOM.createRoot ? ReactDOM.createRoot(container) : null;
    if (!root) { try { ReactDOM.render(React.createElement(Gate), container); } catch (e) { void e; } return; }
    root.render(React.createElement(Gate));
  }

  function Gate() {
    var st = React.useState(parseUrl());
    var url = st[0]; var setUrl = st[1];
    React.useEffect(function () {
      function onChange() { setUrl(parseUrl()); }
      window.addEventListener('popstate', onChange);
      var iv = setInterval(onChange, 1500);
      return function () { window.removeEventListener('popstate', onChange); clearInterval(iv); };
    }, []);
    if (url.screen !== 'db-workspace') return null;
    // R53-W3 · CONVERGENCE. There were two workspace views: this live-DB tile
    // surface (?screen=db-workspace, reachable ONLY by typing the URL — never
    // linked) and the canonical operator view ?screen=workspace
    // (DetailedWorkspaceShellDesign). They are different lenses: this one reads
    // live-DB project_ids; the canonical view reads governance read-model ids.
    // To answer "why do we have 2 versions", db-workspace now REDIRECTS to the
    // canonical view so the operator sees ONE workspace.
    //
    // The live-DB inspector (LiveDbWorkspaceView + EmptyState below) is kept in
    // this file — NOT deleted — so it's reversible: to inspect the raw DB view
    // again, comment out the redirect block below and the inspector returns.
    // (A ?raw=1 runtime escape hatch was tried but the overlay portal fights the
    // main app's URL-rewriter, so it isn't reliable; reverting the redirect is.)
    try {
      var wsForRedirect = url.workspaceId || 'xlooop';
      var loc = window.location;
      loc.replace(loc.origin + loc.pathname + '?screen=workspace&ws=' + encodeURIComponent(wsForRedirect));
    } catch (_) {}
    return null;
  }

  function parseUrl() {
    try {
      var u = new URL(window.location.href);
      return {
        screen: u.searchParams.get('screen'),
        workspaceId: u.searchParams.get('workspace_id') || u.searchParams.get('ws'),
      };
    } catch (_) { return {}; }
  }

  function EmptyState() {
    return el('div', { style: emptyStyle() },
      el('div', { style: { maxWidth: 560, textAlign: 'center', padding: 32 } },
        el('h1', { style: titleH1Style() }, 'Live DB workspace overview'),
        el('p', null, 'Missing ', el('code', null, 'workspace_id'), ' query param.'),
        el('p', { style: { opacity: 0.5, fontSize: 13 } },
          'Try: ', el('code', null, '?screen=db-workspace&workspace_id=org_3EIO8YYcUTtjOQ9d1i7eh8mvBFv')),
        backLink(),
      ),
    );
  }

  function LiveDbWorkspaceView(props) {
    var workspaceId = props.workspaceId;
    var [projects, setProjects] = useState([]);
    var [loading, setLoading] = useState(true);
    var [error, setError] = useState(null);
    var [eventCounts, setEventCounts] = useState({});  // { projectId: count }
    var [provenance, setProvenance] = useState({});    // R52-A2 · { projectId: sources[] }
    var [order, setOrder] = useState(null);            // R52-B2 · operator project order (null=unloaded)
    var [session, setSession] = useState(null);

    // R52-B2 · operator-controlled project order (pillar 3). Progressive
    // enhancement: localStorage persists immediately (works today, no DB);
    // GET/PUT /api/v1/layout durably persists once migration 012 is live.
    // Overlay-safe: a project not in the saved order still renders (appended).
    var LS_KEY = 'xcp.operator_layout.project_order.' + workspaceId;
    function loadSavedOrder() {
      try { var raw = window.localStorage.getItem(LS_KEY); return raw ? JSON.parse(raw) : []; }
      catch (e) { void e; return []; }
    }
    function persistOrder(ids) {
      try { window.localStorage.setItem(LS_KEY, JSON.stringify(ids)); } catch (e) { void e; }
      // Best-effort durable persist (no-op if /layout not yet live).
      authHeaders().then(function (headers) {
        if (!headers || !headers.Authorization) return;
        var body = {}; body[workspaceId] = ids;
        fetch(apiBase() + '/api/v1/layout', {
          method: 'PUT', headers: Object.assign({ 'Content-Type': 'application/json' }, headers),
          body: JSON.stringify({ version: 1, project_order: body }),
        }).catch(function () {});
      }).catch(function () {});
    }
    // Apply saved order as an overlay: ordered ones first (in saved order),
    // then any project not yet in the order, appended in arrival order.
    function applyOrder(rows, savedOrder) {
      if (!Array.isArray(savedOrder) || savedOrder.length === 0) return rows;
      var byId = {}; rows.forEach(function (r) { byId[r.id] = r; });
      var out = [];
      savedOrder.forEach(function (id) { if (byId[id]) { out.push(byId[id]); delete byId[id]; } });
      rows.forEach(function (r) { if (byId[r.id]) out.push(r); });
      return out;
    }

    React.useEffect(function () {
      var s = window.xcp && window.xcp.store && window.xcp.store.useSession
        ? window.xcp.store.useSession() : null;
      setSession(s);
    }, []);

    React.useEffect(function () {
      var cancelled = false;
      async function load() {
        try {
          setLoading(true);
          var headers = await authHeaders();
          var res = await fetch(apiBase() + '/api/v1/projects?workspace_id=' + encodeURIComponent(workspaceId), { headers: headers });
          if (!res.ok) {
            var body = await res.json().catch(function () { return null; });
            throw new Error((body && body.error) || ('HTTP ' + res.status));
          }
          var data = await res.json();
          if (cancelled) return;
          // R47.3 · only show ROOT projects as top-level tiles. Children
          // (parent_project_id !== null) are revealed inside the parent's
          // R47 detail view, not at the workspace level.
          var allRows = Array.isArray(data.projects) ? data.projects : [];
          var rootRows = allRows.filter(function (p) { return !p.parent_project_id; });
          setProjects(rootRows);
          setOrder(loadSavedOrder());   // R52-B2 · apply operator's saved order
          setLoading(false);
          // After projects render, fan out fetches for event counts + source
          // provenance (parallel · R52-A2).
          rootRows.forEach(function (p) { fetchEventCount(p.id, headers); fetchProvenance(p.id, headers); });
        } catch (e) {
          if (cancelled) return;
          setError(e && e.message ? e.message : String(e));
          setLoading(false);
        }
      }
      function fetchEventCount(projectId, headers) {
        fetch(apiBase() + '/api/v1/projects/' + encodeURIComponent(projectId) + '/events?limit=100', { headers: headers })
          .then(function (r) { return r.ok ? r.json() : { events: [] }; })
          .then(function (d) {
            if (cancelled) return;
            var count = Array.isArray(d.events) ? d.events.length : 0;
            setEventCounts(function (prev) { var n = Object.assign({}, prev); n[projectId] = count; return n; });
          })
          .catch(function () {});
      }
      // R52-A2 · source→project provenance · powers the source chips on each tile.
      function fetchProvenance(projectId, headers) {
        fetch(apiBase() + '/api/v1/projects/' + encodeURIComponent(projectId) + '/provenance', { headers: headers })
          .then(function (r) { return r.ok ? r.json() : { sources: [] }; })
          .then(function (d) {
            if (cancelled) return;
            var sources = Array.isArray(d.sources) ? d.sources : [];
            setProvenance(function (prev) { var n = Object.assign({}, prev); n[projectId] = sources; return n; });
          })
          .catch(function () {});
      }
      load();
      return function () { cancelled = true; };
    }, [workspaceId]);

    return el('div', { style: shellStyle() },
      // top bar
      el('div', { style: topBarStyle() },
        backLink(),
        el('span', { style: { flex: 1 } }),
        el('span', { style: { opacity: 0.5, fontSize: 12 } }, 'LIVE DB workspace · ' + workspaceId.slice(0, 16) + '…'),
      ),
      el('div', { style: bodyStyle() },
        // Header
        el('div', { style: { marginBottom: 24 } },
          el('div', { style: { fontSize: 11, opacity: 0.5, fontFamily: 'JetBrains Mono, ui-monospace, monospace', letterSpacing: '0.06em', textTransform: 'uppercase' } },
            'WORKSPACE · /api/v1/projects?workspace_id=' + workspaceId.slice(0, 20) + '…'),
          el('h1', { style: titleH1Style() }, session && session.workspace_name ? session.workspace_name : 'Workspace overview'),
          el('div', { style: { opacity: 0.55, fontSize: 13, marginTop: 4 } },
            projects.length + ' area' + (projects.length === 1 ? '' : 's') +
            ' · session role: ' + (session && session.role ? session.role : '—')),
        ),
        loading
          ? el('div', { style: { padding: 40, opacity: 0.5 } }, 'Loading areas from DB…')
          : error
            ? el('div', { style: errStyle() }, error)
            : projects.length === 0
              ? el('div', { style: { padding: 40, opacity: 0.6 } },
                  'No projects in this workspace yet. Seed via ',
                  el('code', null, 'src/workers/db/seed/operator-mbp-projects.sql'),
                  ' or use MCP ', el('code', null, 'xlooop.event.append'), ' to start.')
              : (function () {
                  // R52-B2 · render in the operator's saved order (overlay).
                  var ordered = applyOrder(projects, order || []);
                  function moveProject(projectId, dir) {
                    var ids = ordered.map(function (r) { return r.id; });
                    var i = ids.indexOf(projectId);
                    var j = i + dir;
                    if (i < 0 || j < 0 || j >= ids.length) return;
                    var tmp = ids[i]; ids[i] = ids[j]; ids[j] = tmp;
                    setOrder(ids);
                    persistOrder(ids);
                  }
                  return el('div', { style: tilesGridStyle() },
                    ordered.map(function (p, idx) {
                      return el(DomainTile, {
                        key: p.id,
                        project: p,
                        eventCount: eventCounts[p.id],
                        sources: provenance[p.id],
                        canUp: idx > 0,
                        canDown: idx < ordered.length - 1,
                        onMove: moveProject,
                      });
                    }));
                })(),
        // Footer · helper
        el('div', { style: footerStyle() },
          'R47.1 live-DB workspace view · click any area to open detail + configure scope binding · R48 will unify this with the main cockpit'),
      ),
    );
  }

  function DomainTile(props) {
    var p = props.project;
    var binding = p.scope_binding;
    var hasBinding = !!binding && Array.isArray(binding.filters) && binding.filters.length > 0;
    var stage = p.metadata && p.metadata.stage;
    var health = p.metadata && p.metadata.health;

    function open() {
      var u = new URL(window.location.href);
      u.searchParams.set('screen', 'db-project');
      u.searchParams.set('project_id', p.id);
      window.history.pushState({}, '', u.toString());
      window.dispatchEvent(new Event('popstate'));
    }

    return el('div', { style: tileStyle(), onClick: open, role: 'button', tabIndex: 0,
                       onKeyDown: function (e) { if (e.key === 'Enter') open(); } },
      el('div', { style: tileTopStyle() },
        el('span', { style: tileNameStyle() }, p.name),
        hasBinding ? el('span', { style: bindChip() }, '⛓ scoped') : el('span', { style: noBindChip() }, 'no binding'),
      ),
      p.description ? el('div', { style: tileDescStyle() }, p.description) : null,
      el('div', { style: tileMetaStyle() },
        stage ? el('span', { style: metaPill('stage', stage) }, stage) : null,
        health ? el('span', { style: metaPill('health', health) }, health) : null,
        el('span', { style: metaPill('status', p.status) }, p.status),
      ),
      // R52-A2 · source provenance chips — "connected from any source" made
      // visible. Foregrounds OAuth sources (github/drive/dropbox/...); each
      // chip shows the provider + event count it contributed.
      sourceChips(props.sources),
      el('div', { style: tileBottomStyle() },
        el('span', { style: countStyle() },
          props.eventCount === undefined ? '…' : props.eventCount + ' event' + (props.eventCount === 1 ? '' : 's')),
        // R52-B2 · reorder affordances (pillar 3). stopPropagation so they
        // don't trigger the tile's open-click.
        el('span', { style: reorderGroupStyle() },
          el('button', {
            type: 'button', 'aria-label': 'Move up', disabled: !props.canUp,
            style: reorderBtnStyle(props.canUp),
            onClick: function (e) { e.stopPropagation(); if (props.canUp && props.onMove) props.onMove(p.id, -1); },
          }, '↑'),
          el('button', {
            type: 'button', 'aria-label': 'Move down', disabled: !props.canDown,
            style: reorderBtnStyle(props.canDown),
            onClick: function (e) { e.stopPropagation(); if (props.canDown && props.onMove) props.onMove(p.id, 1); },
          }, '↓'),
        ),
        el('span', { style: openArrow() }, 'open ›'),
      ),
    );
  }
  function reorderGroupStyle() { return { display: 'inline-flex', gap: 3, marginLeft: 'auto', marginRight: 8 }; }
  function reorderBtnStyle(enabled) {
    return { width: 22, height: 22, borderRadius: 6, cursor: enabled ? 'pointer' : 'default',
      background: 'rgba(255,255,255,0.06)', color: enabled ? 'rgba(255,255,255,0.8)' : 'rgba(255,255,255,0.2)',
      border: '1px solid rgba(255,255,255,0.12)', fontSize: 12, lineHeight: '20px', padding: 0,
      opacity: enabled ? 1 : 0.4 };
  }

  // R52-A2 · source provenance chips. props.sources is the array from
  // GET /projects/:id/provenance ([] while loading or none). OAuth sources
  // (is_oauth_source) render first + highlighted; internal tools muted.
  var SOURCE_LABEL = {
    github: 'GitHub', google_drive: 'Drive', dropbox: 'Dropbox',
    gitlab: 'GitLab', microsoft_onedrive: 'OneDrive',
    codex: 'Codex', claude: 'Claude', harness: 'Harness',
    mbp: 'MB-P', xlooop: 'Xlooop', operator: 'Operator',
  };
  function sourceChips(sources) {
    if (sources === undefined) {
      return el('div', { style: chipRowStyle() }, el('span', { style: chipLoadingStyle() }, 'sources…'));
    }
    if (!Array.isArray(sources) || sources.length === 0) {
      return el('div', { style: chipRowStyle() }, el('span', { style: chipNoneStyle() }, 'no sources yet'));
    }
    // OAuth sources first (the "connected from any source" story), then internal.
    var ordered = sources.slice().sort(function (a, b) {
      if (a.is_oauth_source !== b.is_oauth_source) return a.is_oauth_source ? -1 : 1;
      return (b.event_count || 0) - (a.event_count || 0);
    });
    return el('div', { style: chipRowStyle() },
      ordered.slice(0, 5).map(function (s) {
        var label = SOURCE_LABEL[s.source_tool] || s.source_tool;
        return el('span', {
          key: s.source_tool,
          style: s.is_oauth_source ? oauthChipStyle() : internalChipStyle(),
          title: s.source_tool + ' · ' + (s.event_count || 0) + ' events' + (s.last_event_at ? ' · last ' + new Date(s.last_event_at).toLocaleString() : ''),
        }, label + ' ' + (s.event_count || 0));
      }));
  }
  function chipRowStyle() { return { display: 'flex', flexWrap: 'wrap', gap: 4, marginTop: 8 }; }
  function oauthChipStyle() {
    return { fontSize: 10, fontFamily: 'JetBrains Mono, ui-monospace, monospace', padding: '2px 7px',
      borderRadius: 999, background: 'rgba(80,160,120,0.16)', color: '#7df0a8',
      border: '1px solid rgba(125,240,168,0.3)' };
  }
  function internalChipStyle() {
    return { fontSize: 10, fontFamily: 'JetBrains Mono, ui-monospace, monospace', padding: '2px 7px',
      borderRadius: 999, background: 'rgba(255,255,255,0.05)', color: 'rgba(255,255,255,0.55)',
      border: '1px solid rgba(255,255,255,0.1)' };
  }
  function chipLoadingStyle() { return { fontSize: 10, opacity: 0.4, fontFamily: 'JetBrains Mono, ui-monospace, monospace' }; }
  function chipNoneStyle() { return { fontSize: 10, opacity: 0.35, fontFamily: 'JetBrains Mono, ui-monospace, monospace' }; }

  // Helpers
  var useState = React.useState;
  function el() { return React.createElement.apply(React, arguments); }
  function apiBase() { return window.XLOOOP_API_BASE_URL || 'https://api.xlooop.com'; }
  async function authHeaders() {
    var clerk = window.XcpClerk;
    if (!clerk || !clerk.instance || !clerk.instance.session) throw new Error('not signed in');
    var token = await clerk.getToken({ template: 'xlooop-workers' });
    if (!token) throw new Error('no token');
    return { Authorization: 'Bearer ' + token, 'Content-Type': 'application/json' };
  }
  function backLink() {
    return el('a', { href: '?', style: { color: '#7df0a8', textDecoration: 'none', fontSize: 13 } }, '← back to fixture cockpit');
  }

  // Styles
  function shellStyle() {
    return {
      position: 'fixed', inset: 0,
      background: '#0a0a0e', color: '#f0f0f4',
      fontFamily: 'Geist, ui-sans-serif, system-ui, sans-serif',
      overflowY: 'auto', pointerEvents: 'auto',
      zIndex: 99989,
    };
  }
  function topBarStyle() {
    return {
      display: 'flex', alignItems: 'center', gap: 12,
      padding: '10px 16px',
      borderBottom: '1px solid rgba(255,255,255,0.08)',
      background: 'rgba(14,14,18,0.96)',
      position: 'sticky', top: 0, zIndex: 2,
    };
  }
  function bodyStyle() { return { maxWidth: 1180, margin: '0 auto', padding: '20px 16px 80px' }; }
  function emptyStyle() { return Object.assign({}, shellStyle(), { display: 'flex', alignItems: 'center', justifyContent: 'center' }); }
  function titleH1Style() { return { fontSize: 28, margin: '4px 0', fontWeight: 600, letterSpacing: '-0.01em' }; }
  function tilesGridStyle() {
    return { display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', gap: 14 };
  }
  function tileStyle() {
    return {
      background: 'rgba(18,18,22,0.95)',
      border: '1px solid rgba(255,255,255,0.08)',
      borderRadius: 10,
      padding: '14px 16px',
      cursor: 'pointer',
      transition: 'border-color 0.15s, transform 0.15s',
      display: 'flex', flexDirection: 'column',
      minHeight: 160,
    };
  }
  function tileTopStyle() { return { display: 'flex', alignItems: 'baseline', gap: 8, marginBottom: 6 }; }
  function tileNameStyle() { return { fontSize: 16, fontWeight: 600, flex: 1 }; }
  function tileDescStyle() { return { fontSize: 12, opacity: 0.55, marginBottom: 10, lineHeight: 1.45, flex: 1 }; }
  function tileMetaStyle() { return { display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 10 }; }
  function tileBottomStyle() { return { display: 'flex', alignItems: 'center', gap: 10, marginTop: 'auto', paddingTop: 8, borderTop: '1px solid rgba(255,255,255,0.05)' }; }
  function bindChip() { return { background: 'rgba(125,240,168,0.16)', color: '#7df0a8', padding: '2px 7px', borderRadius: 3, fontSize: 10, fontFamily: 'JetBrains Mono, ui-monospace, monospace' }; }
  function noBindChip() { return { background: 'rgba(180,180,180,0.10)', color: '#aaa', padding: '2px 7px', borderRadius: 3, fontSize: 10, fontFamily: 'JetBrains Mono, ui-monospace, monospace' }; }
  function metaPill(kind, label) {
    var bg = 'rgba(180,180,180,0.10)', fg = '#cdcdcd';
    if (kind === 'health') {
      if (label === 'on-track') { bg = 'rgba(125,240,168,0.13)'; fg = '#7df0a8'; }
      else if (label === 'watch') { bg = 'rgba(240,200,125,0.15)'; fg = '#f0c87d'; }
      else if (label === 'blocked' || label === 'risk') { bg = 'rgba(240,138,138,0.13)'; fg = '#f08a8a'; }
    }
    return { background: bg, color: fg, padding: '2px 8px', borderRadius: 3, fontSize: 10, fontFamily: 'JetBrains Mono, ui-monospace, monospace' };
  }
  function countStyle() { return { fontSize: 12, opacity: 0.75, fontFamily: 'JetBrains Mono, ui-monospace, monospace' }; }
  function openArrow() { return { marginLeft: 'auto', color: '#7df0a8', fontSize: 12 }; }
  function errStyle() { return { color: '#f08a8a', fontSize: 13, padding: '12px 14px', background: 'rgba(240,138,138,0.06)', border: '1px solid rgba(240,138,138,0.2)', borderRadius: 6 }; }
  function footerStyle() { return { marginTop: 24, padding: '14px 8px', opacity: 0.4, fontSize: 11, fontFamily: 'JetBrains Mono, ui-monospace, monospace', borderTop: '1px solid rgba(255,255,255,0.05)' }; }

  window.DbWorkspaceOverview = LiveDbWorkspaceView;
})();
