// ProjectScopeBindingPanel.jsx · R45.3 (2026-05-28)
//
// Closes the dead-end UX surface the operator hit pre-R45: project detail
// page said "0 live scoped rows · Connect a source binding" with NO UI
// to do so. This widget renders a portal on every project detail screen
// (URL contains ?screen=project&ws=...) and lets the operator configure
// the project's scope_binding declaratively:
//
//   - Combine mode (any/all)
//   - 1..N filter rows of type actor_in / source_tool_in / status_in / visibility_in
//   - Each filter takes a comma-separated value list (with * wildcards for actor)
//
// Backed by R45 API (already shipped):
//   GET    /api/v1/projects/:id           → load current binding
//   PATCH  /api/v1/projects/:id/scope     → save new binding (owner/operator only)
//   GET    /api/v1/projects/:id/events    → live count of matching events
//
// Auth: uses the same Clerk JWT that the cockpit already has loaded
// (window.XcpClerk.instance.session.getToken). No new credential flow.
//
// Mounting: self-mounts to <body> as a fixed-position rail when the URL
// includes `screen=project` AND a workspace id is resolvable. Hides itself
// on other screens. Survives client-side navigation by listening to
// popstate + a periodic URL re-check.
//
// Permission: edit controls render only if window.xcp.store.useSession()
// reports role ∈ {owner, operator}. Viewers see the binding read-only.

(function () {
  'use strict';

  if (typeof window === 'undefined') return;
  if (window.ProjectScopeBindingPanel) return; // idempotent

  var React = window.React;
  var ReactDOM = window.ReactDOM;
  if (!React || !ReactDOM) {
    // React not loaded yet · defer mounting to xcp:data-ready
    window.addEventListener('xcp:data-ready', mountWhenReady, { once: true });
    return;
  }
  mountWhenReady();

  function mountWhenReady() {
    if (window.__ProjectScopeBindingPanelMounted) return;
    window.__ProjectScopeBindingPanelMounted = true;

    // R45.3 · self-mount portal
    var container = document.createElement('div');
    container.id = 'xcp-scope-binding-portal';
    document.body.appendChild(container);

    var root = (window.ReactDOM && window.ReactDOM.createRoot)
      ? window.ReactDOM.createRoot(container)
      : null;
    if (!root) {
      // ReactDOM legacy render path
      try { window.ReactDOM.render(React.createElement(PortalGate), container); } catch (e) { void e; }
      return;
    }
    root.render(React.createElement(PortalGate));
  }

  // ──────────────────────────────────────────────────────────────────────
  // PortalGate · only renders the panel when URL says we're on project detail
  // ──────────────────────────────────────────────────────────────────────
  function PortalGate() {
    var state = React.useState(parseUrl());
    var urlState = state[0];
    var setUrlState = state[1];

    React.useEffect(function () {
      function onChange() { setUrlState(parseUrl()); }
      window.addEventListener('popstate', onChange);
      // Listen for in-app navigation events
      window.addEventListener('xcp:nav', onChange);
      // Defensive periodic re-check (SPA might mutate location without dispatching)
      var poll = setInterval(onChange, 1500);
      return function () {
        window.removeEventListener('popstate', onChange);
        window.removeEventListener('xcp:nav', onChange);
        clearInterval(poll);
      };
    }, []);

    if (urlState.screen !== 'project') return null;
    if (!urlState.workspaceId || !urlState.projectId) return null;

    return React.createElement(ScopeBindingPanel, {
      key: urlState.workspaceId + ':' + urlState.projectId,
      workspaceId: urlState.workspaceId,
      projectId: urlState.projectId,
    });
  }

  // Parse query params · normalize aliases (`ws` = `workspace`, etc.)
  function parseUrl() {
    var u;
    try { u = new URL(window.location.href); } catch (_) { return {}; }
    var p = u.searchParams;
    return {
      screen: p.get('screen'),
      // Workspace id resolution: explicit param > session.workspace_id
      workspaceId: p.get('ws') || p.get('workspace') || sessionWorkspaceId(),
      // Project id resolution: explicit param > breadcrumb-derived path
      projectId: p.get('proj') || p.get('project') || projectFromPath(u.pathname),
    };
  }
  function sessionWorkspaceId() {
    try {
      var s = window.xcp && window.xcp.store && window.xcp.store.useSession
        ? window.xcp.store.getState && window.xcp.store.getState().session
        : null;
      return s && s.workspace_id ? s.workspace_id : null;
    } catch (_) { return null; }
  }
  function projectFromPath(path) {
    // Best-effort: `/project/<id>` or `/p/<id>` pattern
    var m = /\/(?:project|p)\/([^/?#]+)/.exec(path || '');
    return m ? m[1] : null;
  }

  // ──────────────────────────────────────────────────────────────────────
  // ScopeBindingPanel · the actual UI surface
  // ──────────────────────────────────────────────────────────────────────
  function ScopeBindingPanel(props) {
    var workspaceId = props.workspaceId;
    var projectId = props.projectId;

    var loading = React.useState(true);
    var setLoading = loading[1];
    var loadError = React.useState(null);
    var setLoadError = loadError[1];
    var project = React.useState(null);
    var setProject = project[1];
    var matchCount = React.useState(null);
    var setMatchCount = matchCount[1];
    var editing = React.useState(false);
    var setEditing = editing[1];
    var draft = React.useState(null);
    var setDraft = draft[1];
    var saving = React.useState(false);
    var setSaving = saving[1];
    var saveError = React.useState(null);
    var setSaveError = saveError[1];
    var collapsed = React.useState(false);
    var setCollapsed = collapsed[1];

    var session = (window.xcp && window.xcp.store && window.xcp.store.useSession)
      ? window.xcp.store.useSession()
      : null;
    var role = session && session.role ? String(session.role).toLowerCase() : null;
    var canEdit = role === 'owner' || role === 'operator';

    React.useEffect(function () {
      var cancelled = false;
      setLoading(true);
      setLoadError(null);
      Promise.all([fetchProject(projectId), fetchMatchCount(projectId)])
        .then(function (results) {
          if (cancelled) return;
          setProject(results[0]);
          setMatchCount(results[1]);
          setLoading(false);
        })
        .catch(function (e) {
          if (cancelled) return;
          setLoadError(e && e.message ? e.message : String(e));
          setLoading(false);
        });
      return function () { cancelled = true; };
    }, [projectId]);

    function startEdit() {
      var current = project[0] && project[0].scope_binding;
      setDraft(current ? cloneBinding(current) : emptyBinding());
      setEditing(true);
      setSaveError(null);
    }
    function cancelEdit() {
      setEditing(false);
      setDraft(null);
      setSaveError(null);
    }
    function saveBinding(payload) {
      setSaving(true);
      setSaveError(null);
      patchProjectScope(projectId, payload)
        .then(function (updated) {
          setProject(updated);
          setEditing(false);
          setDraft(null);
          setSaving(false);
          // Refetch match count with new binding
          return fetchMatchCount(projectId);
        })
        .then(function (n) { setMatchCount(n); })
        .catch(function (e) {
          setSaving(false);
          setSaveError(e && e.message ? e.message : String(e));
        });
    }
    function clearBinding() {
      if (!window.confirm('Clear scope binding? Project will show only directly-linked events.')) return;
      saveBinding(null);
    }

    if (loading[0]) {
      return el('div', { style: panelStyle(collapsed[0]) },
        el('div', { style: headerStyle() },
          el('span', { style: titleStyle() }, 'Scope binding'),
          el('span', { style: { opacity: 0.5, fontSize: 11 } }, 'loading…'),
        ),
      );
    }
    if (loadError[0]) {
      return el('div', { style: panelStyle(collapsed[0]) },
        el('div', { style: headerStyle() },
          el('span', { style: titleStyle() }, 'Scope binding'),
          el('span', { style: { color: '#f08a8a', fontSize: 11 } }, loadError[0]),
        ),
      );
    }
    var p = project[0];
    if (!p) return null;
    var binding = p.scope_binding;
    var filterCount = binding && binding.filters ? binding.filters.length : 0;

    if (collapsed[0]) {
      return el('div', { style: collapsedStyle() },
        el('button', {
          type: 'button',
          onClick: function () { setCollapsed(false); },
          style: chipBtnStyle('#444'),
        }, 'Scope · ' + (filterCount > 0 ? (filterCount + ' filter' + (filterCount === 1 ? '' : 's')) : 'unconfigured')),
      );
    }

    return el('div', { style: panelStyle(false) },
      el('div', { style: headerStyle() },
        el('span', { style: titleStyle() }, 'Scope binding'),
        el('span', { style: subtitleStyle() },
          filterCount > 0
            ? (filterCount + ' filter' + (filterCount === 1 ? '' : 's') + ' · combine ' + binding.combine)
            : 'unconfigured · direct-link events only'),
        el('span', { style: { flex: 1 } }),
        matchCount[0] !== null
          ? el('span', { style: countStyle() }, matchCount[0] + ' matching event' + (matchCount[0] === 1 ? '' : 's'))
          : null,
        el('button', { type: 'button', onClick: function () { setCollapsed(true); }, style: chipBtnStyle('#333') }, '−'),
      ),

      // Body: either edit mode OR view mode
      editing[0]
        ? el(EditMode, {
            workspaceId: workspaceId, projectId: projectId,
            draft: draft[0], setDraft: setDraft,
            onSave: saveBinding, onCancel: cancelEdit,
            saving: saving[0], saveError: saveError[0],
          })
        : el(ViewMode, {
            binding: binding,
            canEdit: canEdit,
            onEdit: startEdit, onClear: clearBinding,
            projectName: p.name,
          }),
    );
  }

  // ──────────────────────────────────────────────────────────────────────
  // ViewMode
  // ──────────────────────────────────────────────────────────────────────
  function ViewMode(props) {
    var binding = props.binding;
    var filters = binding && binding.filters ? binding.filters : [];
    return el('div', { style: bodyStyle() },
      filters.length === 0
        ? el('div', { style: { opacity: 0.6, fontSize: 12, padding: '8px 0' } },
            'No filters configured. Add one to filter workspace events into ',
            el('b', null, props.projectName),
            '. Workspace events with matching actor / source_tool / status / visibility will appear in this project\'s view.')
        : el('div', null,
            filters.map(function (f, i) {
              return el('div', { key: i, style: filterRowReadStyle() },
                el('span', { style: { opacity: 0.5, marginRight: 8 } }, f.type + ':'),
                el('span', null, (f.values || []).join(', ')),
              );
            }),
          ),
      props.canEdit
        ? el('div', { style: { marginTop: 12, display: 'flex', gap: 8 } },
            el('button', { type: 'button', onClick: props.onEdit, style: btnStyle('primary') },
              filters.length === 0 ? 'Configure scope' : 'Edit'),
            filters.length > 0
              ? el('button', { type: 'button', onClick: props.onClear, style: btnStyle('danger') }, 'Clear')
              : null,
          )
        : el('div', { style: { marginTop: 8, fontSize: 11, opacity: 0.4 } },
            'Role ' + (props.role || 'viewer') + ' · contact an owner/operator to change the scope binding.'),
    );
  }

  // ──────────────────────────────────────────────────────────────────────
  // EditMode
  // ──────────────────────────────────────────────────────────────────────
  function EditMode(props) {
    var draft = props.draft;
    function update(patch) { props.setDraft(Object.assign({}, draft, patch)); }
    function updateFilter(i, patch) {
      var next = draft.filters.slice();
      next[i] = Object.assign({}, next[i], patch);
      update({ filters: next });
    }
    function removeFilter(i) {
      var next = draft.filters.slice();
      next.splice(i, 1);
      update({ filters: next });
    }
    function addFilter() {
      update({ filters: draft.filters.concat([{ type: 'actor_in', values: [] }]) });
    }
    function attemptSave() {
      var cleaned = sanitize(draft);
      var err = validate(cleaned);
      if (err) { props.setDraft(cleaned); window.alert(err); return; }
      props.onSave(cleaned);
    }

    return el('div', { style: bodyStyle() },
      el('div', { style: { display: 'flex', gap: 12, marginBottom: 10, alignItems: 'center' } },
        el('span', { style: labelStyle() }, 'Combine:'),
        el('label', { style: { display: 'inline-flex', alignItems: 'center', gap: 4 } },
          el('input', { type: 'radio', name: 'combine',
            checked: draft.combine === 'any',
            onChange: function () { update({ combine: 'any' }); },
          }),
          el('span', null, 'any match'),
        ),
        el('label', { style: { display: 'inline-flex', alignItems: 'center', gap: 4 } },
          el('input', { type: 'radio', name: 'combine',
            checked: draft.combine === 'all',
            onChange: function () { update({ combine: 'all' }); },
          }),
          el('span', null, 'all match'),
        ),
      ),
      draft.filters.map(function (f, i) {
        return el('div', { key: i, style: filterRowEditStyle() },
          el('select', {
            value: f.type,
            onChange: function (e) { updateFilter(i, { type: e.target.value }); },
            style: selectStyle(),
          },
            el('option', { value: 'actor_in' }, 'actor matches'),
            el('option', { value: 'source_tool_in' }, 'source_tool in'),
            el('option', { value: 'status_in' }, 'status in'),
            el('option', { value: 'visibility_in' }, 'visibility in'),
          ),
          el('input', {
            type: 'text',
            value: (f.values || []).join(', '),
            placeholder: f.type === 'actor_in' ? 'claude-session-*, operator' : 'comma-separated values',
            onChange: function (e) {
              updateFilter(i, { values: e.target.value.split(',').map(function (s) { return s.trim(); }).filter(Boolean) });
            },
            style: inputStyle(),
          }),
          el('button', { type: 'button', onClick: function () { removeFilter(i); }, style: btnStyle('icon') }, '×'),
        );
      }),
      el('div', { style: { marginTop: 6 } },
        el('button', { type: 'button', onClick: addFilter, style: btnStyle('secondary') }, '+ Add filter'),
      ),
      props.saveError
        ? el('div', { style: { color: '#f08a8a', fontSize: 11, marginTop: 8 } }, 'Save failed: ' + props.saveError)
        : null,
      el('div', { style: { marginTop: 12, display: 'flex', gap: 8 } },
        el('button', { type: 'button', onClick: attemptSave, disabled: props.saving, style: btnStyle('primary') },
          props.saving ? 'Saving…' : 'Save'),
        el('button', { type: 'button', onClick: props.onCancel, disabled: props.saving, style: btnStyle('secondary') }, 'Cancel'),
      ),
    );
  }

  // ──────────────────────────────────────────────────────────────────────
  // API + helpers
  // ──────────────────────────────────────────────────────────────────────
  function apiBase() { return window.XLOOOP_API_BASE_URL || 'https://api.xlooop.com'; }

  async function authHeaders() {
    try {
      var clerk = window.XcpClerk;
      if (!clerk || !clerk.instance || !clerk.instance.session) return null;
      var token = await clerk.getToken({ template: 'xlooop-workers' });
      if (!token) return null;
      return { Authorization: 'Bearer ' + token, 'Content-Type': 'application/json' };
    } catch (_) { return null; }
  }

  async function fetchProject(projectId) {
    var headers = await authHeaders();
    if (!headers) throw new Error('not signed in · cannot load project');
    var res = await fetch(apiBase() + '/api/v1/projects/' + encodeURIComponent(projectId), { headers: headers });
    if (!res.ok) {
      var body = await safeJson(res);
      throw new Error((body && body.error) || ('HTTP ' + res.status));
    }
    var body2 = await res.json();
    return body2.project;
  }

  async function fetchMatchCount(projectId) {
    var headers = await authHeaders();
    if (!headers) return null;
    var res = await fetch(apiBase() + '/api/v1/projects/' + encodeURIComponent(projectId) + '/events?limit=200', { headers: headers });
    if (!res.ok) return null;
    var body = await res.json();
    return body && body.events ? body.events.length : 0;
  }

  async function patchProjectScope(projectId, scopeBinding) {
    var headers = await authHeaders();
    if (!headers) throw new Error('not signed in · cannot save');
    var res = await fetch(apiBase() + '/api/v1/projects/' + encodeURIComponent(projectId) + '/scope', {
      method: 'PATCH',
      headers: headers,
      body: JSON.stringify({ scope_binding: scopeBinding }),
    });
    if (!res.ok) {
      var body = await safeJson(res);
      throw new Error((body && body.error) || ('HTTP ' + res.status));
    }
    var body2 = await res.json();
    return body2.project;
  }

  async function safeJson(res) { try { return await res.json(); } catch (_) { return null; } }

  function emptyBinding() { return { version: 1, combine: 'any', filters: [{ type: 'actor_in', values: [] }] }; }
  function cloneBinding(b) { return JSON.parse(JSON.stringify(b)); }
  function sanitize(draft) {
    return {
      version: 1,
      combine: draft.combine === 'all' ? 'all' : 'any',
      filters: (draft.filters || [])
        .filter(function (f) { return f && f.type && Array.isArray(f.values) && f.values.length > 0; })
        .map(function (f) { return { type: f.type, values: f.values.filter(Boolean).slice(0, 50) }; }),
    };
  }
  function validate(b) {
    if (!b.filters || b.filters.length === 0) return 'Add at least one filter, or click Cancel to keep the current binding.';
    if (b.filters.length > 20) return 'Max 20 filters per binding.';
    return null;
  }

  // ──────────────────────────────────────────────────────────────────────
  // styles
  // ──────────────────────────────────────────────────────────────────────
  function el() { return React.createElement.apply(React, arguments); }
  function panelStyle(collapsed) {
    return {
      position: 'fixed',
      bottom: collapsed ? 0 : 36, // sit above the R43.22 diag strip (which is z-index 99999)
      right: 16,
      width: 540,
      maxWidth: 'calc(100vw - 32px)',
      background: 'rgba(18, 18, 22, 0.97)',
      color: '#f0f0f4',
      border: '1px solid rgba(255,255,255,0.10)',
      borderRadius: 8,
      padding: '12px 14px',
      fontFamily: 'Geist, ui-sans-serif, system-ui, sans-serif',
      fontSize: 13,
      lineHeight: 1.4,
      zIndex: 99998,
      boxShadow: '0 8px 28px rgba(0,0,0,0.35)',
      backdropFilter: 'blur(8px)',
      WebkitBackdropFilter: 'blur(8px)',
    };
  }
  function collapsedStyle() {
    return { position: 'fixed', bottom: 36, right: 16, zIndex: 99998 };
  }
  function headerStyle() {
    return { display: 'flex', alignItems: 'center', gap: 10, marginBottom: 6 };
  }
  function titleStyle() { return { fontWeight: 600, fontSize: 13, letterSpacing: '0.01em' }; }
  function subtitleStyle() { return { opacity: 0.55, fontSize: 12, fontFamily: 'JetBrains Mono, ui-monospace, monospace' }; }
  function countStyle() {
    return { fontSize: 11, color: '#7df0a8', fontFamily: 'JetBrains Mono, ui-monospace, monospace', opacity: 0.85 };
  }
  function bodyStyle() { return { paddingTop: 4 }; }
  function labelStyle() { return { opacity: 0.65, fontSize: 12 }; }
  function filterRowReadStyle() {
    return {
      padding: '4px 0', fontSize: 12,
      fontFamily: 'JetBrains Mono, ui-monospace, monospace',
      borderBottom: '1px solid rgba(255,255,255,0.06)',
    };
  }
  function filterRowEditStyle() {
    return { display: 'flex', gap: 8, alignItems: 'center', marginBottom: 6 };
  }
  function selectStyle() {
    return {
      background: '#0d0d10', color: '#f0f0f4',
      border: '1px solid rgba(255,255,255,0.12)', borderRadius: 4,
      padding: '4px 6px', fontSize: 12, minWidth: 130,
      fontFamily: 'inherit',
    };
  }
  function inputStyle() {
    return {
      flex: 1, background: '#0d0d10', color: '#f0f0f4',
      border: '1px solid rgba(255,255,255,0.12)', borderRadius: 4,
      padding: '4px 8px', fontSize: 12,
      fontFamily: 'JetBrains Mono, ui-monospace, monospace',
    };
  }
  function btnStyle(variant) {
    var base = {
      border: 'none', borderRadius: 4, padding: '6px 12px',
      fontSize: 12, cursor: 'pointer', fontFamily: 'inherit',
      letterSpacing: '0.02em',
    };
    if (variant === 'primary') return Object.assign({}, base, { background: '#f0f0f4', color: '#0a0a0a', fontWeight: 500 });
    if (variant === 'danger') return Object.assign({}, base, { background: 'transparent', color: '#f08a8a', border: '1px solid rgba(240,138,138,0.4)' });
    if (variant === 'icon') return Object.assign({}, base, { background: 'transparent', color: '#999', padding: '2px 8px', fontSize: 16 });
    return Object.assign({}, base, { background: 'transparent', color: '#999', border: '1px solid rgba(255,255,255,0.15)' });
  }
  function chipBtnStyle(bg) {
    return {
      background: bg, color: '#f0f0f4',
      border: '1px solid rgba(255,255,255,0.12)',
      borderRadius: 4, padding: '4px 8px',
      fontSize: 11, cursor: 'pointer', fontFamily: 'JetBrains Mono, ui-monospace, monospace',
    };
  }

  // expose for tests / future consumers
  window.ProjectScopeBindingPanel = ScopeBindingPanel;
})();
