// Team Sync — SkillsPanel. Split out of team_sync.jsx; loads after team_sync_widgets.jsx.
const { useState: useSkS, useEffect: useSkE } = React;

function SkillsPanel({ orgId, personal, isPersonalWorkspace, onView, reloadSignal }) {
  const [devices, setDevices] = useSkS(null);
  const [selDevice, setSelDevice] = useSkS(null);
  const [agents, setAgents] = useSkS([]);
  const [inventory, setInventory] = useSkS([]);
  const [shared, setShared] = useSkS(null);
  const [mine, setMine] = useSkS(personal ? null : []);
  const [busy, setBusy] = useSkS(false);
  const [flash, showFlash] = useFlash();

  const selInv = useSel();      // local inventory items
  const selMine = useSel();     // my skill ids
  const selLib = useSel();      // library item_ids
  const confirm = useConfirm();
  const [installReq, setInstallReq] = useSkS(null);  // {versions, onDone} → opens the agent-picker modal

  // Team library is org-only — a personal workspace has none, so skip the
  // (would-be 403) call and render an empty team list.
  const loadShared = () => {
    if (isPersonalWorkspace) { setShared([]); return; }
    return tsListSharedSkills(orgId).then(setShared).catch(e => showFlash(e.message, 'error'));
  };
  const loadMine = () => { if (personal) tsListUserSkills(orgId).then(setMine).catch(e => showFlash(e.message, 'error')); };
  const loadDevices = () => tsListDevices(orgId).then(list => {
    // Hide decommissioned devices (daemon-uninstall marks them "removed").
    const visible = (list || []).filter(d => d.status !== 'removed');
    setDevices(visible);
    setSelDevice(prev => (visible.some(d => d.id === prev) ? prev : (visible[0] ? visible[0].id : null)));
  }).catch(e => showFlash(e.message, 'error'));
  const loadInv = () => {
    if (!selDevice) { setAgents([]); setInventory([]); return; }
    Promise.all([tsListAgents(orgId, selDevice), tsListDeviceInventory(orgId, selDevice)])
      .then(([ag, inv]) => { setAgents(ag); setInventory(inv); })
      .catch(e => showFlash(e.message, 'error'));
  };

  useSkE(() => {
    setDevices(null); setShared(null); setSelDevice(null);
    if (personal) setMine(null);
    selInv.clear(); selMine.clear(); selLib.clear();
    if (personal) loadDevices();
    loadShared(); loadMine();
  }, [orgId]);
  useSkE(() => { selInv.clear(); loadInv(); }, [selDevice, orgId]);

  // Devices/agents change out-of-band — a device goes offline, or gets
  // uninstalled (the daemon marks it removed) and an agent's skills change.
  // There's no live channel for team-sync, so poll to reflect status without a
  // manual refresh. (Auto-sync itself is reconciled entirely by the backend.)
  useSkE(() => {
    if (!personal) return undefined;
    const t = setInterval(() => { loadDevices(); loadInv(); }, 15000);
    return () => clearInterval(t);
  }, [orgId, selDevice]);

  // External refresh (e.g. a note saved in the detail rail moved a linked
  // copy): re-pull the skill lists so cross-resolved notes stay consistent.
  useSkE(() => { if (reloadSignal) { loadShared(); loadMine(); loadInv(); } }, [reloadSignal]);

  // ── batch actions ──
  // Install is a two-step flow: pick skills, then pick target agents in a modal
  // (defaults to ALL agents). So you never scroll back to a top agent selector
  // while working a long skill list. Deployments are keyed by cloud_skill_id —
  // the user's My Cloud copy of a skill — so `cloudIds` below are My Cloud ids.
  const doInstallToTargets = async (cloudIds, targets) => {
    if (!targets.length) { showFlash('Select one or more agents', 'error'); return false; }
    if (!cloudIds.length) { showFlash('Select one or more skills', 'error'); return false; }
    setBusy(true);
    try {
      let n = 0;
      for (const cid of cloudIds) { const ops = await tsInstall(orgId, cid, targets); n += ops ? ops.length : 0; }
      showFlash(`Requested ${n} install operation(s)`);
      setTimeout(loadInv, 1500);
      return true;
    } catch (e) { showFlash(e.message, 'error'); return false; } finally { setBusy(false); }
  };
  const installMine = () => {
    const cloudIds = (mine || []).filter(s => selMine.ids.has(s.id)).map(s => s.id);
    if (!cloudIds.length) { showFlash('Select one or more skills first', 'error'); return; }
    setInstallReq({ versions: cloudIds, onDone: () => selMine.clear() });
  };
  // A team skill must be imported into My Cloud before it can be deployed, so we
  // resolve each selected team skill to the user's My Cloud copy (by bundle).
  const installLib = () => {
    const selected = (shared || []).filter(s => selLib.ids.has(s.id));
    const cloudIds = selected.map(s => (mineForBundle(s.bundle_id) || {}).id).filter(Boolean);
    if (!cloudIds.length) { showFlash('Add the selected skill(s) to My Cloud Skills first, then install.', 'error'); return; }
    const skipped = selected.length - cloudIds.length;
    if (skipped) showFlash(`${skipped} skill(s) not in My Cloud Skills were skipped — add them first.`, 'error');
    setInstallReq({ versions: cloudIds, onDone: () => selLib.clear() });
  };
  const shareMine = async () => {
    const ids = Array.from(selMine.ids);
    if (!ids.length) { showFlash('Select one or more skills first', 'error'); return; }
    setBusy(true);
    try { await tsShareSkills(orgId, ids, '1.0.0'); showFlash(`Shared ${ids.length} skill(s)`); selMine.clear(); loadShared(); }
    catch (e) { showFlash(e.message, 'error'); } finally { setBusy(false); }
  };
  const doDeleteMine = async () => {
    const ids = Array.from(selMine.ids);
    if (!ids.length) return;
    setBusy(true);
    try { for (const id of ids) await tsDeleteUserSkill(orgId, id); showFlash(`Deleted ${ids.length} skill(s)`); selMine.clear(); loadMine(); }
    catch (e) { showFlash(e.message, 'error'); } finally { setBusy(false); }
  };
  const deleteMine = () => {
    const n = selMine.ids.size;
    if (!n) { showFlash('Select one or more skills first', 'error'); return; }
    confirm.ask(`Delete ${n} skill(s) from My Cloud Skills?`, 'Delete', doDeleteMine);
  };
  const importToMine = async () => {
    const items = (shared || []).filter(s => selLib.ids.has(s.id));
    if (!items.length) { showFlash('Select one or more skills first', 'error'); return; }
    setBusy(true);
    try { for (const s of items) await tsImportTeamSkill(orgId, s.id); showFlash(`Imported ${items.length} to My Cloud Skills`); selLib.clear(); loadMine(); }
    catch (e) { showFlash(e.message, 'error'); } finally { setBusy(false); }
  };
  const doDeleteLib = async () => {
    const ids = Array.from(selLib.ids);
    if (!ids.length) return;
    setBusy(true);
    try { for (const id of ids) await tsDeleteLibrarySkill(orgId, id); showFlash(`Deleted ${ids.length} from library`); selLib.clear(); loadShared(); }
    catch (e) { showFlash(e.message, 'error'); } finally { setBusy(false); }
  };
  const deleteLib = () => {
    const n = selLib.ids.size;
    if (!n) { showFlash('Select one or more skills first', 'error'); return; }
    confirm.ask(`Delete ${n} skill(s) from the team library? This removes it for everyone in the org.`, 'Delete', doDeleteLib);
  };
  const importInv = async () => {
    const ids = Array.from(selInv.ids);
    if (!ids.length) { showFlash('Select one or more skills first', 'error'); return; }
    setBusy(true);
    try {
      const res = await tsImportLocalSkills(orgId, ids);
      // imported[] = landed in My Cloud immediately (bundle already stored, no
      // daemon round-trip). devices[] = still need the daemon to upload, so only
      // those produce later import_result device events.
      const importedNow = ((res && res.imported) || []).length;
      const pending = ((res && res.devices) || []).some(d => d.sent && d.item_count > 0);
      const parts = [];
      if (importedNow) parts.push(`${importedNow} added to My Cloud Skills`);
      if (pending) parts.push('the rest will upload from your device shortly');
      showFlash(parts.length
        ? `Import done — ${parts.join('; ')}.`
        : 'Selected skill(s) are already in My Cloud Skills — nothing to import.');
      selInv.clear();
      loadInv(); loadMine();
      // Only the daemon-upload path is asynchronous, so re-poll My Cloud Skills
      // until it lands; imported[] is already reflected by loadMine() above.
      if (pending) {
        setTimeout(loadMine, 2000);
        setTimeout(loadMine, 5000);
      }
    } catch (e) { showFlash(e.message, 'error'); } finally { setBusy(false); }
  };
  // Add skills from a folder via the LOCAL daemon: it can read hidden folders
  // (.claude) and pop a native OS folder picker — neither of which the browser
  // file picker can do. No daemon on this machine → guide to install one.
  const reportUpload = (r) => {
    if (r && r.ok) {
      showFlash(`Uploaded ${r.uploaded || 0} skill(s) to My Cloud Skills${r.failed ? `, ${r.failed} failed` : ''}.`,
        r.failed ? 'error' : undefined);
      loadMine();
    } else if (r && r.reason === 'cancelled') {
      showFlash('Folder selection cancelled.');
    } else {
      showFlash((r && r.error) || 'Upload failed.', 'error');
    }
  };
  const addFromFolder = async () => {
    setBusy(true);
    try {
      const who = await tsLocalDaemon();
      if (!who) {
        showFlash('No daemon detected on this machine. Install the daemon (Devices section) to add skills from a folder.', 'error');
        return;
      }
      if (who.gui_available) {
        showFlash('Opening a folder picker on your machine…');
        reportUpload(await tsLocalPickAndUpload());
      } else {
        // Rare for the machine running the browser; offer manual path entry.
        const path = window.prompt('This machine has no graphical session. Enter the absolute path of the skill folder to upload:');
        if (path && path.trim()) reportUpload(await tsLocalScanPath(path.trim()));
      }
    } catch (e) { showFlash(e.message, 'error'); }
    finally { setBusy(false); }
  };
  // Import skills from a path on a SPECIFIC (possibly remote / headless) device:
  // the backend relays a scan command to that device's daemon over the websocket.
  const importFromPath = async () => {
    if (!selDevice) { showFlash('Select a device first', 'error'); return; }
    const path = window.prompt('Enter the absolute folder path on this device to import skills from (e.g. /home/user/.claude/skills):');
    if (!path || !path.trim()) return;
    setBusy(true);
    try {
      const r = await tsImportFromPath(orgId, selDevice, path.trim());
      if (r && r.sent) {
        showFlash('Requested — that device will scan the folder and upload. My Cloud Skills will refresh shortly.');
        setTimeout(loadMine, 3000); setTimeout(loadMine, 7000);
      } else {
        showFlash('Could not reach that device — is its daemon online?', 'error');
      }
    } catch (e) { showFlash(e.message, 'error'); }
    finally { setBusy(false); }
  };
  // Remove a skill from its agent — same gesture for both kinds. Platform-managed
  // (deployed) skills uninstall via deployments/remove, keyed by the user's My
  // Cloud copy (cloud_skill_id, resolved from the item's bundle); everything else
  // is a local folder we ask the daemon to delete.
  const doRemoveInv = async () => {
    const items = inventory.filter(i => selInv.ids.has(i.id));
    if (!items.length) return;
    const managed = [];   // { it, cloudId }
    const local = [];     // inventory items deleted as local folders
    items.forEach(it => {
      const cloudId = it.managed && it.bundle_id ? (mineForBundle(it.bundle_id) || {}).id : null;
      if (cloudId) managed.push({ it, cloudId });
      else local.push(it);
    });
    setBusy(true);
    try {
      for (const m of managed) await tsRemove(orgId, m.cloudId, [{ device_id: selDevice, agent_id: m.it.device_agent_id }]);
      if (local.length) await tsDeleteLocalSkills(orgId, local.map(i => i.id));
      showFlash(`Remove requested for ${items.length} skill(s)`);
      selInv.clear();
      setTimeout(loadInv, 1500);
    } catch (e) { showFlash(e.message, 'error'); } finally { setBusy(false); }
  };
  const removeInv = () => {
    const n = selInv.ids.size;
    if (!n) { showFlash('Select one or more skills first', 'error'); return; }
    confirm.ask(`Remove ${n} skill(s) from their agent(s)? This uninstalls them from the device.`, 'Remove', doRemoveInv);
  };

  const doDeleteDevice = async () => {
    if (!selDevice) return;
    setBusy(true);
    try { await tsDeleteDevice(orgId, selDevice); showFlash('Device removed'); setSelDevice(null); loadDevices(); }
    catch (e) { showFlash(e.message, 'error'); } finally { setBusy(false); }
  };
  const deleteDevice = () => {
    if (!selDevice) return;
    const d = (devices || []).find(x => x.id === selDevice);
    confirm.ask(`Remove device "${d ? d.display_name : selDevice}"? This deletes the device and its agent / inventory records.`, 'Remove', doDeleteDevice);
  };

  const invCount = (aid) => inventory.filter(i => i.device_agent_id === aid).length;
  // Headless online device → no native picker possible there, so offer manual
  // path entry. GUI devices use "Add skills from folder" instead.
  const selDeviceObj = (devices || []).find(d => d.id === selDevice);
  const selDeviceHeadless = !!selDeviceObj && selDeviceObj.gui_available === false;
  // Normalize a clicked skill (My Cloud / Team library / inventory) into the
  // shape SkillDetail consumes. Skills are content-addressed by bundle_id, so we
  // cross-resolve the OTHER layer by bundle: a My Cloud skill finds its team
  // counterpart (for the team note) and vice-versa, and inventory finds both.
  const teamForBundle = (bid) => bid ? (shared || []).find(s => s.bundle_id === bid) : null;
  const mineForBundle = (bid) => bid ? (mine || []).find(s => s.bundle_id === bid) : null;
  const detailFromMine = (s) => {
    const t = teamForBundle(s.bundle_id);
    return {
      kind: 'mine', name: (s.bundle && s.bundle.skill_name) || s.name, description: s.bundle ? s.bundle.description : null,
      bundle_id: s.bundle_id, cloud_skill_id: s.id, team_skill_id: t ? t.id : null,
      user_note: s.user_note != null ? s.user_note : '',
      // MyCloudSkillRead folds the same bundle's team note + attribution.
      team_note: s.team_note != null ? s.team_note : (t ? (t.team_note || '') : ''),
      team_note_updated_by: s.team_note_updated_by || (t && t.team_note_updated_by) || null,
      team_note_updated_at: s.team_note_updated_at || (t && t.team_note_updated_at) || null,
      instances: s.instances || [],
      metadata: s.metadata || {},
    };
  };
  const detailFromTeam = (s) => {
    const m = mineForBundle(s.bundle_id);
    return {
      kind: 'team', name: (s.bundle && s.bundle.skill_name) || s.name, description: s.bundle ? s.bundle.description : null,
      bundle_id: s.bundle_id, cloud_skill_id: m ? m.id : null, team_skill_id: s.id,
      user_note: m ? (m.user_note || '') : '',
      team_note: s.team_note || '',
      team_note_updated_by: s.team_note_updated_by || null,
      team_note_updated_at: s.team_note_updated_at || null,
      metadata: s.metadata || {},
    };
  };
  // A local skill instance carries no notes of its own; we cross-resolve its
  // bundle to the user's My Cloud copy (personal note) and the team library copy
  // (team note). A daemon-folded description may live in metadata.
  const detailFromInv = (it) => {
    const m = mineForBundle(it.bundle_id);
    const t = teamForBundle(it.bundle_id);
    return {
      kind: 'inventory',
      name: it.name,
      description: (it.metadata && (it.metadata.description || it.metadata.summary)) || (m && m.bundle && m.bundle.description) || (t && t.bundle && t.bundle.description) || null,
      bundle_id: it.bundle_id, cloud_skill_id: m ? m.id : null, team_skill_id: t ? t.id : null,
      user_note: m ? (m.user_note || '') : '',
      team_note: m && m.team_note != null ? m.team_note : (t ? (t.team_note || '') : ''),
      team_note_updated_by: (m && m.team_note_updated_by) || (t && t.team_note_updated_by) || null,
      team_note_updated_at: (m && m.team_note_updated_at) || (t && t.team_note_updated_at) || null,
      source_key: it.source_key, path: it.absolute_path, sha256: it.sha256, managed: it.managed, metadata: it.metadata || {},
    };
  };

  // Toggle a skill's checkbox. Whenever a checkbox is *checked* (the skill is
  // newly added to the selection), open that skill's detail — every time, even
  // while building a multi-selection. Unchecking leaves the detail panel as-is.
  const toggleAndView = (sel, id, items, getId, toView) => {
    const wasSelected = sel.ids.has(id);
    sel.toggle(id);
    if (!wasSelected) {
      const item = (items || []).find(x => getId(x) === id);
      if (item) onView(toView ? toView(item) : item);
    }
  };

  const invActions = (
    <>
      {personal && <button className="btn sm" disabled={busy} onClick={importInv}>⇧ Import to My Cloud Skills</button>}
      <button className="btn sm danger" disabled={busy} onClick={removeInv}>⌫ Remove</button>
      <button className="btn sm ghost" onClick={loadInv}>↻</button>
    </>
  );
  const mineActions = (
    <>
      <button className="btn sm" disabled={busy} title="Open a native folder picker on this machine (via the daemon) to add skills — hidden folders like .claude work" onClick={addFromFolder}>⬆ Add skills from folder</button>
      <button className="btn sm accent" disabled={busy} onClick={installMine}>⇩ Install to agents</button>
      {!isPersonalWorkspace && <button className="btn sm" disabled={busy} onClick={shareMine}>⇧ Share</button>}
      <button className="btn sm danger" disabled={busy} onClick={deleteMine}>⌫ Delete</button>
      <button className="btn sm ghost" onClick={loadMine}>↻</button>
    </>
  );
  const libActions = (
    <>
      {personal && <button className="btn sm accent" disabled={busy} onClick={installLib}>⇩ Install to agents</button>}
      {personal && <button className="btn sm" disabled={busy} onClick={importToMine}>⇩ Add to My Cloud Skills</button>}
      <button className="btn sm danger" disabled={busy} onClick={deleteLib}>⌫ Delete</button>
      <button className="btn sm ghost" onClick={loadShared}>↻</button>
    </>
  );

  return (
    <div>
      {/* All scrolling content shares one isolated stacking context so the
          sticky device panel's z-index (below) can outrank the skill list's own
          stacking contexts (pulsing StatusDots, dimmed counts) without also
          outranking the page-level / right-rail modals — those live outside
          this wrapper and so still paint over the panel. */}
      <div style={{ isolation: 'isolate' }}>
      {/* Sticky device panel: device picker + its agent chips together, pinned at
          the top of the viewport so switching devices (and seeing that device's
          agents) never means scrolling back up. At panel top so its sticky range
          spans every section below. */}
      {personal && devices && devices.length > 0 && (
        // zIndex:1 lifts this sticky bar above the scrolling skill list's own
        // stacking contexts — the pulsing StatusDots (animated transform) and
        // the dimmed (opacity) skill counts would otherwise paint *through* it.
        // It stays local to the isolated wrapper above, so right-rail and
        // in-panel modal overlays still cover it.
        <div style={{
          position: 'sticky', top: 0, zIndex: 1, marginBottom: 12,
          padding: '10px 12px', borderRadius: 8,
          background: 'var(--surface-raised, var(--surface))', border: '1px solid var(--border-subtle)',
          boxShadow: 'var(--shadow-1, 0 2px 8px rgba(0,0,0,.06))',
        }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
            <span className="t-micro text-tertiary">Device</span>
            <select style={{ ...fieldStyle, width: 'auto', minWidth: 200, height: 32 }} value={selDevice || ''} onChange={e => setSelDevice(e.target.value)}>
              {devices.map(d => <option key={d.id} value={d.id}>{d.display_name} · {d.os || '?'} · {d.status}</option>)}
            </select>
            {selDeviceHeadless && <button className="btn sm" disabled={busy} title="This device has no graphical session — enter a folder path to import skills from it" onClick={importFromPath}>📁 Import from path</button>}
            <button className="btn sm danger" disabled={busy || !selDevice} onClick={deleteDevice}>⌫ Remove device</button>
            <div style={{ flex: 1 }} />
            <button className="btn sm ghost" onClick={loadDevices} title="Refresh devices">↻</button>
          </div>
          {agents.length > 0 ? (
            <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginTop: 8 }}>
              {agents.map(a => <AgentChip key={a.id} agent={a} count={invCount(a.id)} />)}
            </div>
          ) : (
            <div className="text-tertiary t-small" style={{ marginTop: 8 }}>No agents reported for this device yet.</div>
          )}
        </div>
      )}
      {personal && (<>
      {devices === null && (
        <TsSection title="Devices"><div className="text-secondary t-small">Loading devices…</div></TsSection>
      )}
      {devices && devices.length === 0 && (
        <TsSection title="Devices"><div className="text-secondary t-small">No devices yet — install the daemon on a device using the panel on the right.</div></TsSection>
      )}

      {selDevice && agents.length > 0 && (
        <TsSection title="Installed skills" actions={invActions}>
          {agents.map(a => (
            <AgentSkillGroup
              key={a.id}
              agent={a}
              items={inventory.filter(i => i.device_agent_id === a.id)}
              selInv={selInv}
              inventory={inventory}
              onView={onView}
              toggleAndView={toggleAndView}
              detailFromInv={detailFromInv}
            />
          ))}
        </TsSection>
      )}
      </>)}

      {personal && (
        <TsSection title="My Cloud Skills" collapsible count={mine ? mine.length : undefined} lead={<SelectAllChk ids={(mine || []).map(s => s.id)} sel={selMine} />} actions={mineActions}>
          {mine === null && <div className="text-secondary t-small">Loading…</div>}
          {mine && mine.length === 0 && <div className="text-secondary t-small">{isPersonalWorkspace ? 'No skills yet — add one from a folder, or push one from a device.' : 'No skills yet — add one from the team library below, or push one from a device.'}</div>}
          {mine && mine.length > 0 && (
            <div style={skillGridStyle}>
              {mine.map(s => (
                <SkillRow key={s.id} checked={selMine.ids.has(s.id)} onToggle={() => toggleAndView(selMine, s.id, mine, x => x.id, detailFromMine)} onView={() => onView(detailFromMine(s))}>
                  <span style={skillNameStyle}>{(s.bundle && s.bundle.skill_name) || s.name}</span>
                </SkillRow>
              ))}
            </div>
          )}
        </TsSection>
      )}

      {!isPersonalWorkspace && (
        <TsSection title="Team skills library" collapsible count={shared ? shared.length : undefined} lead={<SelectAllChk ids={(shared || []).map(s => s.id)} sel={selLib} />} actions={libActions}>
          {shared === null && <div className="text-secondary t-small">Loading…</div>}
          {shared && shared.length === 0 && <div className="text-secondary t-small">No shared skills yet.</div>}
          {shared && shared.length > 0 && (
            <div style={skillGridStyle}>
              {shared.map(s => (
                <SkillRow key={s.id} checked={selLib.ids.has(s.id)} onToggle={() => toggleAndView(selLib, s.id, shared, x => x.id, detailFromTeam)} onView={() => onView(detailFromTeam(s))}>
                  <span style={skillNameStyle}>{(s.bundle && s.bundle.skill_name) || s.name}</span>
                </SkillRow>
              ))}
            </div>
          )}
        </TsSection>
      )}

      </div>

      <TsFlash flash={flash} />
      <ConfirmDialog req={confirm.req} onClose={confirm.close} />
      {installReq && (
        <InstallAgentsDialog
          orgId={orgId}
          devices={devices || []}
          count={installReq.versions.length}
          onCancel={() => setInstallReq(null)}
          onConfirm={async (targets) => {
            const ok = await doInstallToTargets(installReq.versions, targets);
            if (ok) { if (installReq.onDone) installReq.onDone(); setInstallReq(null); }
          }}
        />
      )}
    </div>
  );
}

