// Team Sync API — client for org-scoped skill/prompt sharing, devices,
// deployments, and org membership.
//
// All knowledge of paths lives here. Every org-scoped call goes to
//   {base}/orgs/{orgId}/team-sync/*
// membership goes to
//   {base}/orgs/{orgId}/members*
// and the org picker uses
//   {base}/me/orgs
//
// Skill storage model (content-addressed):
//   - SkillBundle      — global, content-addressed skill content (bundle_id).
//   - MyCloudSkill     — a user's personal copy of a bundle (user-scoped); the
//                        personal note lives here (user_note).
//   - TeamSkill        — an org's shared copy of a bundle (org-scoped); the
//                        team note lives here (team_note).
//   - inventory item   — a skill physically present on a device agent; carries
//                        bundle_id once content is known, plus folded notes.
//
// Auth: every request carries parcleAuthHeaders(); a 401 clears the session
// via parcleNotifyUnauthorized(). Mock mode (no PARCLE_API_BASE) is fully
// self-contained so the team-sync UI is demoable offline.

// ──────────────────────────────────────────────────────────────────────────
// Selected-org workspace context (user sessions pick which org to operate in).
// ──────────────────────────────────────────────────────────────────────────
const PARCLE_TS_ORG_KEY = 'parcle.tsOrg';
function tsSelectedOrg() { try { return localStorage.getItem(PARCLE_TS_ORG_KEY) || null; } catch (e) { return null; } }
function tsSetSelectedOrg(orgId) {
  try {
    if (orgId) localStorage.setItem(PARCLE_TS_ORG_KEY, orgId);
    else localStorage.removeItem(PARCLE_TS_ORG_KEY);
  } catch (e) { /* storage disabled */ }
}

function tsMockEnabled() { return !window.PARCLE_API_BASE; }
function tsBase() { return window.PARCLE_API_BASE || '/api'; }

async function _tsError(res) {
  if (res.status === 401 && typeof parcleNotifyUnauthorized === 'function') parcleNotifyUnauthorized();
  let msg = `HTTP ${res.status}`;
  try {
    const j = await res.json();
    if (j && j.detail) msg = typeof j.detail === 'string' ? j.detail : JSON.stringify(j.detail);
  } catch (e) { /* non-JSON body */ }
  const err = new Error(msg);
  err.status = res.status;
  return err;
}

// Core request helper. `path` is appended to the base; callers pass the full
// org-scoped path so this stays endpoint-agnostic.
async function _tsRequest(path, { method = 'GET', body = undefined } = {}) {
  const headers = { ...parcleAuthHeaders() };
  if (body !== undefined) headers['Content-Type'] = 'application/json';
  const res = await fetch(tsBase() + path, {
    method,
    headers,
    body: body !== undefined ? JSON.stringify(body) : undefined,
  });
  if (!res.ok) throw await _tsError(res);
  if (res.status === 204) return null;
  return res.json();
}

const _tsOrg = (orgId) => `/orgs/${encodeURIComponent(orgId)}`;
// Org-scoped team library root — only team skills/prompts and team imports live
// here. User, device, deployment, and personal-prompt resources are NOT org
// scoped: they belong to the authenticated user and hang off `/team-sync/*`.
const _tsTeam = (orgId) => `${_tsOrg(orgId)}/team-sync`;
const _tsUser = () => `/team-sync`;

// ──────────────────────────────────────────────────────────────────────────
// MOCK STORE — in-memory; demoable without a backend. Shapes mirror the
// content-addressed backend (bundles + my_cloud_skills + team_skills).
// ──────────────────────────────────────────────────────────────────────────
const _tsNow = () => new Date().toISOString();
let _tsMock = null;
function _tsBundle(id, name, desc, hash) {
  return {
    id, bundle_hash: hash, size_bytes: 1024, file_count: 1,
    skill_name: name, description: desc, skill_md_hash: hash,
    manifest: [{ path: 'SKILL.md' }], storage_key: `mock/${id}`, created_at: _tsNow(),
  };
}
function _tsMockStore() {
  if (_tsMock) return _tsMock;
  const at = _tsNow();
  const bundles = {
    bdl_1: _tsBundle('bdl_1', 'Data Helper', 'Shared data workflows', 'a1b2c3'),
    bdl_2: _tsBundle('bdl_2', 'Release Notes Writer', 'Drafts release notes from a diff', 'b2c3d4'),
    bdl_3: _tsBundle('bdl_3', 'Local Notes', 'Local scratch-notes helper (from SKILL.md).', 'zz9911'),
  };
  _tsMock = {
    bundles,
    orgs: [{ id: 'org_mock_1', kind: 'org', email: 'acme@parcle.dev', created_at: at }],
    members: [
      { id: 'usr_mock_1', kind: 'user', email: 'maya@acme.dev', created_at: at },
      { id: 'usr_mock_2', kind: 'user', email: 'devin@acme.dev', created_at: at },
    ],
    directory: [
      { id: 'usr_mock_1', kind: 'user', email: 'maya@acme.dev', created_at: at },
      { id: 'usr_mock_2', kind: 'user', email: 'devin@acme.dev', created_at: at },
      { id: 'usr_mock_3', kind: 'user', email: 'priya@acme.dev', created_at: at },
    ],
    // MyCloudSkillRead[]
    myCloudSkills: [
      {
        id: 'mcs_1', user_account_id: 'usr_mock_1', bundle_id: 'bdl_1',
        source: 'local_skill_upload', enabled: true, pinned: false,
        user_note: null, user_tags: [], metadata: { source_path: '~/.codex/skills/data-helper' },
        instances: [
          { bundle_id: 'bdl_1', machine_name: 'maya-desktop', absolute_path: '~/.codex/skills/data-helper', updated_at: at },
        ],
        created_at: at, updated_at: at, bundle: bundles.bdl_1,
      },
    ],
    // SkillRead[] (team library)
    teamSkills: [
      {
        id: 'tsk_1', org_account_id: 'org_mock_1', bundle_id: 'bdl_2',
        shared_by_user_account_id: 'usr_mock_2', team_note: null, team_tags: [], policy: {},
        metadata: {}, created_at: at, updated_at: at, bundle: bundles.bdl_2,
      },
    ],
    userPrompts: [
      {
        id: 'prp_mock_1', owner_user_account_id: 'usr_mock_1',
        name: 'Bug triage', description: 'Quickly assess bug priority',
        content: 'Assess this bug report and assign a priority (P0–P3) with one-line justification.',
        created_at: at, updated_at: at,
      },
    ],
    sharedPrompts: [
      {
        prompt_id: 'prp_shared_1', org_account_id: 'org_mock_1',
        name: 'Code review', description: 'Review a change for regressions',
        uploader_user_account_id: 'usr_mock_2',
        content: 'Review this change for regressions, edge cases, and missing tests.', created_at: at,
      },
    ],
    devices: [
      {
        id: 'dev_mock_1', display_name: 'Maya laptop', hostname: 'maya-desktop', os: 'windows',
        machine_fingerprint: 'fp_mock', status: 'online', last_seen_at: at, created_at: at, gui_available: true,
      },
    ],
    agents: {
      dev_mock_1: [
        { id: 'agt_mock_1', org_account_id: 'org_mock_1', device_id: 'dev_mock_1', agent_type: 'codex', label: 'default', capabilities: { skills: true }, status: 'online', last_seen_at: at, created_at: at },
        { id: 'agt_mock_2', org_account_id: 'org_mock_1', device_id: 'dev_mock_1', agent_type: 'claudecode', label: 'default', capabilities: { skills: true }, status: 'online', last_seen_at: at, created_at: at },
      ],
    },
    // LocalSkillInstanceRead[] keyed by device id. bundle_id present = content
    // known (managed/uploaded); null = purely local, not yet uploaded.
    inventory: {
      dev_mock_1: [
        { id: 'inv_m1', device_id: 'dev_mock_1', device_agent_id: 'agt_mock_1', source_key: 'data-helper', name: 'Data Helper', absolute_path: '~/.codex/skills/data-helper', root_path: '~/.codex/skills', relative_path: 'data-helper', sha256: 'a1b2c3', managed: true, status: 'present', drifted: false, metadata: {}, discovered_at: at, last_seen_at: at, updated_at: at, bundle_id: 'bdl_1' },
        { id: 'inv_m2', device_id: 'dev_mock_1', device_agent_id: 'agt_mock_1', source_key: 'local-notes', name: 'Local Notes', absolute_path: '~/.codex/skills/local-notes', root_path: '~/.codex/skills', relative_path: 'local-notes', sha256: 'zz9911', managed: false, status: 'present', drifted: false, metadata: { description: 'Local scratch-notes helper (from SKILL.md).' }, discovered_at: at, last_seen_at: at, updated_at: at, bundle_id: 'bdl_3' },
        { id: 'inv_m3', device_id: 'dev_mock_1', device_agent_id: 'agt_mock_2', source_key: 'release-notes-writer', name: 'Release Notes Writer', absolute_path: '~/.claude/skills/release-notes-writer', root_path: '~/.claude/skills', relative_path: 'release-notes-writer', sha256: 'b2c3d4', managed: true, status: 'present', drifted: false, metadata: {}, discovered_at: at, last_seen_at: at, updated_at: at, bundle_id: 'bdl_2' },
      ],
    },
    operations: {},
    seq: 1,
  };
  return _tsMock;
}
const _clone = (x) => JSON.parse(JSON.stringify(x));
const _delay = (v, ms = 220) => new Promise(r => setTimeout(() => r(v), ms));

// ──────────────────────────────────────────────────────────────────────────
// Orgs / membership
// ──────────────────────────────────────────────────────────────────────────
async function tsListMyOrgs() {
  if (tsMockEnabled()) return _delay(_clone(_tsMockStore().orgs));
  const data = await _tsRequest('/me/orgs');
  return Array.isArray(data && data.orgs) ? data.orgs : [];
}

async function tsListMembers(orgId) {
  if (tsMockEnabled()) return _delay(_clone(_tsMockStore().members));
  const data = await _tsRequest(`${_tsOrg(orgId)}/members`);
  return Array.isArray(data && data.members) ? data.members : [];
}

async function tsSearchUserByEmail(orgId, email) {
  if (tsMockEnabled()) {
    await _delay(null, 200);
    const hit = _tsMockStore().directory.find(u => u.email.toLowerCase() === String(email).trim().toLowerCase());
    if (!hit) { const e = new Error('No user account with that email'); e.status = 404; throw e; }
    return _clone(hit);
  }
  return _tsRequest(`${_tsOrg(orgId)}/members/search?email=${encodeURIComponent(email)}`);
}

async function tsAddMember(orgId, userId) {
  if (tsMockEnabled()) {
    const store = _tsMockStore();
    const acct = store.directory.find(u => u.id === userId);
    if (acct && !store.members.some(m => m.id === userId)) store.members.push(_clone(acct));
    return _delay({ ok: true });
  }
  return _tsRequest(`${_tsOrg(orgId)}/members/${encodeURIComponent(userId)}`, { method: 'POST', body: {} });
}

async function tsRemoveMember(orgId, userId) {
  if (tsMockEnabled()) {
    const store = _tsMockStore();
    store.members = store.members.filter(m => m.id !== userId);
    return _delay({ ok: true });
  }
  return _tsRequest(`${_tsOrg(orgId)}/members/${encodeURIComponent(userId)}`, { method: 'DELETE' });
}

// ──────────────────────────────────────────────────────────────────────────
// Skills — My Cloud Skills (MyCloudSkillRead) + Team library (SkillRead).
// Both carry a nested `bundle` (skill_name/description/...) and a `bundle_id`.
// ──────────────────────────────────────────────────────────────────────────
async function tsCreateUserSkill(orgId, { name, description, version, files, metadata }) {
  if (tsMockEnabled()) {
    const store = _tsMockStore();
    const at = _tsNow();
    const hash = Math.random().toString(16).slice(2, 8);
    const bundleId = `bdl_mock_${store.seq}`;
    const bundle = _tsBundle(bundleId, name, description || null, hash);
    store.bundles[bundleId] = bundle;
    const rec = {
      id: `mcs_mock_${store.seq++}`, user_account_id: 'usr_mock_1', bundle_id: bundleId,
      source: (metadata && metadata.source) || 'manual', enabled: true, pinned: false,
      user_note: null, user_tags: [], metadata: metadata || {}, created_at: at, updated_at: at, bundle,
    };
    store.myCloudSkills.unshift(rec);
    return _delay(_clone(rec));
  }
  // Skill name/description are derived from SKILL.md server-side — the create
  // body is just the files + metadata (user resource, not org scoped).
  return _tsRequest(`${_tsUser()}/users/me/skills`, {
    method: 'POST', body: { files, metadata: metadata || {} },
  });
}

async function tsListUserSkills(orgId) {
  if (tsMockEnabled()) return _delay(_clone(_tsMockStore().myCloudSkills));
  return _tsRequest(`${_tsUser()}/users/me/skills`);
}

async function tsDeleteUserSkill(orgId, cloudSkillId) {
  if (tsMockEnabled()) {
    const store = _tsMockStore();
    store.myCloudSkills = store.myCloudSkills.filter(s => s.id !== cloudSkillId);
    return _delay({ id: cloudSkillId, deleted: true });
  }
  return _tsRequest(`${_tsUser()}/users/me/skills/${encodeURIComponent(cloudSkillId)}`, { method: 'DELETE' });
}

// Share My Cloud Skills (by cloud_skill_id) into the org's team library.
async function tsShareSkills(orgId, cloudSkillIds, version) {
  if (tsMockEnabled()) {
    const store = _tsMockStore();
    const at = _tsNow();
    const shared = [];
    cloudSkillIds.forEach(cid => {
      const mc = store.myCloudSkills.find(s => s.id === cid);
      if (!mc) return;
      const existing = store.teamSkills.find(t => t.bundle_id === mc.bundle_id);
      const rec = existing || {
        id: `tsk_mock_${store.seq++}`, org_account_id: orgId || 'org_mock_1', bundle_id: mc.bundle_id,
        shared_by_user_account_id: mc.user_account_id, team_note: null, team_tags: [], policy: {},
        metadata: {}, created_at: at, updated_at: at, bundle: mc.bundle,
      };
      if (!existing) store.teamSkills.unshift(rec);
      shared.push(rec);
    });
    return _delay(_clone(shared));
  }
  const data = await _tsRequest(`${_tsTeam(orgId)}/library/skills/share`, {
    method: 'POST', body: { cloud_skill_ids: cloudSkillIds },
  });
  return (data && data.skills) || [];
}

async function tsListSharedSkills(orgId) {
  if (tsMockEnabled()) return _delay(_clone(_tsMockStore().teamSkills));
  return _tsRequest(`${_tsTeam(orgId)}/library/skills`);
}

// Add a team-library skill (by team_skill_id) to the caller's My Cloud Skills.
async function tsImportTeamSkill(orgId, teamSkillId) {
  if (tsMockEnabled()) {
    const store = _tsMockStore();
    const ts = store.teamSkills.find(s => s.id === teamSkillId);
    const at = _tsNow();
    if (ts && !store.myCloudSkills.some(s => s.bundle_id === ts.bundle_id)) {
      store.myCloudSkills.unshift({
        id: `mcs_mock_${store.seq++}`, user_account_id: 'usr_mock_1', bundle_id: ts.bundle_id,
        source: 'imported_from_team', enabled: true, pinned: false, user_note: null, user_tags: [],
        metadata: { imported_from_team: true }, created_at: at, updated_at: at, bundle: ts.bundle,
      });
    }
    return _delay({ ok: true });
  }
  return _tsRequest(`${_tsTeam(orgId)}/users/me/skills/imports/team`, {
    method: 'POST', body: { team_skill_id: teamSkillId },
  });
}

// Save the PERSONAL note (user_note) on a My Cloud Skill (by cloud_skill_id).
// Returns { cloud_skill_id, note, updated_at, skipped_notes }.
async function tsSavePersonalNote(orgId, cloudSkillId, note) {
  if (tsMockEnabled()) {
    const store = _tsMockStore();
    const at = _tsNow();
    const mc = store.myCloudSkills.find(s => s.id === cloudSkillId);
    const skipped = [];
    if (mc) {
      const old = mc.user_note || '';
      mc.user_note = note; mc.updated_at = at;
      // Link-while-equal: propagate to the team note for the same bundle if equal.
      store.teamSkills.filter(t => t.bundle_id === mc.bundle_id).forEach(t => {
        if ((t.team_note || '') === old) { t.team_note = note; t.updated_at = at; }
        else if ((t.team_note || '') !== note) skipped.push({ scope: 'team', team_skill_id: t.id });
      });
    }
    return _delay({ cloud_skill_id: cloudSkillId, note, updated_at: at, skipped_notes: skipped });
  }
  return _tsRequest(`${_tsUser()}/users/me/skills/${encodeURIComponent(cloudSkillId)}/note`, {
    method: 'PUT', body: { note },
  });
}

// Save the TEAM note (team_note) on a team-library skill (by team_skill_id).
// Returns { team_skill_id, note, updated_by, updated_at, skipped_notes }.
async function tsSaveTeamNote(orgId, teamSkillId, note) {
  if (tsMockEnabled()) {
    const store = _tsMockStore();
    const at = _tsNow();
    const ts = store.teamSkills.find(s => s.id === teamSkillId);
    const skipped = [];
    if (ts) {
      const old = ts.team_note || '';
      ts.team_note = note; ts.updated_at = at;
      store.myCloudSkills.filter(m => m.bundle_id === ts.bundle_id).forEach(m => {
        if ((m.user_note || '') === old) { m.user_note = note; m.updated_at = at; }
        else if ((m.user_note || '') !== note) skipped.push({ scope: 'my_cloud', cloud_skill_id: m.id });
      });
    }
    return _delay({ team_skill_id: teamSkillId, note, updated_by: 'you@demo', updated_at: at, skipped_notes: skipped });
  }
  return _tsRequest(`${_tsTeam(orgId)}/library/skills/${encodeURIComponent(teamSkillId)}/note`, {
    method: 'PUT', body: { note },
  });
}

// Fetch the full SKILL.md text (+ file list) for a bundle, on demand for the
// "View SKILL.md" panel. Returns { bundle_id, skill_md, files }.
async function tsGetSkillFiles(orgId, bundleId) {
  if (tsMockEnabled()) {
    return _delay({
      bundle_id: bundleId,
      skill_md: '---\nname: demo-skill\ndescription: A demo skill\n---\n\n# Demo skill\n\nThis is the full SKILL.md content shown in mock mode.',
      files: ['SKILL.md'],
    });
  }
  // Prefer the user's own-bundle endpoint (works for any skill in My Cloud); for
  // a team-only skill the user hasn't imported, fall back to the org library
  // endpoint, which any org member may read.
  try {
    return await _tsRequest(`${_tsUser()}/users/me/skills/bundles/${encodeURIComponent(bundleId)}/files`);
  } catch (e) {
    if (orgId && (e.status === 404 || e.status === 403)) {
      return _tsRequest(`${_tsTeam(orgId)}/library/skills/bundles/${encodeURIComponent(bundleId)}/files`);
    }
    throw e;
  }
}

// Delete a skill from the org's team library (by team_skill_id).
async function tsDeleteLibrarySkill(orgId, teamSkillId) {
  if (tsMockEnabled()) {
    const store = _tsMockStore();
    store.teamSkills = store.teamSkills.filter(s => s.id !== teamSkillId);
    return _delay({ team_skill_id: teamSkillId, deleted: true });
  }
  return _tsRequest(`${_tsTeam(orgId)}/library/skills/${encodeURIComponent(teamSkillId)}`, { method: 'DELETE' });
}

// ──────────────────────────────────────────────────────────────────────────
// Prompts — personal prompts are user-scoped (/team-sync/users/me/prompts);
// the shared library is org-scoped and returns PromptRead (keyed by prompt_id).
// ──────────────────────────────────────────────────────────────────────────
async function tsCreateUserPrompt(orgId, { name, description, content }) {
  if (tsMockEnabled()) {
    const store = _tsMockStore();
    const at = _tsNow();
    const rec = {
      id: `prp_mock_${store.seq++}`, owner_user_account_id: 'usr_mock_1',
      name, description: description || null, content, created_at: at, updated_at: at,
    };
    store.userPrompts.unshift(rec);
    return _delay(_clone(rec));
  }
  return _tsRequest(`${_tsUser()}/users/me/prompts`, { method: 'POST', body: { name, description, content } });
}

async function tsListUserPrompts(orgId) {
  if (tsMockEnabled()) return _delay(_clone(_tsMockStore().userPrompts));
  return _tsRequest(`${_tsUser()}/users/me/prompts`);
}

async function tsDeleteUserPrompt(orgId, promptId) {
  if (tsMockEnabled()) {
    const store = _tsMockStore();
    store.userPrompts = store.userPrompts.filter(p => p.id !== promptId);
    return _delay({ id: promptId, deleted: true });
  }
  return _tsRequest(`${_tsUser()}/users/me/prompts/${encodeURIComponent(promptId)}`, { method: 'DELETE' });
}

async function tsSharePrompts(orgId, promptIds, version) {
  if (tsMockEnabled()) {
    const store = _tsMockStore();
    const at = _tsNow();
    const shared = [];
    promptIds.forEach(pid => {
      const up = store.userPrompts.find(p => p.id === pid);
      if (!up) return;
      const rec = {
        prompt_id: `prp_shared_mock_${store.seq++}`,
        org_account_id: orgId || 'org_mock_1', name: up.name, description: up.description || '',
        uploader_user_account_id: up.owner_user_account_id,
        content: up.content, created_at: at,
      };
      store.sharedPrompts.unshift(rec);
      shared.push(rec);
    });
    return _delay(_clone(shared));
  }
  const data = await _tsRequest(`${_tsTeam(orgId)}/library/prompts/share`, {
    method: 'POST', body: { prompt_ids: promptIds },
  });
  return (data && data.prompts) || [];
}

async function tsListSharedPrompts(orgId) {
  if (tsMockEnabled()) return _delay(_clone(_tsMockStore().sharedPrompts));
  return _tsRequest(`${_tsTeam(orgId)}/library/prompts`);
}

async function tsCreateLibraryPrompt(orgId, { name, description, content }) {
  if (tsMockEnabled()) {
    const store = _tsMockStore();
    const at = _tsNow();
    const rec = {
      prompt_id: `prp_shared_mock_${store.seq++}`,
      org_account_id: orgId || 'org_mock_1', name, description: description || '',
      uploader_user_account_id: 'usr_mock_1', content, created_at: at,
    };
    store.sharedPrompts.unshift(rec);
    return _delay(_clone(rec));
  }
  return _tsRequest(`${_tsTeam(orgId)}/library/prompts`, {
    method: 'POST', body: { name, description: description || '', content },
  });
}

// Partial update of a team-library prompt. `description` must be a string; pass
// "" to clear it (the backend rejects null).
async function tsUpdateLibraryPrompt(orgId, promptId, { name, description, content }) {
  if (tsMockEnabled()) {
    const store = _tsMockStore();
    const rec = store.sharedPrompts.find(p => p.prompt_id === promptId);
    if (rec) {
      if (name !== undefined) rec.name = name;
      if (description !== undefined) rec.description = description;
      if (content !== undefined) rec.content = content;
    }
    return _delay(_clone(rec || { prompt_id: promptId }));
  }
  const body = {};
  if (name !== undefined) body.name = name;
  if (description !== undefined) body.description = description;
  if (content !== undefined) body.content = content;
  return _tsRequest(`${_tsTeam(orgId)}/library/prompts/${encodeURIComponent(promptId)}`, {
    method: 'PATCH', body,
  });
}

async function tsDeleteLibraryPrompt(orgId, promptId) {
  if (tsMockEnabled()) {
    const store = _tsMockStore();
    store.sharedPrompts = store.sharedPrompts.filter(p => p.prompt_id !== promptId);
    return _delay({ prompt_id: promptId, deleted: true });
  }
  return _tsRequest(`${_tsTeam(orgId)}/library/prompts/${encodeURIComponent(promptId)}`, { method: 'DELETE' });
}

// ──────────────────────────────────────────────────────────────────────────
// Devices / agents
// ──────────────────────────────────────────────────────────────────────────
async function tsCreateInstallCommand(orgId, { platform, owner_user_account_id, name, hostname, os }) {
  if (tsMockEnabled()) {
    const token = 'enr_' + Math.random().toString(16).slice(2, 10);
    const base = '<backend>';
    const cmd = platform === 'windows'
      ? `$env:TEAM_SYNC_SERVER_URL="${base}/team-sync"; $env:TEAM_SYNC_ENROLLMENT_TOKEN="${token}"; iwr ${base}/team-sync/api/daemon/package.zip -OutFile daemon.zip`
      : `TEAM_SYNC_SERVER_URL=${base}/team-sync TEAM_SYNC_ENROLLMENT_TOKEN=${token} curl -fsSL ${base}/team-sync/api/daemon/package.zip -o daemon.zip`;
    return _delay({ platform, enrollment_id: 'enrid_mock', enrollment_token: token, package_url: `${base}/team-sync/api/daemon/package.zip`, install_command: cmd });
  }
  // The enrolling device belongs to the current user (not an org); the body
  // carries the chosen display name + optional hostname/os hints.
  return _tsRequest(`${_tsUser()}/devices/install-command`, {
    method: 'POST',
    body: { platform, display_name: name, hostname: hostname || null, os: os || platform },
  });
}

async function tsListDevices(orgId) {
  if (tsMockEnabled()) return _delay(_clone(_tsMockStore().devices));
  return _tsRequest(`${_tsUser()}/devices`);
}

async function tsListAgents(orgId, deviceId) {
  if (tsMockEnabled()) return _delay(_clone(_tsMockStore().agents[deviceId] || []));
  return _tsRequest(`${_tsUser()}/devices/${encodeURIComponent(deviceId)}/agents`);
}

// Per-device inventory (all agents' installed skills). Each item carries
// bundle_id (content) once known, plus folded user_note / team_note.
async function tsListDeviceInventory(orgId, deviceId) {
  if (tsMockEnabled()) return _delay(_clone(_tsMockStore().inventory[deviceId] || []));
  return _tsRequest(`${_tsUser()}/devices/${encodeURIComponent(deviceId)}/inventory`);
}

async function tsListAgentInventory(orgId, deviceId, agentId) {
  if (tsMockEnabled()) {
    const items = (_tsMockStore().inventory[deviceId] || []).filter(i => i.device_agent_id === agentId);
    return _delay(_clone(items));
  }
  return _tsRequest(`${_tsUser()}/devices/${encodeURIComponent(deviceId)}/agents/${encodeURIComponent(agentId)}/inventory`);
}

// Import local (unmanaged) inventory skills into the caller's My Cloud Skills.
async function tsImportLocalSkills(orgId, inventoryItemIds, version) {
  if (tsMockEnabled()) {
    const store = _tsMockStore();
    const at = _tsNow();
    // Mock imports resolve synchronously from the stored bundle, so every item
    // lands in imported[] (no daemon round-trip) and devices[] stays empty.
    const imported = [];
    inventoryItemIds.forEach(invId => {
      let inv = null;
      Object.values(store.inventory).forEach(arr => { const f = arr.find(i => i.id === invId); if (f) inv = f; });
      if (!inv) return;
      const bundleId = inv.bundle_id || `bdl_mock_${invId}`;
      const desc = (inv.metadata && inv.metadata.description) || null;
      if (!store.bundles[bundleId]) store.bundles[bundleId] = _tsBundle(bundleId, inv.name, desc, inv.sha256 || 'mock');
      const existing = store.myCloudSkills.find(s => s.bundle_id === bundleId);
      if (existing) { imported.push({ local_skill_instance_id: invId, cloud_skill_id: existing.id, name: inv.name }); return; }
      const cloudId = `mcs_mock_${store.seq++}`;
      store.myCloudSkills.unshift({
        id: cloudId, user_account_id: 'usr_mock_1', bundle_id: bundleId,
        source: 'local_skill_upload', enabled: true, pinned: false, user_note: null, user_tags: [],
        instances: [{ bundle_id: bundleId, machine_name: 'maya-desktop', absolute_path: inv.absolute_path, updated_at: at }],
        metadata: { source_path: inv.absolute_path }, created_at: at, updated_at: at, bundle: store.bundles[bundleId],
      });
      imported.push({ local_skill_instance_id: invId, cloud_skill_id: cloudId, name: inv.name });
    });
    return _delay({ request_id: 'imp_mock', devices: [], imported });
  }
  return _tsRequest(`${_tsUser()}/library/imports/local-skills`, {
    method: 'POST', body: { local_skill_instance_ids: inventoryItemIds },
  });
}

// Delete local skill directories from the device(s) holding the inventory items.
async function tsDeleteLocalSkills(orgId, inventoryItemIds) {
  if (tsMockEnabled()) {
    const store = _tsMockStore();
    Object.keys(store.inventory).forEach(dev => {
      store.inventory[dev] = store.inventory[dev].filter(i => !inventoryItemIds.includes(i.id));
    });
    return _delay({ request_id: 'del_mock', devices: [{ device_id: 'dev_mock_1', sent: true, item_count: inventoryItemIds.length }] });
  }
  return _tsRequest(`${_tsUser()}/devices/local-skills/delete`, {
    method: 'POST', body: { local_skill_instance_ids: inventoryItemIds },
  });
}

async function tsCreateUninstallCommand(orgId, platform) {
  if (tsMockEnabled()) {
    const base = '<backend>';
    const cmd = platform === 'windows'
      ? `iwr ${base}/team-sync/api/daemon/package.zip -OutFile daemon.zip; Expand-Archive daemon.zip; ./uninstall-daemon.ps1`
      : `curl -fsSL ${base}/team-sync/api/daemon/package.zip -o daemon.zip && unzip -o daemon.zip && ./uninstall-daemon.sh`;
    return _delay({ platform, package_url: `${base}/team-sync/api/daemon/package.zip`, uninstall_command: cmd });
  }
  return _tsRequest(`${_tsUser()}/devices/uninstall-command`, { method: 'POST', body: { platform } });
}

async function tsDeleteDevice(orgId, deviceId) {
  if (tsMockEnabled()) {
    const store = _tsMockStore();
    store.devices = store.devices.filter(d => d.id !== deviceId);
    delete store.agents[deviceId];
    delete store.inventory[deviceId];
    return _delay({ device_id: deviceId, deleted: true, access_revoked: true, device_deleted: true });
  }
  return _tsRequest(`${_tsUser()}/devices/${encodeURIComponent(deviceId)}`, { method: 'DELETE' });
}

// ──────────────────────────────────────────────────────────────────────────
// Deployments / operations — keyed by cloud_skill_id (the user's My Cloud copy).
// The backend resolves the cloud skill to its content bundle; operations still
// report a bundle_id.
// ──────────────────────────────────────────────────────────────────────────
function _tsMockDeploy(action, cloudSkillId, targets) {
  const store = _tsMockStore();
  const at = _tsNow();
  const mc = store.myCloudSkills.find(s => s.id === cloudSkillId);
  const bundleId = mc ? mc.bundle_id : null;
  if (action === 'remove') {
    targets.forEach(t => {
      if (store.inventory[t.device_id]) {
        store.inventory[t.device_id] = store.inventory[t.device_id].filter(
          i => !(i.device_agent_id === t.agent_id && i.bundle_id === bundleId)
        );
      }
    });
  } else if (action === 'install' && bundleId) {
    // Reflect the install in the device inventory so the demo shows a closed
    // loop (the real daemon reports this back over the websocket).
    const name = (mc.bundle && mc.bundle.skill_name) || mc.bundle_id;
    const key = String(name).toLowerCase().replace(/\s+/g, '-');
    targets.forEach(t => {
      const arr = store.inventory[t.device_id] || (store.inventory[t.device_id] = []);
      if (arr.some(i => i.device_agent_id === t.agent_id && i.bundle_id === bundleId)) return;
      arr.push({
        id: `inv_mock_${store.seq++}`, device_id: t.device_id, device_agent_id: t.agent_id,
        source_key: key, name, absolute_path: `~/.skills/${key}`, root_path: '~/.skills', relative_path: key,
        sha256: (mc.bundle && mc.bundle.bundle_hash) || 'mock', managed: true, status: 'present', drifted: false,
        metadata: {}, discovered_at: at, last_seen_at: at, updated_at: at, bundle_id: bundleId,
      });
    });
  }
  const ops = targets.map(t => {
    const id = `op_mock_${store.seq++}`;
    const op = {
      id, device_id: t.device_id, device_agent_id: t.agent_id,
      bundle_id: bundleId, action, status: 'sent', requested_by_user_account_id: 'usr_mock_1',
      sequence: store.seq, attempts: 1, error: null, created_at: at, updated_at: at, sent_at: at, finished_at: null,
    };
    store.operations[id] = op;
    setTimeout(() => {
      const live = store.operations[id];
      if (!live) return;
      live.status = 'succeeded'; live.finished_at = _tsNow(); live.updated_at = live.finished_at;
    }, 1500);
    return op;
  });
  return ops;
}

async function tsInstall(orgId, cloudSkillId, targets) {
  if (tsMockEnabled()) return _delay(_clone(_tsMockDeploy('install', cloudSkillId, targets)));
  const data = await _tsRequest(`${_tsUser()}/deployments/install`, {
    method: 'POST', body: { cloud_skill_id: cloudSkillId, targets },
  });
  return (data && data.operations) || [];
}

async function tsRemove(orgId, cloudSkillId, targets) {
  if (tsMockEnabled()) return _delay(_clone(_tsMockDeploy('remove', cloudSkillId, targets)));
  const data = await _tsRequest(`${_tsUser()}/deployments/remove`, {
    method: 'POST', body: { cloud_skill_id: cloudSkillId, targets },
  });
  return (data && data.operations) || [];
}

async function tsGetOperation(orgId, operationId) {
  if (tsMockEnabled()) {
    const op = _tsMockStore().operations[operationId];
    if (!op) { const e = new Error('operation not found'); e.status = 404; throw e; }
    return _delay(_clone(op), 120);
  }
  return _tsRequest(`${_tsUser()}/operations/${encodeURIComponent(operationId)}`);
}

// ──────────────────────────────────────────────────────────────────────────
// Auto-sync — fully BACKEND controlled.
//
// The backend persists the per-user flag and owns ALL reconciliation: which My
// Cloud skill goes to which agent (unique name → every online agent; duplicate
// name → only matching agent_type), and WHEN (server-side triggers: skill
// uploaded, agent online, …). The frontend's entire role is:
//   1. read/write the flag — tsGetAutoSync / tsSetAutoSync
//   2. animate the enable response (plan + operation_ids), awaiting the
//      operations with tsAwaitOperations.
// Everything below the flag helpers is MOCK-ONLY: it synthesizes the same
// response shapes so the offline demo's animation still works without a backend.
// ──────────────────────────────────────────────────────────────────────────
function _tsSkillName(s) { return (s.bundle && s.bundle.skill_name) || s.name || ''; }
function _tsSkillAgentType(s) { return (s && s.metadata && (s.metadata.agent_type || s.metadata.source_agent_type)) || null; }

// MOCK ONLY — build the desired plan (online agents per skill) for the demo.
// Returns { skills, agents, assignments }.
async function tsComputeAutoSyncPlan(orgId) {
  const [mine, devices] = await Promise.all([tsListUserSkills(orgId), tsListDevices(orgId)]);
  const visible = (devices || []).filter(d => d.status !== 'removed');
  const agents = [];
  for (const d of visible) {
    try {
      const ags = await tsListAgents(orgId, d.id);
      (ags || []).forEach(a => {
        if (a.status !== 'online') return;   // skip offline agents — filled in on reconnect
        agents.push({
          device_id: d.id, device_name: d.display_name, agent_id: a.id,
          agent_type: a.agent_type, label: a.label, status: a.status,
        });
      });
    } catch (e) { /* skip a device we can't read */ }
  }
  const nameCount = {};
  (mine || []).forEach(s => { const n = _tsSkillName(s); nameCount[n] = (nameCount[n] || 0) + 1; });
  const assignments = (mine || []).map(s => {
    const name = _tsSkillName(s);
    const agentType = _tsSkillAgentType(s);
    const duplicate = nameCount[name] > 1;
    const matched = duplicate ? agents.filter(a => agentType && a.agent_type === agentType) : agents.slice();
    return {
      cloud_skill_id: s.id, name, agent_type: agentType, duplicate,
      targets: matched.map(t => ({ device_id: t.device_id, agent_id: t.agent_id, agent_type: t.agent_type, device_name: t.device_name, label: t.label })),
      skipped: duplicate && matched.length === 0,
    };
  });
  return { skills: mine || [], agents, assignments };
}

// MOCK ONLY — dispatch install operations for a precomputed plan so the offline
// demo's first-run animation has real operation ids to await. In live mode the
// BACKEND creates and pushes these operations; the frontend never does.
async function tsDispatchAutoSync(orgId, plan) {
  const active = (plan.assignments || []).filter(a => a.targets && a.targets.length);
  const results = await Promise.all(active.map(async a => {
    const targets = a.targets.map(t => ({ device_id: t.device_id, agent_id: t.agent_id }));
    try { return (await tsInstall(orgId, a.cloud_skill_id, targets)) || []; }
    catch (e) { return null; }
  }));
  const operationIds = [];
  results.forEach(r => { if (r) r.forEach(op => { if (op && op.id) operationIds.push(op.id); }); });
  return { operationIds };
}

// Reshape the mock plan ({skills, agents, assignments}) into the backend's
// AutoSyncPlanRead shape ({skills_synced, skipped, agents:[{…, skill_count}]})
// so the overlay consumes one shape regardless of mode.
function _tsShapePlan(plan) {
  const agents = plan.agents || [];
  const assignments = plan.assignments || [];
  const countFor = (d, a) => assignments.reduce((n, x) => n + ((x.targets || []).some(t => t.device_id === d && t.agent_id === a) ? 1 : 0), 0);
  return {
    skills_synced: assignments.filter(a => (a.targets || []).length > 0).length,
    skipped: assignments.filter(a => a.skipped).length,
    agents: agents.map(a => ({
      device_id: a.device_id, device_name: a.device_name, agent_id: a.agent_id,
      agent_type: a.agent_type, label: a.label, skill_count: countFor(a.device_id, a.agent_id),
    })),
  };
}

// Wait until the given deployment operations reach a terminal state (succeeded /
// failed / cancelled), or a cap elapses. Lets a caller hold a "syncing" UI until
// the installs actually finish instead of guessing with a timer. Polls all
// pending operations in parallel; an unreadable/missing operation is dropped
// from tracking. Returns { done, remaining }.
async function tsAwaitOperations(orgId, operationIds, opts) {
  const o = opts || {};
  const timeoutMs = o.timeoutMs != null ? o.timeoutMs : 15000;
  const intervalMs = o.intervalMs != null ? o.intervalMs : 700;
  const pending = new Set((operationIds || []).filter(Boolean));
  if (!pending.size) return { done: true, remaining: 0 };
  // Backend OperationRead.status terminal values (pending/sent/running are
  // in-flight). Anything still in-flight at timeoutMs is left to the cap.
  const terminal = new Set(['succeeded', 'failed']);
  const start = Date.now();
  while (pending.size && (Date.now() - start) < timeoutMs) {
    await new Promise(r => setTimeout(r, intervalMs));
    await Promise.all(Array.from(pending).map(async id => {
      try {
        const op = await tsGetOperation(orgId, id);
        if (op && terminal.has(op.status)) pending.delete(id);
      } catch (e) { pending.delete(id); }
    }));
  }
  return { done: pending.size === 0, remaining: pending.size };
}

// AUTO SYNC flag. The BACKEND persists it (per user) and owns all ongoing
// reconciliation — server-side triggers (skill uploaded, agent online, …) keep
// every agent in sync with no involvement from this client. The frontend only
// reads/writes the flag and animates the enable response. Mock mode falls back
// to localStorage so the offline demo still works.
const PARCLE_TS_AUTOSYNC_KEY = 'parcle.tsAutoSync';
function _tsAutoSyncLocal() { try { return localStorage.getItem(PARCLE_TS_AUTOSYNC_KEY) === '1'; } catch (e) { return false; } }
function _tsSetAutoSyncLocal(on) {
  try {
    if (on) localStorage.setItem(PARCLE_TS_AUTOSYNC_KEY, '1');
    else localStorage.removeItem(PARCLE_TS_AUTOSYNC_KEY);
  } catch (e) { /* storage disabled */ }
}

// Read the persisted flag. Returns { enabled }.
async function tsGetAutoSync(orgId) {
  if (tsMockEnabled()) return _delay({ enabled: _tsAutoSyncLocal() }, 120);
  return _tsRequest(`${_tsUser()}/users/me/auto-sync`);
}

// Toggle the flag. Enabling returns { enabled, plan, operation_ids }: the BACKEND
// has already created + pushed the install operations, so the frontend just
// animates `plan` and awaits `operation_ids`. Disabling clears the flag (the
// backend stops reconciling; nothing is uninstalled).
async function tsSetAutoSync(orgId, enabled) {
  if (tsMockEnabled()) {
    _tsSetAutoSyncLocal(enabled);
    if (!enabled) return _delay({ enabled: false, plan: { skills_synced: 0, skipped: 0, agents: [] }, operation_ids: [] });
    const raw = await tsComputeAutoSyncPlan(orgId);
    const res = await tsDispatchAutoSync(orgId, raw);
    return { enabled: true, plan: _tsShapePlan(raw), operation_ids: res.operationIds || [] };
  }
  return _tsRequest(`${_tsUser()}/users/me/auto-sync`, { method: 'PUT', body: { enabled } });
}

// ──────────────────────────────────────────────────────────────────────────
// Local daemon (add skills from a folder). The daemon runs on this machine and
// can read hidden folders (.claude) + pop a native folder picker. We reach it
// at 127.0.0.1 — loopback is a secure context, so HTTPS→http is allowed; the
// daemon returns CORS/PNA headers for the parcle origin.
// ──────────────────────────────────────────────────────────────────────────
const DAEMON_LOCAL = 'http://127.0.0.1:38271';

async function tsLocalDaemon() {
  try {
    const res = await fetch(DAEMON_LOCAL + '/whoami', { mode: 'cors' });
    if (!res.ok) return null;
    return await res.json();
  } catch (e) { return null; }
}

async function tsLocalPickAndUpload() {
  const res = await fetch(DAEMON_LOCAL + '/v1/pick-and-upload', { method: 'POST', mode: 'cors' });
  return await res.json();
}

async function tsLocalScanPath(path) {
  const res = await fetch(DAEMON_LOCAL + '/v1/scan-path', {
    method: 'POST', mode: 'cors',
    headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path }),
  });
  return await res.json();
}

// Local Folder connector: daemon pops a native folder picker, then hashes +
// prechecks + uploads the documents inside into memory (deduped by device+path).
async function tsLocalPickAndIngest() {
  const res = await fetch(DAEMON_LOCAL + '/v1/pick-and-ingest', { method: 'POST', mode: 'cors' });
  return await res.json();
}

// Headless / remote device: ask the backend to relay a scan-path command to
// that device's daemon over the websocket.
async function tsImportFromPath(orgId, deviceId, path) {
  return _tsRequest(`${_tsUser()}/devices/${encodeURIComponent(deviceId)}/import-from-path`, {
    method: 'POST', body: { path },
  });
}

Object.assign(window, {
  tsSelectedOrg, tsSetSelectedOrg, tsMockEnabled,
  tsListMyOrgs, tsListMembers, tsSearchUserByEmail, tsAddMember, tsRemoveMember,
  tsCreateUserSkill, tsListUserSkills, tsDeleteUserSkill, tsShareSkills, tsListSharedSkills, tsImportTeamSkill,
  tsDeleteLibrarySkill, tsDeleteLibraryPrompt, tsSavePersonalNote, tsSaveTeamNote, tsGetSkillFiles,
  tsLocalDaemon, tsLocalPickAndUpload, tsLocalScanPath, tsLocalPickAndIngest, tsImportFromPath,
  tsCreateUserPrompt, tsListUserPrompts, tsDeleteUserPrompt, tsSharePrompts, tsListSharedPrompts, tsCreateLibraryPrompt, tsUpdateLibraryPrompt,
  tsCreateInstallCommand, tsCreateUninstallCommand, tsListDevices, tsListAgents, tsDeleteDevice,
  tsListDeviceInventory, tsListAgentInventory, tsImportLocalSkills, tsDeleteLocalSkills,
  tsInstall, tsRemove, tsGetOperation,
  tsGetAutoSync, tsSetAutoSync, tsAwaitOperations,
});
