'use client';

import { useState, useEffect, useCallback, useRef, lazy, Suspense } from 'react';
import { useSidebarResize } from '@/hooks/useSidebarResize';

const PluginsPanel = lazy(() => import('./PluginsPanel'));
const ConnectorsPanel = lazy(() => import('./ConnectorsPanel'));
const CraftsMarketplacePanelLazy = lazy(() => import('./CraftsMarketplacePanel'));

type ItemType = 'skill' | 'command';

interface Skill {
  name: string;
  type: ItemType;
  displayName: string;
  description: string;
  author: string;
  version: string;
  tags: string[];
  score: number;
  rating: number;
  sourceUrl: string;
  installedGlobal: boolean;
  installedVersion: string;
  hasUpdate: boolean;
  installedProjects: string[];
  deletedRemotely: boolean;
  source?: 'registry' | 'local';
}

interface ProjectInfo {
  path: string;
  name: string;
}

// ─── Skill File Tree (collapsible directories) ──────────

interface TreeNode {
  name: string;
  path: string;
  type: 'file' | 'dir';
  children: TreeNode[];
}

function buildTree(files: { name: string; path: string; type: string }[]): TreeNode[] {
  const root: TreeNode[] = [];
  const dirMap = new Map<string, TreeNode>();

  for (const f of files) {
    const parts = f.path.split('/');
    if (f.type === 'dir') {
      const node: TreeNode = { name: f.name.replace(/\/$/, ''), path: f.path, type: 'dir', children: [] };
      dirMap.set(f.path, node);
      // Find parent
      const parentPath = parts.slice(0, -1).join('/');
      const parent = parentPath ? dirMap.get(parentPath) : null;
      if (parent) parent.children.push(node);
      else root.push(node);
    } else {
      const node: TreeNode = { name: f.name, path: f.path, type: 'file', children: [] };
      const parentPath = parts.slice(0, -1).join('/');
      const parent = parentPath ? dirMap.get(parentPath) : null;
      if (parent) parent.children.push(node);
      else root.push(node);
    }
  }
  return root;
}

function SkillFileTree({ files, activeFile, onSelect, collapseVersion = 0 }: {
  files: { name: string; path: string; type: string }[];
  activeFile: string | null;
  onSelect: (path: string) => void;
  collapseVersion?: number;
}) {
  const tree = buildTree(files);
  return <TreeNodeList nodes={tree} depth={0} activeFile={activeFile} onSelect={onSelect} collapseVersion={collapseVersion} />;
}

function TreeNodeList({ nodes, depth, activeFile, onSelect, collapseVersion = 0 }: {
  nodes: TreeNode[]; depth: number; activeFile: string | null; onSelect: (path: string) => void; collapseVersion?: number;
}) {
  const [expanded, setExpanded] = useState<Set<string>>(new Set(
    // Auto-expand first level
    nodes.filter(n => n.type === 'dir').map(n => n.path)
  ));

  // Collapse all on version bump
  useEffect(() => {
    if (collapseVersion > 0) setExpanded(new Set());
  }, [collapseVersion]);

  const toggle = (path: string) => {
    setExpanded(prev => {
      const next = new Set(prev);
      next.has(path) ? next.delete(path) : next.add(path);
      return next;
    });
  };

  return (
    <>
      {nodes.map(node => (
        node.type === 'dir' ? (
          <div key={node.path}>
            <button
              onClick={() => toggle(node.path)}
              className="w-full text-left px-1 py-0.5 text-[9px] text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-tertiary)] flex items-center gap-0.5"
              style={{ paddingLeft: `${depth * 10 + 4}px` }}
            >
              <span className="text-[8px]">{expanded.has(node.path) ? '▼' : '▶'}</span>
              <span>📁 {node.name}</span>
            </button>
            {expanded.has(node.path) && (
              <TreeNodeList nodes={node.children} depth={depth + 1} activeFile={activeFile} onSelect={onSelect} collapseVersion={collapseVersion} />
            )}
          </div>
        ) : (
          <button
            key={node.path}
            onClick={() => onSelect(node.path)}
            className={`w-full text-left py-0.5 text-[10px] truncate ${
              activeFile === node.path
                ? 'bg-[var(--accent)]/15 text-[var(--accent)]'
                : 'text-[var(--text-secondary)] hover:bg-[var(--bg-tertiary)] hover:text-[var(--text-primary)]'
            }`}
            style={{ paddingLeft: `${depth * 10 + 14}px` }}
            title={node.path}
          >
            {node.name}
          </button>
        )
      ))}
    </>
  );
}

export default function SkillsPanel({ projectFilter }: { projectFilter?: string }) {
  const { sidebarWidth, onSidebarDragStart } = useSidebarResize({ defaultWidth: 224, minWidth: 140, maxWidth: 400 });
  const [skills, setSkills] = useState<Skill[]>([]);
  const [projects, setProjects] = useState<ProjectInfo[]>([]);
  const [syncing, setSyncing] = useState(false);
  const [loading, setLoading] = useState(true);
  const [installTarget, setInstallTarget] = useState<{ skill: string; show: boolean }>({ skill: '', show: false });
  const [typeFilter, setTypeFilter] = useState<'all' | 'skill' | 'command' | 'local' | 'rules' | 'plugins' | 'connectors' | 'crafts' | 'recipes' | 'pipelines'>('connectors');
  const [localItems, setLocalItems] = useState<{ name: string; type: string; scope: string; fileCount: number; projectPath?: string }[]>([]);
  // Rules (CLAUDE.md templates)
  const [rulesTemplates, setRulesTemplates] = useState<{ id: string; name: string; description: string; tags: string[]; builtin: boolean; isDefault: boolean; content: string }[]>([]);
  const [rulesProjects, setRulesProjects] = useState<{ name: string; path: string }[]>([]);
  const [rulesSelectedTemplate, setRulesSelectedTemplate] = useState<string | null>(null);
  const [rulesEditing, setRulesEditing] = useState(false);
  const [rulesEditId, setRulesEditId] = useState('');
  const [rulesEditName, setRulesEditName] = useState('');
  const [rulesEditDesc, setRulesEditDesc] = useState('');
  const [rulesEditContent, setRulesEditContent] = useState('');
  const [rulesEditDefault, setRulesEditDefault] = useState(false);
  const [rulesShowNew, setRulesShowNew] = useState(false);
  const [rulesBatchProjects, setRulesBatchProjects] = useState<Set<string>>(new Set());
  const [searchQuery, setSearchQuery] = useState('');
  const [collapsedLocalSections, setCollapsedLocalSections] = useState<Set<string>>(new Set());
  const [expandedSkill, setExpandedSkill] = useState<string | null>(null);
  const [skillFiles, setSkillFiles] = useState<{ name: string; path: string; type: string }[]>([]);
  const [activeFile, setActiveFile] = useState<string | null>(null);
  const [skillTreeCollapseVersion, setSkillTreeCollapseVersion] = useState(0);
  const [fileContent, setFileContent] = useState<string>('');

  const fetchSkills = useCallback(async () => {
    try {
      const [registryRes, localRes] = await Promise.all([
        fetch('/api/skills'),
        fetch('/api/skills/local?action=scan&all=1'),
      ]);
      const data = await registryRes.json();
      setSkills(data.skills || []);
      setProjects(data.projects || []);
      const localData = await localRes.json();
      // Hide filesystem items that match a *synced* registry row (those
      // belong to the registry list). Uploaded items (source='local')
      // are intentionally not in this set so they appear under "Local"
      // — that's where users expect to find what they just uploaded.
      const registryNames = new Set(
        (data.skills || [])
          .filter((s: any) => s.source !== 'local')
          .map((s: any) => s.name)
      );
      setLocalItems((localData.items || []).filter((i: any) => !registryNames.has(i.name)));
    } catch {}
    setLoading(false);
  }, []);

  useEffect(() => { fetchSkills(); }, [fetchSkills]);

  const fetchRules = useCallback(async () => {
    try {
      const res = await fetch('/api/claude-templates?action=list');
      const data = await res.json();
      setRulesTemplates(data.templates || []);
      setRulesProjects(data.projects || []);
    } catch {}
  }, []);

  useEffect(() => { if (typeFilter === 'rules') fetchRules(); }, [typeFilter, fetchRules]);

  const saveRule = async () => {
    if (!rulesEditId || !rulesEditName || !rulesEditContent) return;
    await fetch('/api/claude-templates', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ action: 'save', id: rulesEditId, name: rulesEditName, description: rulesEditDesc, tags: [], content: rulesEditContent, isDefault: rulesEditDefault }),
    });
    setRulesEditing(false);
    setRulesShowNew(false);
    fetchRules();
  };

  const deleteRule = async (id: string) => {
    if (!confirm(`Delete template "${id}"?`)) return;
    await fetch('/api/claude-templates', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ action: 'delete', id }),
    });
    if (rulesSelectedTemplate === id) setRulesSelectedTemplate(null);
    fetchRules();
  };

  const toggleDefault = async (id: string, isDefault: boolean) => {
    await fetch('/api/claude-templates', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ action: 'set-default', id, isDefault }),
    });
    fetchRules();
  };

  const batchInject = async (templateId: string) => {
    const projects = [...rulesBatchProjects];
    if (!projects.length) return;
    await fetch('/api/claude-templates', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ action: 'inject', templateId, projects }),
    });
    setRulesBatchProjects(new Set());
    fetchRules();
  };

  const [syncProgress, setSyncProgress] = useState('');
  const [syncingGroup, setSyncingGroup] = useState<string>('');
  const [templatesSyncTick, setTemplatesSyncTick] = useState(0);
  const [uploading, setUploading] = useState(false);
  const skillUploadRef = useRef<HTMLInputElement | null>(null);

  async function uploadSkillFile(file: File) {
    setUploading(true);
    try {
      const fd = new FormData();
      fd.append('file', file);
      // Use the upload filename as the default install name. Without
      // this, the server falls back to frontmatter / wrapper-dir name,
      // which can silently clobber an unrelated skill (e.g. uploading
      // prelude-nac.zip whose wrapper dir is `prelude/` would overwrite
      // the registry-synced `prelude`).
      const baseName = file.name
        .replace(/\.(md|zip)$/i, '')
        .toLowerCase()
        .replace(/[^a-z0-9_-]/g, '-')
        .replace(/^-+|-+$/g, '');
      if (/^[a-z0-9][a-z0-9_-]*$/.test(baseName)) fd.append('name', baseName);
      const r = await fetch('/api/skills/install-local', { method: 'POST', body: fd });
      const j = await r.json();
      if (!r.ok || j.ok === false) {
        alert(`Upload failed: ${j.error || `HTTP ${r.status}`}`);
        return;
      }
      await fetchSkills();
      alert(`Uploaded as "${j.name}" → ${j.target}`);
    } catch (e) {
      alert(`Upload failed: ${e instanceof Error ? e.message : String(e)}`);
    } finally { setUploading(false); }
  }

  // Unified marketplace sync — replaces the old skills-only loop. Covers
  // every kind from every source (public + each enterprise tenant):
  //   - connectors (registry + manifests + wizard templates)
  //   - workflows (pipelines + recipes)
  //   - skills
  // The old per-tab Sync buttons were confusing — users couldn't tell
  // whether they covered enterprise sources or just public. One button,
  // one trip to the server.
  const sync = async () => {
    setSyncing(true);
    setSyncProgress('');
    try {
      const res = await fetch('/api/marketplace/sync-all', { method: 'POST' });
      const data = await res.json();
      if (!data.ok && data.error) {
        alert(`Sync failed: ${data.error}`);
      }
      await fetchSkills();
    } catch (err: any) {
      alert(`Sync failed: ${err.message || 'Network error'}`);
    } finally {
      setSyncing(false);
      setSyncProgress('');
    }
  };

  // Per-group sync — hits only the relevant endpoint, faster + clearer
  // than the catch-all sync-all when the user is staring at one category.
  // Each endpoint already walks every enterprise source for that kind.
  const syncGroup = async (group: 'extensions' | 'catalog' | 'templates') => {
    setSyncingGroup(group);
    try {
      const url = group === 'extensions'
        ? '/api/connectors/marketplace'
        : group === 'catalog'
          ? '/api/skills'
          : '/api/workflows/marketplace';
      const res = await fetch(url, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ action: 'sync' }),
      });
      const data = await res.json().catch(() => ({}));
      if (res.ok && data.ok === false && data.error) {
        alert(`Sync failed: ${data.error}`);
      } else if (!res.ok) {
        alert(`Sync failed: HTTP ${res.status}`);
      }
      await fetchSkills();
      if (group === 'templates') setTemplatesSyncTick(t => t + 1);
    } catch (err: any) {
      alert(`Sync failed: ${err.message || 'Network error'}`);
    } finally {
      setSyncingGroup('');
    }
  };

  const install = async (name: string, target: string) => {
    await fetch('/api/skills', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ action: 'install', name, target }),
    });
    setInstallTarget({ skill: '', show: false });
    fetchSkills();
    alert(`"${name}" installed. Restart Claude in terminal to apply.`);
  };

  const toggleDetail = async (name: string) => {
    if (expandedSkill === name) {
      setExpandedSkill(null);
      return;
    }
    setExpandedSkill(name);
    setSkillFiles([]);
    setActiveFile(null);
    setFileContent('');
    // Fetch file list from GitHub API
    try {
      const res = await fetch(`/api/skills?action=files&name=${encodeURIComponent(name)}`);
      const data = await res.json();
      const files = data.files || [];
      setSkillFiles(files);
      // Auto-select skill.md if exists, otherwise first file
      const defaultFile = files.find((f: any) => f.name === 'skill.md') || files.find((f: any) => f.type === 'file');
      if (defaultFile) loadFile(name, defaultFile.path);
    } catch { setSkillFiles([]); }
  };

  const loadFile = async (skillName: string, filePath: string, isLocalItem?: boolean, localType?: string, localProject?: string) => {
    setActiveFile(filePath);
    setFileContent('Loading...');
    try {
      let res;
      if (isLocalItem) {
        const projectParam = localProject ? `&project=${encodeURIComponent(localProject)}` : '';
        res = await fetch(`/api/skills/local?action=read&name=${encodeURIComponent(skillName)}&type=${localType || 'command'}&path=${encodeURIComponent(filePath)}${projectParam}`);
      } else {
        res = await fetch(`/api/skills?action=file&name=${encodeURIComponent(skillName)}&path=${encodeURIComponent(filePath)}`);
      }
      const data = await res.json();
      setFileContent(data.content || '(Empty)');
    } catch { setFileContent('(Failed to load)'); }
  };

  const uninstall = async (name: string, target: string) => {
    await fetch('/api/skills', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ action: 'uninstall', name, target }),
    });
    fetchSkills();
  };

  // Filter by project, type, and search
  const q = searchQuery.toLowerCase();
  const filtered = (typeFilter === 'local' ? [] : skills
    .filter(s => s.source !== 'local') // local-uploaded skills render under the Local section instead
    .filter(s => projectFilter ? (s.installedGlobal || s.installedProjects.includes(projectFilter)) : true)
    .filter(s => typeFilter === 'all' ? true : s.type === typeFilter)
    .filter(s => !q || s.name.toLowerCase().includes(q) || s.displayName.toLowerCase().includes(q) || s.description.toLowerCase().includes(q)
      || s.author.toLowerCase().includes(q) || s.tags.some(t => t.toLowerCase().includes(q)))
  ).sort((a, b) => a.displayName.localeCompare(b.displayName));

  const filteredLocal = localItems
    .filter(item => typeFilter === 'local' || typeFilter === 'all' || item.type === typeFilter)
    .filter(item => !q || item.name.toLowerCase().includes(q))
    .sort((a, b) => a.name.localeCompare(b.name));

  // Group local items by scope
  const localGroups = new Map<string, typeof localItems>();
  for (const item of filteredLocal) {
    const key = item.scope;
    if (!localGroups.has(key)) localGroups.set(key, []);
    localGroups.get(key)!.push(item);
  }

  const toggleLocalSection = (section: string) => {
    setCollapsedLocalSections(prev => {
      const next = new Set(prev);
      if (next.has(section)) next.delete(section);
      else next.add(section);
      return next;
    });
  };

  const skillCount = skills.filter(s => s.type === 'skill').length;
  const commandCount = skills.filter(s => s.type === 'command').length;
  const localCount = localItems.length;

  if (loading) {
    return <div className="p-4 text-xs text-[var(--text-secondary)]">Loading skills...</div>;
  }

  return (
    <div className="flex-1 flex flex-col min-h-0">
      {/* Header */}
      <div className="flex items-center justify-between px-4 py-2 border-b border-[var(--border)] shrink-0">
        <div className="flex items-center gap-2">
          <span className="text-xs font-semibold text-[var(--text-primary)]">Marketplace</span>
          {/* Three group-scoped dropdowns instead of one big <select>.
              Order is by usage frequency: Extensions (Connectors lives
              here) → Templates → Catalog. Each dropdown highlights when
              its current value is active; the inactive ones show the
              group label as a placeholder. Picking from any sets the
              single global typeFilter. */}
          {(() => {
            // Display order: Extensions (Connectors) → Catalog (Skills) →
            // Templates (Pipelines). Each group is an independent
            // switcher with its OWN sync button — the syncs cover
            // disjoint endpoints so users don't have to wait for the
            // catch-all sync-all to refresh just one category.
            const groups: Array<{
              key: 'extensions' | 'catalog' | 'templates';
              label: string;
              syncTitle: string;
              opts: Array<{ value: typeof typeFilter; label: string }>;
            }> = [
              { key: 'extensions', label: 'Extensions', syncTitle: 'Sync connectors (manifests + registry) from every source',
                opts: [
                  { value: 'connectors', label: 'Connectors' },
                  { value: 'plugins',    label: 'Plugins' },
                  { value: 'crafts',     label: 'Crafts' },
                ]},
              { key: 'catalog', label: 'Catalog', syncTitle: 'Sync skills & commands from every source',
                opts: [
                  { value: 'all',     label: `All (${skills.length})` },
                  { value: 'skill',   label: `Skills (${skillCount})` },
                  { value: 'command', label: `Commands (${commandCount})` },
                  { value: 'local',   label: `Local (${localCount})` },
                  { value: 'rules',   label: 'Rules' },
                ]},
              { key: 'templates', label: 'Templates', syncTitle: 'Sync pipelines & recipes from every source',
                opts: [
                  { value: 'pipelines',  label: 'Pipelines' },
                ]},
            ];
            return groups.map((g) => {
              const isActive = g.opts.some((o) => o.value === typeFilter);
              const groupSyncing = syncingGroup === g.key;
              return (
                <div key={g.key} className="flex items-center gap-0.5">
                  {g.opts.length === 1 ? (
                    <button
                      onClick={() => setTypeFilter(g.opts[0].value)}
                      className={`text-[10px] px-2 py-1 rounded border ${
                        isActive
                          ? 'border-[var(--accent)] text-[var(--accent)] bg-[var(--accent)]/10'
                          : 'border-[var(--border)] text-[var(--text-primary)] bg-[var(--bg-tertiary)] hover:border-[var(--text-secondary)]'
                      }`}
                    >{g.opts[0].label}</button>
                  ) : (
                    <select
                      value={isActive ? (typeFilter as string) : ''}
                      onChange={(e) => { if (e.target.value) setTypeFilter(e.target.value as typeof typeFilter); }}
                      className={`text-[10px] px-2 py-1 rounded bg-[var(--bg-tertiary)] border focus:outline-none ${
                        isActive
                          ? 'border-[var(--accent)] text-[var(--accent)]'
                          : 'border-[var(--border)] text-[var(--text-primary)] focus:border-[var(--accent)]'
                      }`}
                    >
                      <option value="" disabled hidden>{g.label}</option>
                      {g.opts.map((o) => (
                        <option key={o.value} value={o.value as string}>{o.label}</option>
                      ))}
                    </select>
                  )}
                  <button
                    onClick={() => void syncGroup(g.key)}
                    disabled={groupSyncing || syncing}
                    title={g.syncTitle}
                    className="text-[10px] px-1.5 py-1 rounded border border-[var(--border)] text-[var(--text-secondary)] hover:border-[var(--text-primary)] hover:text-[var(--text-primary)] transition-colors disabled:opacity-40"
                  >{groupSyncing ? '…' : '↻'}</button>
                </div>
              );
            });
          })()}
        </div>
        <span className="text-[8px] px-1.5 py-0.5 rounded bg-blue-500/15 text-blue-400">Claude Code</span>
        <input
          ref={skillUploadRef}
          type="file"
          accept=".md,.zip"
          className="hidden"
          onChange={(e) => {
            const f = e.target.files?.[0];
            e.target.value = '';
            if (f) void uploadSkillFile(f);
          }}
        />
        <button
          type="button"
          onClick={() => skillUploadRef.current?.click()}
          disabled={uploading}
          title="Upload your own skill — SKILL.md or a .zip bundle. Stored locally, not synced from the marketplace."
          className="text-[9px] text-[var(--text-secondary)] hover:text-[var(--text-primary)] disabled:opacity-50"
        >
          {uploading ? 'Uploading…' : '+ Upload'}
        </button>
        <button
          onClick={sync}
          disabled={syncing}
          title="Sync EVERYTHING from every source (public + each enterprise tenant): connectors + workflows + skills + wizard templates."
          className="text-[9px] text-[var(--text-secondary)] hover:text-[var(--text-primary)] disabled:opacity-50"
        >
          {syncing ? 'Syncing…' : '↻ Sync all'}
        </button>
      </div>
      {/* Search — hide on rules tab */}
      {typeFilter !== 'rules' && typeFilter !== 'plugins' && typeFilter !== 'connectors' && typeFilter !== 'crafts' && <div className="px-3 py-1.5 border-b border-[var(--border)] shrink-0">
        <input
          type="text"
          value={searchQuery}
          onChange={e => setSearchQuery(e.target.value)}
          placeholder="Search skills & commands..."
          className="w-full px-2 py-1 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-[10px] text-[var(--text-primary)] placeholder:text-[var(--text-secondary)] focus:outline-none focus:border-[var(--accent)]"
        />
      </div>}

      {typeFilter === 'rules' || typeFilter === 'plugins' || typeFilter === 'connectors' || typeFilter === 'crafts' || typeFilter === 'recipes' || typeFilter === 'pipelines' ? null : skills.length === 0 ? (
        <div className="flex-1 flex flex-col items-center justify-center gap-2 text-[var(--text-secondary)]">
          <p className="text-xs">No skills yet</p>
          <button onClick={sync} className="text-xs px-3 py-1 bg-[var(--accent)] text-white rounded hover:opacity-90">
            Sync from Registry
          </button>
        </div>
      ) : (
        <div className="flex-1 flex min-h-0">
          {/* Left: skill list */}
          <div style={{ width: sidebarWidth }} className="overflow-y-auto shrink-0">
            {/* Registry items */}
            {filtered.map(skill => {
              const isInstalled = skill.installedGlobal || skill.installedProjects.length > 0;
              const isActive = expandedSkill === skill.name;
              return (
                <div
                  key={skill.name}
                  className={`px-3 py-2.5 border-b border-[var(--border)]/50 cursor-pointer ${
                    isActive ? 'bg-[var(--accent)]/10 border-l-2 border-l-[var(--accent)]' : 'hover:bg-[var(--bg-tertiary)] border-l-2 border-l-transparent'
                  }`}
                  onClick={() => toggleDetail(skill.name)}
                >
                  <div className="flex items-center gap-2">
                    <span className="text-[11px] font-semibold text-[var(--text-primary)] truncate flex-1">{skill.displayName}</span>
                    <span className="text-[8px] text-[var(--text-secondary)] font-mono shrink-0">v{skill.version}</span>
                    {skill.rating > 0 && (
                      <span className="text-[8px] text-[var(--yellow)] shrink-0" title={`Rating: ${skill.rating}/5`}>
                        {'★'.repeat(Math.round(skill.rating))}{'☆'.repeat(5 - Math.round(skill.rating))}
                      </span>
                    )}
                    {skill.score > 0 && !skill.rating && (
                      <span className="text-[8px] text-[var(--text-secondary)] shrink-0">{skill.score}pt</span>
                    )}
                  </div>
                  <p className="text-[9px] text-[var(--text-secondary)] mt-0.5 line-clamp-1">{skill.description}</p>
                  <div className="flex items-center gap-1.5 mt-1">
                    <span className={`text-[7px] px-1 rounded font-medium ${
                      skill.type === 'skill' ? 'bg-purple-500/20 text-purple-400' : 'bg-blue-500/20 text-blue-400'
                    }`}>{skill.type === 'skill' ? 'SKILL' : 'CMD'}</span>
                    <span className="text-[8px] text-[var(--text-secondary)]">{skill.author}</span>
                    {skill.tags.slice(0, 3).map(t => (
                      <span key={t} className="text-[7px] px-1 rounded bg-[var(--bg-tertiary)] text-[var(--text-secondary)]">{t}</span>
                    ))}
                    {skill.deletedRemotely && <span className="text-[8px] text-[var(--red)] ml-auto">deleted remotely</span>}
                    {!skill.deletedRemotely && skill.hasUpdate && <span className="text-[8px] text-[var(--yellow)] ml-auto">update</span>}
                    {!skill.deletedRemotely && isInstalled && !skill.hasUpdate && <span className="text-[8px] text-[var(--green)] ml-auto">installed</span>}
                  </div>
                </div>
              );
            })}
            {/* Local items — collapsible by scope group. Visible on
                All/Local AND on Skills/Commands so uploaded items show
                up under their own type tab too. */}
            {(typeFilter === 'all' || typeFilter === 'local' || typeFilter === 'skill' || typeFilter === 'command') && filteredLocal.length > 0 && (
              <>
                {/* Local section header — collapsible */}
                {typeFilter !== 'local' && (
                  <button
                    onClick={() => toggleLocalSection('__local__')}
                    className="w-full px-3 py-1 text-[8px] text-[var(--text-secondary)] uppercase bg-[var(--bg-tertiary)] border-b border-[var(--border)]/50 flex items-center gap-1 hover:text-[var(--text-primary)]"
                  >
                    <span>{collapsedLocalSections.has('__local__') ? '▸' : '▾'}</span>
                    Local ({filteredLocal.length})
                  </button>
                )}
                {(typeFilter === 'local' || !collapsedLocalSections.has('__local__')) && (
                  <>
                    {[...localGroups.entries()].sort(([a], [b]) => a === 'global' ? -1 : b === 'global' ? 1 : a.localeCompare(b)).map(([scope, items]) => (
                      <div key={scope}>
                        {/* Scope group header — collapsible */}
                        <button
                          onClick={() => toggleLocalSection(scope)}
                          className="w-full px-3 py-1 text-[8px] text-[var(--text-secondary)] border-b border-[var(--border)]/30 flex items-center gap-1.5 hover:bg-[var(--bg-tertiary)]"
                        >
                          <span className="text-[7px]">{collapsedLocalSections.has(scope) ? '▸' : '▾'}</span>
                          <span className={scope === 'global' ? 'text-green-400' : 'text-[var(--accent)]'}>{scope}</span>
                          <span className="text-[var(--text-secondary)]">({items.length})</span>
                        </button>
                        {!collapsedLocalSections.has(scope) && items.map(item => {
                          const key = `local:${item.name}:${item.scope}`;
                          const isActive = expandedSkill === key;
                          const projectParam = item.projectPath ? encodeURIComponent(item.projectPath) : '';
                          return (
                            <div
                              key={key}
                              className={`px-3 py-2 border-b border-[var(--border)]/50 cursor-pointer ${
                                isActive ? 'bg-[var(--accent)]/10 border-l-2 border-l-[var(--accent)]' : 'hover:bg-[var(--bg-tertiary)] border-l-2 border-l-transparent'
                              }`}
                              onClick={() => {
                                if (expandedSkill === key) { setExpandedSkill(null); return; }
                                setExpandedSkill(key);
                                setSkillFiles([]);
                                setActiveFile(null);
                                setFileContent('');
                                const fetchUrl = `/api/skills/local?action=files&name=${encodeURIComponent(item.name)}&type=${item.type}${projectParam ? `&project=${projectParam}` : ''}`;
                                fetch(fetchUrl)
                                  .then(r => r.json())
                                  .then(d => {
                                    const files = (d.files || []).map((f: any) => ({ name: f.path.split('/').pop(), path: f.path, type: 'file' }));
                                    setSkillFiles(files);
                                    const first = files.find((f: any) => f.name?.endsWith('.md'));
                                    if (first) {
                                      setActiveFile(first.path);
                                      fetch(`/api/skills/local?action=read&name=${encodeURIComponent(item.name)}&type=${item.type}&path=${encodeURIComponent(first.path)}${projectParam ? `&project=${projectParam}` : ''}`)
                                        .then(r => r.json())
                                        .then(rd => setFileContent(rd.content || ''))
                                        .catch(() => {});
                                    }
                                  })
                                  .catch(() => {});
                              }}
                            >
                              <div className="flex items-center gap-2">
                                <span className={`text-[7px] px-1 rounded font-medium ${
                                  item.type === 'skill' ? 'bg-purple-500/20 text-purple-400' : 'bg-blue-500/20 text-blue-400'
                                }`}>{item.type === 'skill' ? 'S' : 'C'}</span>
                                <span className="text-[10px] text-[var(--text-primary)] truncate flex-1">{item.name}</span>
                                <span className="text-[8px] text-[var(--text-secondary)]">{item.fileCount}</span>
                              </div>
                            </div>
                          );
                        })}
                      </div>
                    ))}
                  </>
                )}
              </>
            )}
          </div>

          {/* Sidebar resize handle */}
          <div
            onMouseDown={onSidebarDragStart}
            className="w-1 bg-[var(--border)] cursor-col-resize shrink-0 hover:bg-[var(--accent)]/50 transition-colors"
          />

          {/* Right: detail panel */}
          <div className="flex-1 flex flex-col min-w-0">
            {expandedSkill ? (() => {
              const isLocal = expandedSkill.startsWith('local:');
              // Key format: "local:<name>:<scope>" — extract name (could contain colons in scope)
              const localParts = isLocal ? expandedSkill.slice(6).split(':') : [];
              const itemName = isLocal ? localParts[0] : expandedSkill;
              const localScope = isLocal ? localParts.slice(1).join(':') : '';
              const skill = isLocal ? null : skills.find(s => s.name === expandedSkill);
              const localItem = isLocal ? localItems.find(i => i.name === itemName && i.scope === localScope) : null;
              if (!skill && !localItem) return null;
              const isInstalled = skill ? (skill.installedGlobal || skill.installedProjects.length > 0) : true;
              return (
                <>
                  {/* Skill header */}
                  <div className="px-4 py-2 border-b border-[var(--border)] shrink-0">
                    <div className="flex items-center gap-2">
                      <span className="text-sm font-semibold text-[var(--text-primary)]">{skill?.displayName || localItem?.name || itemName}</span>
                      <span className={`text-[8px] px-1.5 py-0.5 rounded font-medium ${
                        (skill?.type || localItem?.type) === 'skill' ? 'bg-purple-500/20 text-purple-400' : 'bg-blue-500/20 text-blue-400'
                      }`}>{(skill?.type || localItem?.type) === 'skill' ? 'Skill' : 'Command'}</span>
                      {isLocal && <span className="text-[7px] px-1 rounded bg-green-500/10 text-green-400">local</span>}
                      {skill?.deletedRemotely && <span className="text-[7px] px-1.5 py-0.5 rounded bg-red-500/20 text-red-400 font-medium">Deleted remotely</span>}
                      {skill && !skill.deletedRemotely && <span className="text-[9px] text-[var(--text-secondary)] font-mono">v{skill.version}</span>}
                      {skill?.installedVersion && skill.installedVersion !== skill.version && (
                        <span className="text-[9px] text-[var(--yellow)] font-mono">installed: v{skill.installedVersion}</span>
                      )}
                      {skill && skill.rating > 0 && (
                        <span className="text-[9px] text-[var(--yellow)]" title={`Rating: ${skill.rating}/5`}>
                          {'★'.repeat(Math.round(skill.rating))}{'☆'.repeat(5 - Math.round(skill.rating))}
                        </span>
                      )}
                      {skill && skill.score > 0 && <span className="text-[9px] text-[var(--text-secondary)]">{skill.score}pt</span>}

                      {/* Update button */}
                      {skill?.hasUpdate && !skill.deletedRemotely && (
                        <button
                          onClick={async () => {
                            if (skill.installedGlobal) await install(skill.name, 'global');
                            for (const pp of skill.installedProjects) await install(skill.name, pp);
                          }}
                          className="text-[9px] px-2 py-1 bg-[var(--yellow)]/20 text-[var(--yellow)] border border-[var(--yellow)]/50 rounded hover:bg-[var(--yellow)]/30 transition-colors"
                        >
                          Update
                        </button>
                      )}

                      {/* Delete button for skills removed from remote registry */}
                      {skill?.deletedRemotely && (
                        <button
                          onClick={async () => {
                            if (!confirm(`"${skill.name}" was deleted from the remote repository.\n\nDelete the local installation as well?`)) return;
                            await fetch('/api/skills', {
                              method: 'POST',
                              headers: { 'Content-Type': 'application/json' },
                              body: JSON.stringify({ action: 'purge-deleted', name: skill.name }),
                            });
                            setExpandedSkill(null);
                            fetchSkills();
                          }}
                          className="text-[9px] px-2 py-1 bg-red-500/20 text-red-400 border border-red-500/40 rounded hover:bg-red-500/30 transition-colors ml-auto"
                        >
                          Delete local
                        </button>
                      )}

                      {/* Local item actions: install to other projects, delete */}
                      {isLocal && localItem && (
                        <>
                          <div className="relative ml-auto">
                            <button
                              onClick={() => setInstallTarget(prev =>
                                prev.skill === itemName && prev.show ? { skill: '', show: false } : { skill: itemName, show: true }
                              )}
                              className="text-[9px] px-2 py-1 border border-[var(--accent)] text-[var(--accent)] rounded hover:bg-[var(--accent)] hover:text-white transition-colors"
                            >
                              Install to...
                            </button>
                            {installTarget.skill === itemName && installTarget.show && (
                              <>
                                <div className="fixed inset-0 z-40" onClick={() => setInstallTarget({ skill: '', show: false })} />
                                <div className="absolute right-0 top-7 w-[200px] max-h-[60vh] overflow-y-auto bg-[var(--bg-secondary)] border border-[var(--border)] rounded-lg shadow-xl z-50 py-1">
                                  <button
                                    onClick={async () => {
                                      const res = await fetch('/api/skills/local', { method: 'POST', headers: { 'Content-Type': 'application/json' },
                                        body: JSON.stringify({ action: 'install-local', name: itemName, type: localItem.type, sourceProject: localItem.projectPath, target: 'global', force: true }) });
                                      const data = await res.json();
                                      if (!data.ok) alert(data.error);
                                      else alert(`"${itemName}" installed globally. Restart Claude to apply.`);
                                      setInstallTarget({ skill: '', show: false });
                                      fetchSkills();
                                    }}
                                    className="w-full text-left text-[10px] px-3 py-1.5 hover:bg-[var(--bg-tertiary)] text-[var(--text-primary)] sticky top-0 bg-[var(--bg-secondary)]"
                                  >Global (~/.claude)</button>
                                  <div className="border-t border-[var(--border)] my-0.5" />
                                  {projects.map(p => (
                                    <button
                                      key={p.path}
                                      onClick={async () => {
                                        const res = await fetch('/api/skills/local', { method: 'POST', headers: { 'Content-Type': 'application/json' },
                                          body: JSON.stringify({ action: 'install-local', name: itemName, type: localItem.type, sourceProject: localItem.projectPath, target: p.path, force: true }) });
                                        const data = await res.json();
                                        if (!data.ok) alert(data.error);
                                        else alert(`"${itemName}" installed to ${p.name}. Restart Claude to apply.`);
                                        setInstallTarget({ skill: '', show: false });
                                        fetchSkills();
                                      }}
                                      className="w-full text-left text-[10px] px-3 py-1.5 hover:bg-[var(--bg-tertiary)] text-[var(--text-primary)] truncate"
                                      title={p.path}
                                    >{p.name}</button>
                                  ))}
                                </div>
                              </>
                            )}
                          </div>
                          <button
                            onClick={async () => {
                              if (!confirm(`Delete "${itemName}" from ${localScope}?`)) return;
                              await fetch('/api/skills/local', { method: 'POST', headers: { 'Content-Type': 'application/json' },
                                body: JSON.stringify({ action: 'delete-local', name: itemName, type: localItem.type, project: localItem.projectPath }) });
                              setExpandedSkill(null);
                              fetchSkills();
                            }}
                            className="text-[9px] text-[var(--red)] hover:underline"
                          >Delete</button>
                        </>
                      )}

                      {/* Install dropdown — registry items only (not deleted remotely) */}
                      {skill && !skill.deletedRemotely && <div className="relative ml-auto">
                        <button
                          onClick={() => setInstallTarget(prev =>
                            prev.skill === skill.name && prev.show ? { skill: '', show: false } : { skill: skill.name, show: true }
                          )}
                          className="text-[9px] px-2 py-1 border border-[var(--accent)] text-[var(--accent)] rounded hover:bg-[var(--accent)] hover:text-white transition-colors"
                        >
                          Install
                        </button>
                        {installTarget.skill === skill.name && installTarget.show && (
                          <>
                            <div className="fixed inset-0 z-40" onClick={() => setInstallTarget({ skill: '', show: false })} />
                            <div className="absolute right-0 top-7 w-[180px] max-h-[60vh] overflow-y-auto bg-[var(--bg-secondary)] border border-[var(--border)] rounded-lg shadow-xl z-50 py-1">
                              <button
                                onClick={() => install(skill.name, 'global')}
                                className={`w-full text-left text-[10px] px-3 py-1.5 hover:bg-[var(--bg-tertiary)] sticky top-0 bg-[var(--bg-secondary)] ${
                                  skill.installedGlobal ? 'text-[var(--green)]' : 'text-[var(--text-primary)]'
                                }`}
                              >
                                {skill.installedGlobal ? '✓ ' : ''}Global (~/.claude)
                              </button>
                              <div className="border-t border-[var(--border)] my-0.5" />
                              {projects.map(p => {
                                const inst = skill.installedProjects.includes(p.path);
                                return (
                                  <button
                                    key={p.path}
                                    onClick={() => install(skill.name, p.path)}
                                    className={`w-full text-left text-[10px] px-3 py-1.5 hover:bg-[var(--bg-tertiary)] truncate ${
                                      inst ? 'text-[var(--green)]' : 'text-[var(--text-primary)]'
                                    }`}
                                    title={p.path}
                                  >
                                    {inst ? '✓ ' : ''}{p.name}
                                  </button>
                                );
                              })}
                            </div>
                          </>
                        )}
                      </div>}
                    </div>
                    <p className="text-[10px] text-[var(--text-secondary)] mt-0.5">{skill?.description || ''}</p>
                    {skill?.author && (
                      <div className="text-[9px] text-[var(--text-secondary)] mt-1">By {skill.author}</div>
                    )}
                    {skill?.tags && skill.tags.length > 0 && (
                      <div className="flex flex-wrap gap-1 mt-1">
                        {skill.tags.map(t => (
                          <span key={t} className="text-[8px] px-1.5 py-0.5 rounded bg-[var(--bg-tertiary)] text-[var(--text-secondary)]">{t}</span>
                        ))}
                      </div>
                    )}
                    {skill?.sourceUrl && (
                      <a href={skill.sourceUrl} target="_blank" rel="noopener noreferrer" className="text-[9px] text-[var(--accent)] hover:underline mt-0.5 block truncate">{skill.sourceUrl.replace(/^https?:\/\//, '').slice(0, 60)}</a>
                    )}
                    {/* Installed indicators */}
                    {skill && isInstalled && (
                      <div className="flex items-center gap-2 mt-1">
                        {skill.installedGlobal && (
                          <span className="flex items-center gap-1 text-[8px] text-[var(--green)]">
                            Global
                            <button onClick={() => { if (confirm(`Uninstall "${skill.name}" from global?`)) uninstall(skill.name, 'global'); }} className="text-[var(--text-secondary)] hover:text-[var(--red)]">x</button>
                          </span>
                        )}
                        {skill.installedProjects.map(pp => (
                          <span key={pp} className="flex items-center gap-1 text-[8px] text-[var(--accent)]">
                            {pp.split('/').pop()}
                            <button onClick={() => { if (confirm(`Uninstall "${skill.name}" from ${pp.split('/').pop()}?`)) uninstall(skill.name, pp); }} className="text-[var(--text-secondary)] hover:text-[var(--red)]">x</button>
                          </span>
                        ))}
                      </div>
                    )}
                  </div>

                  {/* File browser */}
                  <div className="flex-1 flex min-h-0 overflow-hidden">
                    {/* File tree */}
                    <div className="w-36 border-r border-[var(--border)] flex flex-col shrink-0">
                      {skillFiles.length > 0 && (
                        <div className="px-2 py-0.5 border-b border-[var(--border)] flex items-center justify-between shrink-0">
                          <span className="text-[8px] text-[var(--text-secondary)] uppercase">Files</span>
                          <button
                            onClick={() => setSkillTreeCollapseVersion(v => v + 1)}
                            className="text-[8px] text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
                            title="Collapse all"
                          >⇱</button>
                        </div>
                      )}
                      <div className="overflow-y-auto flex-1">
                      {skillFiles.length === 0 ? (
                        <div className="p-2 text-[9px] text-[var(--text-secondary)]">Loading...</div>
                      ) : (
                        <SkillFileTree
                          files={skillFiles}
                          activeFile={activeFile}
                          onSelect={(path) => loadFile(itemName, path, isLocal, localItem?.type, localItem?.projectPath)}
                          collapseVersion={skillTreeCollapseVersion}
                        />
                      )}
                      </div>
                      {skill?.sourceUrl && (
                        <div className="border-t border-[var(--border)] p-2">
                          <a
                            href={skill.sourceUrl.replace(/\/blob\/main\/.*/, '')}
                            target="_blank"
                            rel="noopener noreferrer"
                            className="text-[9px] text-[var(--accent)] hover:underline"
                          >
                            GitHub
                          </a>
                        </div>
                      )}
                    </div>
                    {/* File content */}
                    <div className="flex-1 flex flex-col" style={{ width: 0 }}>
                      {activeFile && (
                        <div className="px-3 py-1 border-b border-[var(--border)] text-[9px] text-[var(--text-secondary)] font-mono shrink-0 truncate">
                          {activeFile}
                        </div>
                      )}
                      <div className="flex-1 overflow-auto">
                        <pre className="p-3 text-[11px] text-[var(--text-primary)] font-mono whitespace-pre-wrap break-all">
                          {fileContent}
                        </pre>
                      </div>
                    </div>
                  </div>
                </>
              );
            })() : (
              <div className="flex-1 flex items-center justify-center text-[var(--text-secondary)]">
                <p className="text-xs">Select a skill to view details</p>
              </div>
            )}
          </div>
        </div>
      )}

      {/* Rules (CLAUDE.md Templates) — full-page view */}
      {typeFilter === 'rules' && (
        <div className="flex-1 flex min-h-0">
          {/* Left: template list */}
          <div className="w-56 border-r border-[var(--border)] overflow-y-auto shrink-0 flex flex-col">
            <div className="px-3 py-1.5 border-b border-[var(--border)] flex items-center justify-between">
              <span className="text-[9px] text-[var(--text-secondary)] uppercase">Rule Templates</span>
              <button
                onClick={() => { setRulesShowNew(true); setRulesEditing(true); setRulesEditId(''); setRulesEditName(''); setRulesEditDesc(''); setRulesEditContent(''); setRulesEditDefault(false); setRulesSelectedTemplate(null); }}
                className="text-[9px] text-[var(--accent)] hover:underline"
              >+ New</button>
            </div>
            <div className="flex-1 overflow-y-auto">
              {rulesTemplates.map(t => {
                const isActive = rulesSelectedTemplate === t.id;
                return (
                  <div
                    key={t.id}
                    className={`px-3 py-2 border-b border-[var(--border)]/50 cursor-pointer ${
                      isActive ? 'bg-[var(--accent)]/10 border-l-2 border-l-[var(--accent)]' : 'hover:bg-[var(--bg-tertiary)] border-l-2 border-l-transparent'
                    }`}
                    onClick={() => { setRulesSelectedTemplate(t.id); setRulesEditing(false); setRulesShowNew(false); }}
                  >
                    <div className="flex items-center gap-1.5">
                      <span className="text-[10px] text-[var(--text-primary)] truncate flex-1">{t.name}</span>
                      {t.builtin && <span className="text-[7px] text-[var(--text-secondary)]">built-in</span>}
                      <button
                        onClick={(e) => { e.stopPropagation(); toggleDefault(t.id, !t.isDefault); }}
                        className={`text-[7px] px-1 rounded ${t.isDefault ? 'bg-[var(--accent)]/20 text-[var(--accent)]' : 'bg-[var(--bg-tertiary)] text-[var(--text-secondary)] hover:text-[var(--text-primary)]'}`}
                        title={t.isDefault ? 'Default: auto-applied to new projects' : 'Click to set as default'}
                      >{t.isDefault ? 'default' : 'set default'}</button>
                    </div>
                    <p className="text-[8px] text-[var(--text-secondary)] mt-0.5 line-clamp-1">{t.description}</p>
                  </div>
                );
              })}
            </div>
          </div>

          {/* Right: template detail / editor / batch apply */}
          <div className="flex-1 flex flex-col min-w-0">
            {rulesShowNew || rulesEditing ? (
              /* Edit / New form */
              <div className="flex-1 flex flex-col overflow-hidden">
                <div className="px-4 py-2 border-b border-[var(--border)] shrink-0">
                  <div className="text-[11px] font-semibold text-[var(--text-primary)]">{rulesShowNew ? 'New Rule Template' : 'Edit Template'}</div>
                </div>
                <div className="flex-1 overflow-auto p-4 space-y-3">
                  <div className="flex gap-2">
                    <input
                      type="text"
                      value={rulesEditId}
                      onChange={e => setRulesEditId(e.target.value.replace(/[^a-z0-9-]/g, ''))}
                      placeholder="template-id (kebab-case)"
                      disabled={!rulesShowNew}
                      className="flex-1 px-2 py-1 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-[10px] text-[var(--text-primary)] font-mono disabled:opacity-50"
                    />
                    <input
                      type="text"
                      value={rulesEditName}
                      onChange={e => setRulesEditName(e.target.value)}
                      placeholder="Display Name"
                      className="flex-1 px-2 py-1 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-[10px] text-[var(--text-primary)]"
                    />
                  </div>
                  <input
                    type="text"
                    value={rulesEditDesc}
                    onChange={e => setRulesEditDesc(e.target.value)}
                    placeholder="Description"
                    className="w-full px-2 py-1 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-[10px] text-[var(--text-primary)]"
                  />
                  <textarea
                    value={rulesEditContent}
                    onChange={e => setRulesEditContent(e.target.value)}
                    placeholder="Template content (markdown)..."
                    className="w-full flex-1 min-h-[200px] p-2 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-[10px] font-mono text-[var(--text-primary)] resize-none"
                    spellCheck={false}
                  />
                  <div className="flex items-center gap-3">
                    <label className="flex items-center gap-1.5 text-[10px] text-[var(--text-secondary)] cursor-pointer">
                      <input type="checkbox" checked={rulesEditDefault} onChange={e => setRulesEditDefault(e.target.checked)} className="accent-[var(--accent)]" />
                      Auto-apply to new projects
                    </label>
                    <div className="flex gap-2 ml-auto">
                      <button onClick={saveRule} className="text-[10px] px-3 py-1 bg-[var(--accent)] text-white rounded hover:opacity-90">Save</button>
                      <button onClick={() => { setRulesEditing(false); setRulesShowNew(false); }} className="text-[10px] text-[var(--text-secondary)] hover:text-[var(--text-primary)]">Cancel</button>
                    </div>
                  </div>
                </div>
              </div>
            ) : rulesSelectedTemplate ? (() => {
              const tmpl = rulesTemplates.find(t => t.id === rulesSelectedTemplate);
              if (!tmpl) return null;
              return (
                <div className="flex-1 flex flex-col overflow-hidden">
                  {/* Template header */}
                  <div className="px-4 py-2 border-b border-[var(--border)] shrink-0">
                    <div className="flex items-center gap-2">
                      <span className="text-sm font-semibold text-[var(--text-primary)]">{tmpl.name}</span>
                      {tmpl.builtin && <span className="text-[8px] px-1 rounded bg-[var(--bg-tertiary)] text-[var(--text-secondary)]">built-in</span>}
                      <div className="ml-auto flex gap-1.5">
                        <button
                          onClick={() => { setRulesEditing(true); setRulesShowNew(false); setRulesEditId(tmpl.id); setRulesEditName(tmpl.name); setRulesEditDesc(tmpl.description); setRulesEditContent(tmpl.content); setRulesEditDefault(tmpl.isDefault); }}
                          className="text-[9px] text-[var(--accent)] hover:underline"
                        >Edit</button>
                        {!tmpl.builtin && (
                          <button onClick={() => deleteRule(tmpl.id)} className="text-[9px] text-[var(--red)] hover:underline">Delete</button>
                        )}
                      </div>
                    </div>
                    <p className="text-[9px] text-[var(--text-secondary)] mt-0.5">{tmpl.description}</p>
                  </div>

                  {/* Content + batch apply */}
                  <div className="flex-1 flex min-h-0 overflow-hidden">
                    {/* Template content */}
                    <div className="flex-1 min-w-0 overflow-auto">
                      <pre className="p-3 text-[11px] font-mono text-[var(--text-primary)] whitespace-pre-wrap break-words">
                        {tmpl.content}
                      </pre>
                    </div>

                    {/* Batch apply panel */}
                    <div className="w-48 border-l border-[var(--border)] overflow-y-auto shrink-0 flex flex-col">
                      <div className="px-2 py-1.5 border-b border-[var(--border)] text-[9px] text-[var(--text-secondary)] uppercase">Apply to Projects</div>
                      <div className="flex-1 overflow-y-auto">
                        {rulesProjects.map(p => (
                          <label key={p.path} className="flex items-center gap-1.5 px-2 py-1 hover:bg-[var(--bg-tertiary)] cursor-pointer">
                            <input
                              type="checkbox"
                              checked={rulesBatchProjects.has(p.path)}
                              onChange={() => {
                                setRulesBatchProjects(prev => {
                                  const next = new Set(prev);
                                  if (next.has(p.path)) next.delete(p.path); else next.add(p.path);
                                  return next;
                                });
                              }}
                              className="accent-[var(--accent)]"
                            />
                            <span className="text-[9px] text-[var(--text-primary)] truncate">{p.name}</span>
                          </label>
                        ))}
                      </div>
                      {rulesBatchProjects.size > 0 && (
                        <div className="p-2 border-t border-[var(--border)]">
                          <button
                            onClick={() => batchInject(tmpl.id)}
                            className="w-full text-[9px] px-2 py-1 bg-[var(--accent)] text-white rounded hover:opacity-90"
                          >
                            Apply to {rulesBatchProjects.size} project{rulesBatchProjects.size > 1 ? 's' : ''}
                          </button>
                        </div>
                      )}
                    </div>
                  </div>
                </div>
              );
            })() : (
              <div className="flex-1 flex items-center justify-center text-[var(--text-secondary)]">
                <p className="text-xs">Select a template or create a new one</p>
              </div>
            )}
          </div>
        </div>
      )}

      {/* Plugins — full-page view */}
      {typeFilter === 'plugins' && (
        <Suspense fallback={<div className="p-4 text-xs text-[var(--text-secondary)]">Loading...</div>}>
          <PluginsPanel />
        </Suspense>
      )}

      {/* Connectors — full-page view */}
      {typeFilter === 'connectors' && (
        <Suspense fallback={<div className="p-4 text-xs text-[var(--text-secondary)]">Loading...</div>}>
          <ConnectorsPanel />
        </Suspense>
      )}

      {/* Crafts — registry browse view */}
      {typeFilter === 'crafts' && (
        <Suspense fallback={<div className="p-4 text-xs text-[var(--text-secondary)]">Loading...</div>}>
          <CraftsMarketplacePanelLazy searchQuery={searchQuery} />
        </Suspense>
      )}

      {/* Recipes / Pipelines — read-only marketplace browser. Actual
          use lives in extension's Jobs tab (recipes) and Pipelines tab
          (pipelines): both show the same registry data with a "create
          local copy / instantiate" action. This page exists for
          discovery only. */}
      {(typeFilter === 'recipes' || typeFilter === 'pipelines') && (
        <WorkflowMarketplaceBrowser kind={typeFilter === 'recipes' ? 'recipe' : 'pipeline'} searchQuery={searchQuery} reloadTick={templatesSyncTick} />
      )}
    </div>
  );
}

// ─── Workflow Marketplace browser (read-only) ────────────────
// Pulls /api/workflows/marketplace and renders recipes or pipelines.
// Each row links to where you'd actually use it; no Install button here
// because the use flow (Jobs/Pipelines tab) handles install implicitly.

interface MarketRow {
  kind: 'recipe' | 'pipeline';
  name: string;
  display_name: string;
  description?: string;
  version: string;
  author?: string;
  tags?: string[];
  source: 'registry' | 'local';
  installed: boolean;
  installed_version?: string;
  has_update?: boolean;
}

function WorkflowMarketplaceBrowser({ kind, searchQuery, reloadTick = 0 }: { kind: 'recipe' | 'pipeline'; searchQuery: string; reloadTick?: number }) {
  const [rows, setRows] = useState<MarketRow[] | null>(null);
  const [err, setErr] = useState<string>('');

  const useInLocation = kind === 'recipe' ? 'Jobs tab' : 'Pipelines tab';

  const load = async () => {
    try {
      const res = await fetch('/api/workflows/marketplace');
      const data = await res.json();
      setRows((kind === 'recipe' ? data.recipes : data.pipelines) || []);
    } catch (e) {
      setErr(e instanceof Error ? e.message : String(e));
      setRows([]);
    }
  };

  useEffect(() => { void load(); }, [kind, reloadTick]);

  const q = searchQuery.toLowerCase();
  const filtered = (rows || []).filter(r => !q
    || r.name.toLowerCase().includes(q)
    || r.display_name.toLowerCase().includes(q)
    || (r.description || '').toLowerCase().includes(q)
    || (r.tags || []).some(t => t.toLowerCase().includes(q)));

  return (
    <div className="flex-1 flex flex-col min-h-0 overflow-y-auto">
      <div className="px-4 py-2 border-b border-[var(--border)] flex items-center gap-2 shrink-0">
        <span className="text-[10px] text-[var(--text-secondary)] flex-1">
          {kind === 'recipe' ? 'Job recipes' : 'Pipeline templates'} from <code className="font-mono">aiwatching/forge-workflow</code>
          {' · '}use them from the {useInLocation}
          {' · '}sync via the ↻ next to Pipelines above
        </span>
      </div>
      {err && (
        <div className="px-4 py-2 text-[10px] text-[var(--red)] border-b border-[var(--border)]">{err}</div>
      )}
      <div className="p-3 space-y-2">
        {rows == null ? (
          <div className="text-[11px] text-[var(--text-secondary)] text-center py-8">Loading…</div>
        ) : filtered.length === 0 ? (
          <div className="text-[11px] text-[var(--text-secondary)] text-center py-8">
            {rows.length === 0 ? <>Nothing in the registry yet — click <b>Sync</b>.</> : 'No matches for your search.'}
          </div>
        ) : (
          filtered.map((r) => (
            <div key={r.name} className="border border-[var(--border)] rounded p-3 bg-[var(--bg-tertiary)]/30">
              <div className="flex items-baseline gap-2">
                <span className="text-[11px] font-semibold text-[var(--text-primary)] flex-1">{r.display_name}</span>
                <span className="text-[8px] text-[var(--text-secondary)] font-mono">v{r.version}</span>
                {/* Read-only catalog — no install/uninstall here; usage
                    happens in Jobs / Pipelines tab and that's where the
                    install state matters. */}
                {r.source === 'local' && <span className="text-[8px] text-[var(--text-secondary)]" title="Only on this machine — not in the marketplace registry">· local-only</span>}
              </div>
              {r.description && (
                <p className="text-[10px] text-[var(--text-secondary)] mt-1.5 leading-relaxed whitespace-pre-wrap">{r.description}</p>
              )}
              <div className="mt-2 flex items-center gap-2">
                <code className="text-[9px] text-[var(--text-secondary)] font-mono">{r.name}</code>
                {r.author && <span className="text-[9px] text-[var(--text-secondary)]">· {r.author}</span>}
                {r.tags && r.tags.length > 0 && (
                  <span className="text-[9px] text-[var(--text-secondary)]">{r.tags.map(t => `#${t}`).join(' ')}</span>
                )}
                <span className="flex-1" />
                <span className="text-[9px] text-[var(--text-secondary)] italic">Use in {useInLocation} →</span>
              </div>
            </div>
          ))
        )}
      </div>
    </div>
  );
}
