import express from 'express';
import { createHash } from 'crypto';
import { promises as fs } from 'fs';
import path from 'path';
import { spawn } from 'child_process';
import AdmZip from 'adm-zip';
import { getUserPaths, getPublicPaths } from '../services/user-directories.js';
import { scanAgents, ensureAgentRepo, incrementPatchVersion, publishAgentToRepo } from '../services/system-agent-repo.js';
import { ensureSystemRepo, SYSTEM_REPO_URL } from '../services/system-repo.js';
import { ensureSystemMcpRepo, SYSTEM_MCP_REPO_URL } from '../services/system-mcp-repo.js';
import { addProjectManually, loadProjectConfig, saveProjectConfig } from '../projects.js';
import { agentSubmissionDb, userDb } from '../database/db.js';
import { chatCompletion } from '../services/llm.js';

const router = express.Router();

// ─── helpers ──────────────────────────────────────────────────────────────────

/**
 * Recursively list all files under a directory, returning relative paths.
 * Excludes agent.yaml (metadata file, never copied to user projects).
 */
async function listAgentFiles(dir, base = dir) {
  const entries = await fs.readdir(dir, { withFileTypes: true });
  const files = [];
  for (const entry of entries) {
    if (entry.name === 'agent.yaml') continue;
    const fullPath = path.join(dir, entry.name);
    if (entry.isDirectory()) {
      const sub = await listAgentFiles(fullPath, base);
      files.push(...sub);
    } else {
      files.push(path.relative(base, fullPath));
    }
  }
  return files;
}

function isSshUrl(url) {
  return /^[a-zA-Z0-9_-]+@[a-zA-Z0-9.-]+:.+$/.test(url);
}

function parseSshUrl(url) {
  const match = url.match(/^[a-zA-Z0-9_-]+@([a-zA-Z0-9.-]+):(.+)$/);
  if (!match) return null;
  const host = match[1].toLowerCase();
  const pathPart = match[2].replace(/\.git$/, '');
  const parts = pathPart.split('/');
  if (parts.length >= 2) return { host, owner: parts[0], repo: parts[1] };
  return { host, owner: null, repo: null };
}

function parseGitUrl(url) {
  if (isSshUrl(url)) {
    const parsed = parseSshUrl(url);
    if (parsed && parsed.owner && parsed.repo) return { owner: parsed.owner, repo: parsed.repo };
    return null;
  }
  try {
    const u = new URL(url);
    const parts = u.pathname.replace(/^\//, '').replace(/\.git$/, '').split('/');
    if (parts.length >= 2) return { owner: parts[0], repo: parts[1] };
  } catch {}
  return null;
}

function runGit(args, cwd = null) {
  return new Promise((resolve, reject) => {
    const opts = { stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, GIT_TERMINAL_PROMPT: '0' } };
    if (cwd) opts.cwd = cwd;
    const proc = spawn('git', args, opts);
    let stderr = '';
    proc.stderr.on('data', d => { stderr += d.toString(); });
    proc.on('close', code => {
      if (code === 0) resolve();
      else reject(new Error(stderr || `git ${args[0]} failed with code ${code}`));
    });
    proc.on('error', err => reject(err));
  });
}

/**
 * Recursively copy all files and directories from src to dst.
 */
async function copyDirRecursive(src, dst) {
  await fs.mkdir(dst, { recursive: true });
  const entries = await fs.readdir(src, { withFileTypes: true });
  for (const entry of entries) {
    const srcPath = path.join(src, entry.name);
    const dstPath = path.join(dst, entry.name);
    if (entry.isDirectory()) {
      await copyDirRecursive(srcPath, dstPath);
    } else {
      await fs.copyFile(srcPath, dstPath);
    }
  }
}

/**
 * Recursively add all files under dirPath into a ZIP object under the given zipPrefix.
 */
async function addDirToZip(zip, dirPath, zipPrefix) {
  const entries = await fs.readdir(dirPath, { withFileTypes: true });
  for (const entry of entries) {
    const fullPath = path.join(dirPath, entry.name);
    const zipPath = zipPrefix ? `${zipPrefix}/${entry.name}` : entry.name;
    if (entry.isDirectory()) {
      await addDirToZip(zip, fullPath, zipPath);
    } else {
      const content = await fs.readFile(fullPath);
      zip.addFile(zipPath, content);
    }
  }
}

/**
 * Parse skills list from agent.yaml content string.
 * Returns array of { name, repo }.
 */
function parseYamlSkills(yamlContent) {
  const skills = [];
  const section = yamlContent.match(/^skills:\s*\n((?:[ \t]+.+\n?)*)/m);
  if (!section) return skills;
  const lines = section[1].split('\n');
  let current = null;
  for (const line of lines) {
    const nameMatch = line.match(/^\s+-\s+name:\s*["']?(.+?)["']?\s*$/);
    if (nameMatch) {
      if (current) skills.push(current);
      current = { name: nameMatch[1].trim(), repo: '' };
    }
    const repoMatch = line.match(/^\s+repo:\s*["']?(.*?)["']?\s*$/);
    if (repoMatch && current) current.repo = repoMatch[1].trim();
  }
  if (current) skills.push(current);
  return skills;
}

/**
 * Parse MCPs list from agent.yaml content string.
 * Returns array of { name, repo }.
 */
function parseYamlMcps(yamlContent) {
  const mcps = [];
  const section = yamlContent.match(/^mcps:\s*\n((?:[ \t]+.+\n?)*)/m);
  if (!section) return mcps;
  const lines = section[1].split('\n');
  let current = null;
  for (const line of lines) {
    const nameMatch = line.match(/^\s+-\s+name:\s*["']?(.+?)["']?\s*$/);
    if (nameMatch) {
      if (current) mcps.push(current);
      current = { name: nameMatch[1].trim(), repo: '' };
    }
    const repoMatch = line.match(/^\s+repo:\s*["']?(.*?)["']?\s*$/);
    if (repoMatch && current) current.repo = repoMatch[1].trim();
  }
  if (current) mcps.push(current);
  return mcps;
}

/**
 * Parse git_repos list from agent.yaml content string.
 * Returns array of { name, repo, branch }.
 */
function parseYamlGitRepos(yamlContent) {
  const repos = [];
  const section = yamlContent.match(/^git_repos:\s*\n((?:[ \t]+.+\n?)*)/m);
  if (!section) return repos;
  const lines = section[1].split('\n');
  let current = null;
  for (const line of lines) {
    const nameMatch = line.match(/^\s+-\s+name:\s*["']?(.+?)["']?\s*$/);
    if (nameMatch) {
      if (current) repos.push(current);
      current = { name: nameMatch[1].trim(), repo: '', branch: 'main' };
    }
    const repoMatch = line.match(/^\s+repo:\s*["']?(.+?)["']?\s*$/);
    if (repoMatch && current) current.repo = repoMatch[1].trim();
    const branchMatch = line.match(/^\s+branch:\s*["']?(.+?)["']?\s*$/);
    if (branchMatch && current) current.branch = branchMatch[1].trim();
  }
  if (current) repos.push(current);
  return repos;
}

/**
 * Run a git command and return stdout (for reading git info).
 */
function runGitOutput(args, cwd) {
  return new Promise((resolve, reject) => {
    const opts = { stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, GIT_TERMINAL_PROMPT: '0' } };
    if (cwd) opts.cwd = cwd;
    const proc = spawn('git', args, opts);
    let stdout = '';
    let stderr = '';
    proc.stdout.on('data', d => { stdout += d.toString(); });
    proc.stderr.on('data', d => { stderr += d.toString(); });
    proc.on('close', code => {
      if (code === 0) resolve(stdout.trim());
      else reject(new Error(stderr || `git ${args[0]} failed with code ${code}`));
    });
    proc.on('error', err => reject(err));
  });
}

/**
 * Try to get git repo info (remote URL + branch) for a directory.
 * Returns { repo, branch } or null if not a git repo / no remote.
 */
async function getGitRepoInfo(dirPath) {
  try {
    await fs.access(path.join(dirPath, '.git'));
  } catch {
    return null;
  }
  let repoUrl;
  try {
    repoUrl = await runGitOutput(['remote', 'get-url', 'origin'], dirPath);
  } catch {
    return null; // No remote
  }
  if (!repoUrl) return null;

  let branch;
  try {
    branch = await runGitOutput(['branch', '--show-current'], dirPath);
  } catch {
    try {
      branch = await runGitOutput(['rev-parse', '--abbrev-ref', 'HEAD'], dirPath);
    } catch {
      branch = 'main';
    }
  }
  return { repo: repoUrl, branch: branch || 'main' };
}

/**
 * Scan subdirectories of a project for git repositories (up to 2 levels deep).
 * Returns array of { name, repo, branch }.
 * Two-level scan is needed for comparison projects where git repos live inside
 * sub-project folders (e.g. jigsolitaire/jigsolitaire, tile-journey-analytics/tile-genius).
 */
async function scanGitRepos(projectPath) {
  const results = [];
  const SKIP_DIRS = new Set(['node_modules', '.claude', '.git', '.vscode', '__pycache__', '.next', 'dist', 'build']);

  let entries;
  try {
    entries = await fs.readdir(projectPath, { withFileTypes: true });
  } catch {
    return results;
  }

  for (const entry of entries) {
    if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
    if (entry.name.startsWith('.') || SKIP_DIRS.has(entry.name)) continue;

    const subdirPath = path.join(projectPath, entry.name);
    const info = await getGitRepoInfo(subdirPath);

    if (info) {
      results.push({ name: entry.name, ...info });
      continue;
    }

    // Not a git repo itself — scan one level deeper (for comparison project structure)
    let subEntries;
    try {
      subEntries = await fs.readdir(subdirPath, { withFileTypes: true });
    } catch {
      continue;
    }

    for (const subEntry of subEntries) {
      if (!subEntry.isDirectory() && !subEntry.isSymbolicLink()) continue;
      if (subEntry.name.startsWith('.') || SKIP_DIRS.has(subEntry.name)) continue;

      const subSubdirPath = path.join(subdirPath, subEntry.name);
      const subInfo = await getGitRepoInfo(subSubdirPath);
      if (subInfo) {
        results.push({ name: `${entry.name}/${subEntry.name}`, ...subInfo });
      }
    }
  }

  return results;
}

/**
 * Get the shared git repo path for a given repo URL and branch.
 * Path structure: data/git-repo/{owner}/{repo}/{branch}/
 */
function getSharedGitRepoPath(repoUrl, branch) {
  const parsed = parseGitUrl(repoUrl);
  if (!parsed) return null;
  const publicPaths = getPublicPaths();
  return path.join(publicPaths.gitRepoDir, parsed.owner, parsed.repo, branch);
}

/**
 * Ensure a shared git repo exists at data/git-repo/{owner}/{repo}/{branch}/.
 * - If target exists: fetch + checkout + pull (reset on conflict)
 * - If target doesn't exist but same repo other branch exists: copy + fetch + checkout
 * - If completely new: clone + checkout
 * Returns the local path to the shared repo.
 */
async function ensureSharedGitRepo(repoUrl, branch) {
  const parsed = parseGitUrl(repoUrl);
  if (!parsed) throw new Error(`Cannot parse git URL: ${repoUrl}`);

  const publicPaths = getPublicPaths();
  const targetPath = path.join(publicPaths.gitRepoDir, parsed.owner, parsed.repo, branch);
  const repoParentDir = path.join(publicPaths.gitRepoDir, parsed.owner, parsed.repo);

  // Case 1: Target path already exists — update it
  try {
    await fs.access(targetPath);
    console.log(`[GitRepo] Updating existing shared repo at ${targetPath}`);
    try {
      await runGit(['fetch', 'origin'], targetPath);
    } catch (e) {
      console.warn(`[GitRepo] fetch failed: ${e.message}`);
    }
    await runGit(['checkout', branch], targetPath);
    try {
      await runGit(['pull', 'origin', branch], targetPath);
    } catch {
      // Pull failed (conflict) — reset and retry
      console.warn(`[GitRepo] pull conflict, resetting...`);
      await runGit(['checkout', '.'], targetPath);
      await runGit(['clean', '-fd'], targetPath);
      await runGit(['pull', 'origin', branch], targetPath);
    }
    return targetPath;
  } catch {
    // Target doesn't exist — continue to Case 2/3
  }

  // Case 2: Same repo, different branch exists — copy + fetch + checkout
  try {
    await fs.access(repoParentDir);
    const branches = await fs.readdir(repoParentDir, { withFileTypes: true });
    const existingBranch = branches.find(b => b.isDirectory());
    if (existingBranch) {
      const existingPath = path.join(repoParentDir, existingBranch.name);
      console.log(`[GitRepo] Copying from existing branch "${existingBranch.name}" to "${branch}"`);
      await fs.mkdir(targetPath, { recursive: true });
      await fs.cp(existingPath, targetPath, { recursive: true });
      try {
        await runGit(['fetch', 'origin'], targetPath);
      } catch (e) {
        console.warn(`[GitRepo] fetch after copy failed: ${e.message}`);
      }
      await runGit(['checkout', branch], targetPath);
      try {
        await runGit(['pull', 'origin', branch], targetPath);
      } catch {
        await runGit(['checkout', '.'], targetPath);
        await runGit(['clean', '-fd'], targetPath);
        await runGit(['pull', 'origin', branch], targetPath);
      }
      return targetPath;
    }
  } catch {
    // No existing branches — continue to Case 3
  }

  // Case 3: Fresh clone
  console.log(`[GitRepo] Cloning ${repoUrl} branch ${branch} to ${targetPath}`);
  await fs.mkdir(path.dirname(targetPath), { recursive: true });
  await runGit(['clone', repoUrl, targetPath]);
  await runGit(['checkout', branch], targetPath);
  return targetPath;
}

/**
 * Try to move an existing local git clone directly into the shared location,
 * avoiding a fresh network clone. Only succeeds when:
 *   - localPath is a real directory (not a symlink) with a .git folder
 *   - The shared target path does not yet exist
 * Returns the shared path on success, or null if promotion is not possible.
 */
async function tryPromoteLocalRepoToShared(localPath, repoUrl, branch) {
  const parsed = parseGitUrl(repoUrl);
  if (!parsed) return null;

  const publicPaths = getPublicPaths();
  const targetPath = path.join(publicPaths.gitRepoDir, parsed.owner, parsed.repo, branch);

  // Shared repo already exists — nothing to promote
  try {
    await fs.access(targetPath);
    return null;
  } catch {}

  // Source must be a real git directory (not a symlink, must have .git)
  try {
    const stat = await fs.lstat(localPath);
    if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
    await fs.access(path.join(localPath, '.git'));
  } catch {
    return null;
  }

  try {
    await fs.mkdir(path.dirname(targetPath), { recursive: true });
    await fs.rename(localPath, targetPath);
    console.log(`[GitRepo] Promoted local repo to shared: ${localPath} → ${targetPath}`);
    return targetPath;
  } catch (e) {
    // rename may fail on cross-device links — fall back to normal clone
    console.warn(`[GitRepo] Could not promote local repo (will clone instead): ${e.message}`);
    return null;
  }
}

/**
 * Create a symlink from a project subdirectory to a shared git repo.
 * Removes existing directory or symlink at the target location.
 */
async function linkGitRepoToProject(projectDir, subdirName, sharedRepoPath) {
  const linkPath = path.join(projectDir, subdirName);

  // Remove existing directory or symlink
  try {
    const stat = await fs.lstat(linkPath);
    if (stat.isSymbolicLink()) {
      await fs.unlink(linkPath);
    } else if (stat.isDirectory()) {
      await fs.rm(linkPath, { recursive: true, force: true });
    }
  } catch {
    // Doesn't exist — fine
  }

  await fs.symlink(sharedRepoPath, linkPath);
  console.log(`[GitRepo] Created symlink: ${linkPath} → ${sharedRepoPath}`);
}

/**
 * Ensure a skill repo is available for the user. Clone if needed.
 * Returns the local path to the skill directory inside the repo.
 */
async function ensureSkillRepo(repoUrl, userUuid) {
  const parsed = parseGitUrl(repoUrl);
  if (!parsed) throw new Error(`Cannot parse git URL: ${repoUrl}`);

  const { owner, repo } = parsed;
  const publicPaths = getPublicPaths();
  const userPaths = getUserPaths(userUuid);

  const publicRepoPath = path.join(publicPaths.skillsRepoDir, owner, repo);
  const userRepoPath = path.join(userPaths.skillsRepoDir, owner, repo);

  // Clone to public dir if not present
  try {
    await fs.access(publicRepoPath);
  } catch {
    await fs.mkdir(path.dirname(publicRepoPath), { recursive: true });
    await runGit(['clone', '--depth', '1', repoUrl, publicRepoPath]);
    console.log(`[AgentInstall] Cloned skill repo ${repoUrl}`);
  }

  // Create user symlink if not present
  try {
    await fs.lstat(userRepoPath);
  } catch {
    await fs.mkdir(path.dirname(userRepoPath), { recursive: true });
    await fs.symlink(publicRepoPath, userRepoPath);
  }

  return publicRepoPath;
}

/**
 * Install a single skill by name from a given repo URL.
 */
async function installSkill(skillName, repoUrl, userUuid, projectPath) {
  const publicRepoPath = await ensureSkillRepo(repoUrl, userUuid);
  const userPaths = getUserPaths(userUuid);

  // Search for skill directory in repo (supports 1 level nesting)
  const skillPath = await findSkillInRepo(publicRepoPath, skillName);
  if (!skillPath) {
    console.warn(`[AgentInstall] Skill "${skillName}" not found in repo`);
    return false;
  }

  const targetDir = projectPath
    ? path.join(projectPath, '.claude', 'skills')
    : userPaths.skillsDir;
  await fs.mkdir(targetDir, { recursive: true });
  const linkPath = path.join(targetDir, skillName);
  try { await fs.unlink(linkPath); } catch {}
  await fs.symlink(skillPath, linkPath);
  return true;
}

/** Get the git remote origin URL for a given local repo directory. */
async function getGitRemoteUrl(repoDir) {
  return new Promise((resolve) => {
    const proc = spawn('git', ['remote', 'get-url', 'origin'], {
      cwd: repoDir,
      stdio: ['ignore', 'pipe', 'pipe']
    });
    let out = '';
    proc.stdout.on('data', d => { out += d.toString(); });
    proc.on('close', code => resolve(code === 0 ? out.trim() : ''));
    proc.on('error', () => resolve(''));
  });
}

/**
 * Given a skill symlink path, resolve it and find the git repo root inside the
 * public skills repo dir, then return its remote URL.
 */
async function getSkillRepoUrl(skillLinkPath, publicSkillsRepoDir) {
  try {
    const realTarget = await fs.realpath(skillLinkPath);
    // Walk up from the symlink target until we find a .git directory
    // that is still inside publicSkillsRepoDir
    let dir = path.dirname(realTarget);
    while (dir.startsWith(publicSkillsRepoDir) && dir !== publicSkillsRepoDir) {
      try {
        await fs.access(path.join(dir, '.git'));
        return await getGitRemoteUrl(dir);
      } catch {
        dir = path.dirname(dir);
      }
    }
  } catch {}
  return '';
}

/**
 * Scan the public mcp repo dir to find which cloned repo contains a given
 * MCP service directory, and return that repo's remote URL.
 */
async function getMcpRepoUrl(mcpName, publicMcpRepoDir) {
  try {
    const owners = await fs.readdir(publicMcpRepoDir);
    for (const owner of owners) {
      const ownerDir = path.join(publicMcpRepoDir, owner);
      let stat;
      try { stat = await fs.stat(ownerDir); } catch { continue; }
      if (!stat.isDirectory()) continue;
      const repos = await fs.readdir(ownerDir);
      for (const repo of repos) {
        const repoDir = path.join(ownerDir, repo);
        const mcpJsonPath = path.join(repoDir, mcpName, 'mcp.json');
        try {
          await fs.access(mcpJsonPath);
          return await getGitRemoteUrl(repoDir);
        } catch {}
      }
    }
  } catch {}
  return '';
}

/**
 * Scan CLAUDE.md for referenced local files (markdown links, excluding URLs).
 * Returns array of { path: relativePath, exists: bool }.
 */
async function scanClaudeMdRefs(projectPath) {
  let content;
  try {
    content = await fs.readFile(path.join(projectPath, 'CLAUDE.md'), 'utf-8');
  } catch {
    return [];
  }

  // Match [text](path) — skip http/https/mailto/# links and anchors
  const linkRe = /\[[^\]]*\]\(([^)]+)\)/g;
  const seen = new Set();
  const results = [];
  let m;
  while ((m = linkRe.exec(content)) !== null) {
    let ref = m[1].split('#')[0].trim(); // strip fragment
    if (!ref) continue;
    if (/^https?:\/\/|^mailto:|^\/\//.test(ref)) continue; // skip absolute URLs
    if (path.isAbsolute(ref)) continue; // skip absolute paths
    if (ref.endsWith('/')) continue; // skip directory references like [src/utils/]
    if (seen.has(ref)) continue;
    seen.add(ref);

    const absPath = path.join(projectPath, ref);
    let exists = false;
    try { await fs.access(absPath); exists = true; } catch {}
    results.push({ path: ref, exists });
  }
  return results;
}

async function findSkillInRepo(repoPath, skillName) {
  // Direct match
  const direct = path.join(repoPath, skillName);
  try {
    await fs.access(path.join(direct, 'SKILL.md'));
    return direct;
  } catch {}
  // Nested under skills/
  const nested = path.join(repoPath, 'skills', skillName);
  try {
    await fs.access(path.join(nested, 'SKILL.md'));
    return nested;
  } catch {}
  return null;
}

/**
 * Ensure an MCP repo is available for the user. Clone if needed.
 */
async function ensureMcpRepo(repoUrl, userUuid) {
  const parsed = parseGitUrl(repoUrl);
  if (!parsed) throw new Error(`Cannot parse git URL: ${repoUrl}`);

  const { owner, repo } = parsed;
  const publicPaths = getPublicPaths();
  const userPaths = getUserPaths(userUuid);

  const publicRepoPath = path.join(publicPaths.mcpRepoDir, owner, repo);
  const userRepoPath = path.join(userPaths.mcpRepoDir, owner, repo);

  try {
    await fs.access(publicRepoPath);
  } catch {
    await fs.mkdir(path.dirname(publicRepoPath), { recursive: true });
    await runGit(['clone', '--depth', '1', repoUrl, publicRepoPath]);
    console.log(`[AgentInstall] Cloned MCP repo ${repoUrl}`);
  }

  try {
    await fs.lstat(userRepoPath);
  } catch {
    await fs.mkdir(path.dirname(userRepoPath), { recursive: true });
    await fs.symlink(publicRepoPath, userRepoPath);
  }

  return publicRepoPath;
}

/**
 * Install a single MCP server by service dir name from a given repo URL.
 * If projectPath is provided, installs as project-scoped (local); otherwise user-scoped.
 */
async function installMcp(mcpName, repoUrl, userUuid, projectPath) {
  const publicRepoPath = await ensureMcpRepo(repoUrl, userUuid);
  const userPaths = getUserPaths(userUuid);

  // Find service directory
  const servicePath = path.join(publicRepoPath, mcpName);
  const mcpJsonPath = path.join(servicePath, 'mcp.json');
  let mcpJson;
  try {
    mcpJson = JSON.parse(await fs.readFile(mcpJsonPath, 'utf-8'));
  } catch {
    console.warn(`[AgentInstall] mcp.json not found for "${mcpName}"`);
    return false;
  }

  // Write to user's .claude.json
  const claudeJsonPath = path.join(userPaths.claudeDir, '.claude.json');
  let claudeConfig = {};
  try {
    claudeConfig = JSON.parse(await fs.readFile(claudeJsonPath, 'utf-8'));
  } catch {
    claudeConfig = { hasCompletedOnboarding: true };
  }

  if (projectPath) {
    // Project-scoped: write to projects[projectPath].mcpServers
    if (!claudeConfig.projects) claudeConfig.projects = {};
    if (!claudeConfig.projects[projectPath]) claudeConfig.projects[projectPath] = {};
    if (!claudeConfig.projects[projectPath].mcpServers) claudeConfig.projects[projectPath].mcpServers = {};
    Object.assign(claudeConfig.projects[projectPath].mcpServers, mcpJson);
  } else {
    // User-scoped (fallback)
    claudeConfig.mcpServers = { ...(claudeConfig.mcpServers || {}), ...mcpJson };
  }

  await fs.writeFile(claudeJsonPath, JSON.stringify(claudeConfig, null, 2), 'utf-8');
  // Return the server keys that were written, so callers can track them for later removal
  return { success: true, keys: Object.keys(mcpJson) };
}

/**
 * Uninstall a single MCP server by removing its keys from .claude.json.
 * `installedKeys` are the server key names that were written during install.
 */
async function uninstallMcp(installedKeys, userUuid, projectPath) {
  if (!installedKeys?.length) return;
  const userPaths = getUserPaths(userUuid);
  const claudeJsonPath = path.join(userPaths.claudeDir, '.claude.json');
  let claudeConfig = {};
  try {
    claudeConfig = JSON.parse(await fs.readFile(claudeJsonPath, 'utf-8'));
  } catch {
    return; // Nothing to remove
  }

  let changed = false;
  if (projectPath) {
    const mcpServers = claudeConfig.projects?.[projectPath]?.mcpServers;
    if (mcpServers) {
      for (const key of installedKeys) {
        if (key in mcpServers) { delete mcpServers[key]; changed = true; }
      }
    }
  } else {
    if (claudeConfig.mcpServers) {
      for (const key of installedKeys) {
        if (key in claudeConfig.mcpServers) { delete claudeConfig.mcpServers[key]; changed = true; }
      }
    }
  }

  if (changed) {
    await fs.writeFile(claudeJsonPath, JSON.stringify(claudeConfig, null, 2), 'utf-8');
  }
}

/**
 * Uninstall a single skill by removing its symlink.
 */
async function uninstallSkill(skillName, userUuid, projectPath) {
  const userPaths = getUserPaths(userUuid);
  const linkPath = projectPath
    ? path.join(projectPath, '.claude', 'skills', skillName)
    : path.join(userPaths.skillsDir, skillName);
  try { await fs.unlink(linkPath); } catch {}
}

// ─── Routes ──────────────────────────────────────────────────────────────────

/**
 * GET /api/agents
 * List all available agents from the system agent repo
 */
router.get('/', async (req, res) => {
  try {
    const agents = await scanAgents();

    // admin/super_admin see all agents
    const userRole = req.user?.role;
    if (userRole === 'admin' || userRole === 'super_admin') {
      return res.json({ agents });
    }

    // Regular user: filter by visibility whitelist + own approved submissions
    const userId = req.user?.id;
    const visibleSet = new Set();

    // 1. Admin-configured visible agents
    const raw = userId ? userDb.getVisibleAgents(userId) : null;
    if (raw) {
      try {
        const list = JSON.parse(raw);
        if (Array.isArray(list)) {
          list.forEach(name => visibleSet.add(name));
        }
      } catch (_) { /* ignore */ }
    }

    // 2. User's own approved submissions (auto-visible)
    if (userId) {
      const approvedNames = agentSubmissionDb.getApprovedAgentNamesByUser(userId);
      approvedNames.forEach(name => visibleSet.add(name));
    }

    // If no visible agents at all, return empty list
    if (visibleSet.size === 0) {
      return res.json({ agents: [] });
    }

    const filtered = agents.filter(a => visibleSet.has(a.dirName) || visibleSet.has(a.name));
    res.json({ agents: filtered });
  } catch (error) {
    console.error('Error listing agents:', error);
    res.status(500).json({ error: 'Failed to list agents', details: error.message });
  }
});

/**
 * POST /api/agents/refresh
 * Pull the latest changes from the agent repo
 */
router.post('/refresh', async (_req, res) => {
  try {
    const agents = await scanAgents(true);
    res.json({ success: true, agentCount: agents.length });
  } catch (error) {
    console.error('Error refreshing agent repo:', error);
    res.status(500).json({ error: 'Failed to refresh agent repo', details: error.message });
  }
});

/**
 * POST /api/agents/install
 * Install an agent: create project, copy CLAUDE.md, install Skills + MCPs
 */
router.post('/install', async (req, res) => {
  try {
    const userUuid = req.user?.uuid;
    if (!userUuid) return res.status(401).json({ error: 'User authentication required' });

    const { agentName, force = false } = req.body;
    if (!agentName) return res.status(400).json({ error: 'agentName is required' });

    // Scan to find the agent
    const agents = await scanAgents();
    const agent = agents.find(a => a.name === agentName || a.dirName === agentName);
    if (!agent) return res.status(404).json({ error: `Agent "${agentName}" not found` });

    const userPaths = getUserPaths(userUuid);
    const projectDir = path.join(userPaths.projectsDir, agentName);

    // Create project directory
    await fs.mkdir(projectDir, { recursive: true });

    // Register project (idempotent)
    let project;
    try {
      project = await addProjectManually(projectDir, agent.display_name || agentName, userUuid);
    } catch (err) {
      // Project already exists — load from config
      const config = await loadProjectConfig(userUuid);
      const projectKey = projectDir.replace(/\//g, '-');
      const entry = config[projectKey] || {};
      project = {
        name: projectKey,
        path: projectDir,
        fullPath: projectDir,
        displayName: entry.displayName || agentName,
        isManuallyAdded: true,
        sessions: []
      };
    }

    // CLAUDE.md conflict check (only when updating an existing install)
    let claudeMdHash = '';
    if (agent.hasClaudeMd && !force) {
      const existingConfig = await loadProjectConfig(userUuid);
      const existingKey = projectDir.replace(/\//g, '-');
      const storedHash = existingConfig[existingKey]?.agentInfo?.claudeMdHash;
      if (storedHash) {
        try {
          const localContent = await fs.readFile(path.join(projectDir, 'CLAUDE.md'), 'utf-8');
          const localHash = createHash('sha256').update(localContent).digest('hex');
          if (localHash !== storedHash) {
            let repoContent = '';
            try { repoContent = await fs.readFile(path.join(agent.path, 'CLAUDE.md'), 'utf-8'); } catch {}
            return res.status(409).json({
              conflict: true,
              error: '本地 CLAUDE.md 已被修改，更新将覆盖本地改动',
              localContent,
              repoContent
            });
          }
        } catch {}
      }
    }

    // Copy all agent files to project directory (excluding agent.yaml metadata)
    // This includes CLAUDE.md and any referenced files in subdirectories
    let installedFiles = [];
    try {
      const agentEntries = await fs.readdir(agent.path, { withFileTypes: true });
      for (const entry of agentEntries) {
        if (entry.name === 'agent.yaml') continue;
        const src = path.join(agent.path, entry.name);
        const dst = path.join(projectDir, entry.name);
        if (entry.isDirectory()) {
          await fs.cp(src, dst, { recursive: true, force: true });
        } else {
          await fs.copyFile(src, dst);
          if (entry.name === 'CLAUDE.md') {
            try {
              const content = await fs.readFile(dst, 'utf-8');
              claudeMdHash = createHash('sha256').update(content).digest('hex');
            } catch {}
          }
        }
      }
      installedFiles = await listAgentFiles(agent.path);
    } catch (err) {
      console.error('[AgentInstall] Failed to copy agent files:', err.message);
    }

    // Load existing agentInfo to perform diff-based uninstall of removed skills/MCPs
    const prevConfig = await loadProjectConfig(userUuid);
    const prevKey = projectDir.replace(/\//g, '-');
    const prevAgentInfo = prevConfig[prevKey]?.agentInfo;

    // Remove skills that existed in old version but are no longer in new version
    const newSkillNames = new Set((agent.skills || []).filter(s => s.name).map(s => s.name));
    if (prevAgentInfo?.installedSkills?.length > 0) {
      // Precise diff using recorded data
      for (const old of prevAgentInfo.installedSkills) {
        if (!newSkillNames.has(old.name)) {
          try {
            await uninstallSkill(old.name, userUuid, projectDir);
            console.log(`[AgentInstall] Removed old skill "${old.name}" (no longer in agent)`);
          } catch (e) {
            console.warn(`[AgentInstall] Could not remove old skill "${old.name}":`, e.message);
          }
        }
      }
    } else if (prevAgentInfo) {
      // No recorded data — fallback: scan actual skills dir and remove anything not in new agent
      try {
        const skillsDir = path.join(projectDir, '.claude', 'skills');
        const entries = await fs.readdir(skillsDir).catch(() => []);
        for (const name of entries) {
          if (!newSkillNames.has(name)) {
            try {
              await fs.unlink(path.join(skillsDir, name));
              console.log(`[AgentInstall] Removed residual skill "${name}" (fallback cleanup)`);
            } catch (e) {
              console.warn(`[AgentInstall] Could not remove residual skill "${name}":`, e.message);
            }
          }
        }
      } catch {}
    }

    // Remove MCPs that existed in old version but are no longer in new version
    const newMcpNames = new Set((agent.mcps || []).filter(m => m.name).map(m => m.name));
    if (prevAgentInfo?.installedMcps?.length > 0) {
      // Precise diff using recorded data (includes exact server keys)
      for (const old of prevAgentInfo.installedMcps) {
        if (!newMcpNames.has(old.name)) {
          try {
            await uninstallMcp(old.keys, userUuid, projectDir);
            console.log(`[AgentInstall] Removed old MCP "${old.name}" (no longer in agent)`);
          } catch (e) {
            console.warn(`[AgentInstall] Could not remove old MCP "${old.name}":`, e.message);
          }
        }
      }
    } else if (prevAgentInfo) {
      // No recorded data — fallback: remove ALL project-scoped MCP keys (will be reinstalled)
      try {
        const userPaths = getUserPaths(userUuid);
        const claudeJsonPath = path.join(userPaths.claudeDir, '.claude.json');
        const claudeConfig = JSON.parse(await fs.readFile(claudeJsonPath, 'utf-8'));
        const projectMcpKeys = Object.keys(claudeConfig.projects?.[projectDir]?.mcpServers || {});
        if (projectMcpKeys.length > 0) {
          await uninstallMcp(projectMcpKeys, userUuid, projectDir);
          console.log(`[AgentInstall] Removed ${projectMcpKeys.length} residual project MCPs (fallback cleanup)`);
        }
      } catch {}
    }

    // Install skills
    const skillResults = [];
    const installedSkills = [];
    for (const skill of agent.skills || []) {
      if (!skill.name || !skill.repo) continue;
      try {
        const ok = await installSkill(skill.name, skill.repo, userUuid, projectDir);
        skillResults.push({ name: skill.name, success: ok });
        if (ok) installedSkills.push({ name: skill.name, repo: skill.repo });
      } catch (err) {
        console.error(`[AgentInstall] Failed to install skill "${skill.name}":`, err.message);
        skillResults.push({ name: skill.name, success: false, error: err.message });
      }
    }

    // Install MCPs (project-scoped to the newly created project directory)
    const mcpResults = [];
    const installedMcps = [];
    for (const mcp of agent.mcps || []) {
      if (!mcp.name || !mcp.repo) continue;
      try {
        const result = await installMcp(mcp.name, mcp.repo, userUuid, projectDir);
        mcpResults.push({ name: mcp.name, success: result.success });
        if (result.success) installedMcps.push({ name: mcp.name, repo: mcp.repo, keys: result.keys });
      } catch (err) {
        console.error(`[AgentInstall] Failed to install MCP "${mcp.name}":`, err.message);
        mcpResults.push({ name: mcp.name, success: false, error: err.message });
      }
    }

    // Install git repo dependencies (clone/update shared repo + create symlink)
    const gitRepoResults = [];
    for (const gitRepo of agent.git_repos || []) {
      if (!gitRepo.name || !gitRepo.repo) continue;
      try {
        const sharedPath = await ensureSharedGitRepo(gitRepo.repo, gitRepo.branch || 'main');
        await linkGitRepoToProject(projectDir, gitRepo.name, sharedPath);
        gitRepoResults.push({ name: gitRepo.name, success: true });
      } catch (err) {
        console.error(`[AgentInstall] Failed to install git repo "${gitRepo.name}":`, err.message);
        gitRepoResults.push({ name: gitRepo.name, success: false, error: err.message });
      }
    }

    // Mark project as agent in project-config.json
    const config = await loadProjectConfig(userUuid);
    const projectKey = projectDir.replace(/\//g, '-');
    config[projectKey] = {
      ...(config[projectKey] || {}),
      agentInfo: {
        agentName: agent.dirName || agentName,
        installedVersion: agent.version,
        installedAt: new Date().toISOString(),
        isAgent: true,
        claudeMdHash,
        installedFiles,
        installedSkills,
        installedMcps,
        gitRepos: (agent.git_repos || []).filter(g => g.name && g.repo).map(g => ({
          name: g.name, repo: g.repo, branch: g.branch || 'main'
        }))
      }
    };
    await saveProjectConfig(config, userUuid);

    res.json({
      success: true,
      project: { ...project, agentInfo: config[projectKey].agentInfo },
      skills: skillResults,
      mcps: mcpResults,
      gitRepos: gitRepoResults
    });
  } catch (error) {
    console.error('Error installing agent:', error);
    res.status(500).json({ error: 'Failed to install agent', details: error.message });
  }
});

/**
 * GET /api/agents/preview
 * Preview what files/skills/MCPs a project would include if submitted as an Agent
 */
router.get('/preview', async (req, res) => {
  try {
    const userUuid = req.user?.uuid;
    if (!userUuid) return res.status(401).json({ error: 'User authentication required' });

    const { projectKey } = req.query;
    if (!projectKey) return res.status(400).json({ error: 'projectKey is required' });

    const config = await loadProjectConfig(userUuid);
    const entry = config[projectKey];
    if (!entry) return res.status(404).json({ error: 'Project not found' });

    const projectPath = entry.originalPath || projectKey.replace(/-/g, '/');
    const userPaths = getUserPaths(userUuid);

    // Check CLAUDE.md
    let hasClaudeMd = false;
    try { await fs.access(path.join(projectPath, 'CLAUDE.md')); hasClaudeMd = true; } catch {}

    // Get installed skills with repo URLs
    const skills = [];
    const publicPaths = getPublicPaths();
    try {
      const skillNames = await fs.readdir(userPaths.skillsDir);
      for (const name of skillNames) {
        if (name.startsWith('.')) continue;
        const linkPath = path.join(userPaths.skillsDir, name);
        const repo = await getSkillRepoUrl(linkPath, publicPaths.skillsRepoDir);
        let localPath = '';
        if (!repo) {
          // Skill not from a git repo — check if it's a user-imported skill
          try {
            const realPath = await fs.realpath(linkPath);
            if (realPath.includes('/skills-import/')) localPath = realPath;
          } catch {}
        }
        skills.push({ name, repo, localPath });
      }
    } catch {}

    // Get configured MCPs from .claude.json with repo URLs
    const mcps = [];
    try {
      const claudeJsonPath = path.join(userPaths.claudeDir, '.claude.json');
      const claudeConfig = JSON.parse(await fs.readFile(claudeJsonPath, 'utf-8'));

      // User-scoped MCPs (top-level mcpServers)
      for (const name of Object.keys(claudeConfig.mcpServers || {})) {
        const repo = await getMcpRepoUrl(name, publicPaths.mcpRepoDir);
        const config = !repo ? (claudeConfig.mcpServers[name] || null) : null;
        mcps.push({ name, repo, config, scope: 'user', mcpProjectPath: null });
      }

      // Project-scoped MCPs (projects[projectPath].mcpServers)
      const projectMcpServers = claudeConfig.projects?.[projectPath]?.mcpServers || {};
      for (const name of Object.keys(projectMcpServers)) {
        // Skip if already listed as user-scoped
        if (mcps.some(m => m.name === name)) continue;
        const repo = await getMcpRepoUrl(name, publicPaths.mcpRepoDir);
        const config = !repo ? (projectMcpServers[name] || null) : null;
        mcps.push({ name, repo, config, scope: 'local', mcpProjectPath: projectPath });
      }
    } catch {}

    // Scan CLAUDE.md for referenced local files
    const refFiles = await scanClaudeMdRefs(projectPath);

    // Scan subdirectories for git repositories
    const gitRepos = await scanGitRepos(projectPath);

    res.json({ hasClaudeMd, skills, mcps, refFiles, gitRepos });
  } catch (error) {
    console.error('Error getting agent preview:', error);
    res.status(500).json({ error: 'Failed to get preview', details: error.message });
  }
});

/**
 * GET /api/agents/project-files
 * List files in a project directory for manual file selection when submitting an Agent.
 */
const AGENT_SKIP_DIRS = new Set([
  'node_modules', 'dist', 'build', '.git', '.svn', '.hg',
  '__pycache__', '.pytest_cache', '.venv', 'venv',
  '.next', '.nuxt', 'coverage', '.cache',
  '.claude', '.codex', '.gemini', 'data'
]);
const MAX_PROJECT_FILES = 500;
const MAX_DEPTH = 5;

router.get('/project-files', async (req, res) => {
  try {
    const userUuid = req.user?.uuid;
    if (!userUuid) return res.status(401).json({ error: 'User authentication required' });

    const { projectKey } = req.query;
    if (!projectKey) return res.status(400).json({ error: 'projectKey is required' });

    const config = await loadProjectConfig(userUuid);
    const entry = config[projectKey];
    if (!entry) return res.status(404).json({ error: 'Project not found' });
    const projectPath = entry.originalPath || projectKey.replace(/-/g, '/');

    const files = [];
    let truncated = false;

    async function walk(dir, depth) {
      if (depth > MAX_DEPTH || truncated) return;
      let entries;
      try {
        entries = await fs.readdir(dir, { withFileTypes: true });
      } catch { return; }

      for (const entry of entries) {
        if (truncated) return;
        if (entry.isSymbolicLink()) continue;
        if (entry.name.startsWith('.')) continue;

        const fullPath = path.join(dir, entry.name);

        if (entry.isDirectory()) {
          if (AGENT_SKIP_DIRS.has(entry.name)) continue;
          // Skip directories that are git repositories (handled as git_repos dependencies)
          try {
            await fs.access(path.join(fullPath, '.git'));
            continue; // Has .git → skip entire subtree
          } catch {}
          await walk(fullPath, depth + 1);
        } else if (entry.isFile()) {
          if (entry.name === 'CLAUDE.md') continue;
          const relPath = path.relative(projectPath, fullPath);
          let size = 0;
          try { size = (await fs.stat(fullPath)).size; } catch {}
          files.push({ path: relPath, size });
          if (files.length >= MAX_PROJECT_FILES) {
            truncated = true;
            return;
          }
        }
      }
    }

    await walk(projectPath, 0);
    files.sort((a, b) => a.path.localeCompare(b.path));

    res.json({ files, truncated });
  } catch (error) {
    console.error('Error listing project files:', error);
    res.status(500).json({ error: 'Failed to list project files', details: error.message });
  }
});

/**
 * GET /api/agents/check-name/:name
 * Check if an agent name already exists in the repo, and if there's a pending conflict.
 * Returns existing agent config for pre-fill and pending conflict info.
 */
router.get('/check-name/:name', async (req, res) => {
  try {
    const { name } = req.params;
    const userId = req.user?.id;
    if (!name) return res.status(400).json({ error: 'name is required' });

    // 1. Check if agent exists in the repo
    const agents = await scanAgents();
    const existing = agents.find(a => a.dirName === name || a.name === name);

    // 2. Check for pending submissions with the same name from other users
    const pendingConflict = agentSubmissionDb.listAll('pending')
      .find(s => s.agent_name === name && s.user_id !== userId);

    res.json({
      existing: existing ? {
        display_name: existing.display_name,
        description: existing.description,
        version: existing.version
      } : null,
      pendingConflict: pendingConflict ? {
        username: pendingConflict.username || pendingConflict.email,
        submitted_at: pendingConflict.submitted_at
      } : null
    });
  } catch (error) {
    console.error('Error checking agent name:', error);
    res.status(500).json({ error: 'Failed to check agent name', details: error.message });
  }
});

/**
 * POST /api/agents/generate-description
 * Use LLM to generate an Agent description based on CLAUDE.md + selected skills/MCPs.
 */
router.post('/generate-description', async (req, res) => {
  try {
    const userUuid = req.user?.uuid;
    if (!userUuid) return res.status(401).json({ error: 'User authentication required' });

    const { projectKey, skills = [], mcps = [] } = req.body;
    if (!projectKey) return res.status(400).json({ error: 'projectKey is required' });

    const config = await loadProjectConfig(userUuid);
    const entry = config[projectKey];
    if (!entry) return res.status(404).json({ error: 'Project not found' });

    const projectPath = entry.originalPath || projectKey.replace(/-/g, '/');

    // Read CLAUDE.md if available
    let claudeMdContent = '';
    try {
      claudeMdContent = await fs.readFile(path.join(projectPath, 'CLAUDE.md'), 'utf-8');
    } catch { /* optional */ }

    // Build context for LLM
    const skillList = skills.length > 0 ? skills.map(s => `- ${s}`).join('\n') : '(无)';
    const mcpList = mcps.length > 0 ? mcps.map(m => `- ${m}`).join('\n') : '(无)';

    const systemPrompt = `你是一个技术文档专家，专门为 共享项目 编写简洁、清晰的功能描述。
描述应该：
- 简明扼要，2-4 句话
- 说明项目的主要用途和能力
- 自然流畅，不使用模板化套话
- 使用中文
只输出描述内容，不要有前缀或标题。`;

    const userPrompt = `请根据以下信息，为这个项目生成一段功能描述：

${claudeMdContent ? `## CLAUDE.md 内容\n${claudeMdContent.slice(0, 3000)}\n` : '## CLAUDE.md\n(未找到)\n'}
## 已集成的 Skills
${skillList}

## 已集成的 MCP 服务
${mcpList}`;

    const description = await chatCompletion({
      systemPrompt,
      userPrompt,
      model: process.env.LLM_DEFAULT_MODEL || 'gemini-3.1-flash-lite-preview',
      maxTokens: 300,
      temperature: 0.7
    });

    res.json({ description });
  } catch (error) {
    console.error('Error generating description:', error);
    res.status(500).json({ error: error.message || 'Failed to generate description' });
  }
});

/**
 * POST /api/agents/submit
 * Submit a project as an agent for review.
 * Builds the ZIP server-side from the project's CLAUDE.md + agent.yaml (provided in body).
 */
router.post('/submit', async (req, res) => {
  try {
    const userUuid = req.user?.uuid;
    const userId = req.user?.id;
    if (!userUuid || !userId) return res.status(401).json({ error: 'User authentication required' });

    const { agentName, displayName, description, projectKey, agentYaml, refFiles = [], updateNotes } = req.body;
    if (!agentName) return res.status(400).json({ error: 'agentName is required' });
    if (!displayName) return res.status(400).json({ error: 'displayName is required' });
    if (!projectKey) return res.status(400).json({ error: 'projectKey is required' });
    if (!agentYaml) return res.status(400).json({ error: 'agentYaml is required' });
    if (!/^[a-zA-Z0-9_-]{1,100}$/.test(agentName)) {
      return res.status(400).json({ error: 'agentName must be 1-100 alphanumeric, hyphen, or underscore characters' });
    }

    // Resolve project path from config
    const config = await loadProjectConfig(userUuid);
    const entry = config[projectKey];
    if (!entry) return res.status(404).json({ error: 'Project not found' });
    const projectPath = entry.originalPath || projectKey.replace(/-/g, '/');

    // Build ZIP: agent.yaml + CLAUDE.md (if exists)
    const zip = new AdmZip();
    zip.addFile('agent.yaml', Buffer.from(agentYaml, 'utf-8'));

    try {
      const claudeMdContent = await fs.readFile(path.join(projectPath, 'CLAUDE.md'), 'utf-8');
      zip.addFile('CLAUDE.md', Buffer.from(claudeMdContent, 'utf-8'));
    } catch {
      // CLAUDE.md is optional - add a placeholder
      zip.addFile('CLAUDE.md', Buffer.from('# Agent\n', 'utf-8'));
    }

    // Include selected referenced files, preserving relative paths
    for (const refPath of refFiles) {
      if (!refPath || path.isAbsolute(refPath)) continue;
      const absRef = path.resolve(projectPath, refPath);
      // Security: ensure the resolved path stays within the project directory
      if (!absRef.startsWith(path.resolve(projectPath) + path.sep)) continue;
      try {
        const content = await fs.readFile(absRef);
        zip.addFile(refPath, content);
      } catch {
        // Skip files that can't be read
      }
    }

    // Include local skill files for imported skills (repo is absolute path)
    const parsedSkills = parseYamlSkills(agentYaml);
    for (const skill of parsedSkills) {
      if (!skill.repo.startsWith('/')) continue; // Only handle absolute local paths
      try {
        await addDirToZip(zip, skill.repo, `_skill_files/${skill.name}`);
        console.log(`[AgentSubmit] Included local skill files for "${skill.name}" from ${skill.repo}`);
      } catch (e) {
        console.warn(`[AgentSubmit] Could not include skill files for "${skill.name}": ${e.message}`);
      }
    }

    // Capture user-configured MCP configs (repo is empty)
    const parsedMcps = parseYamlMcps(agentYaml);
    if (parsedMcps.some(m => !m.repo)) {
      try {
        const userPaths = getUserPaths(userUuid);
        const claudeJsonPath = path.join(userPaths.claudeDir, '.claude.json');
        const claudeConfig = JSON.parse(await fs.readFile(claudeJsonPath, 'utf-8'));
        const projectMcpServers = claudeConfig.projects?.[projectPath]?.mcpServers || {};
        for (const mcp of parsedMcps) {
          if (mcp.repo) continue; // Already has a repo URL
          // Check user-scoped first, then project-scoped
          const serverConfig = claudeConfig.mcpServers?.[mcp.name] ?? projectMcpServers[mcp.name];
          if (serverConfig) {
            const mcpConfig = { [mcp.name]: serverConfig };
            zip.addFile(`_mcp_configs/${mcp.name}/mcp.json`, Buffer.from(JSON.stringify(mcpConfig, null, 2)));
            console.log(`[AgentSubmit] Captured user-configured MCP config for "${mcp.name}"`);
          }
        }
      } catch (e) {
        console.warn(`[AgentSubmit] Could not capture MCP configs: ${e.message}`);
      }
    }

    // Save ZIP to disk
    const publicPaths = getPublicPaths();
    const userSubmitDir = path.join(publicPaths.agentSubmissionsDir, userUuid);
    await fs.mkdir(userSubmitDir, { recursive: true });

    const timestamp = Date.now();
    const zipPath = path.join(userSubmitDir, `${agentName}-${timestamp}.zip`);
    await fs.writeFile(zipPath, zip.toBuffer());

    // Save to DB
    const submissionId = agentSubmissionDb.create({
      userId,
      agentName,
      displayName,
      description: description || '',
      zipPath,
      updateNotes: updateNotes || null
    });

    res.json({
      success: true,
      submissionId,
      message: 'Agent 已提交审核，管理员审核通过后将发布到仓库。'
    });
  } catch (error) {
    console.error('Error submitting agent:', error);
    res.status(500).json({ error: 'Failed to submit agent', details: error.message });
  }
});

/**
 * GET /api/agents/submissions/my
 * Get current user's submissions
 */
router.get('/submissions/my', async (req, res) => {
  try {
    const userId = req.user?.id;
    if (!userId) return res.status(401).json({ error: 'User authentication required' });

    const submissions = agentSubmissionDb.listByUser(userId);
    res.json({ submissions });
  } catch (error) {
    console.error('Error fetching submissions:', error);
    res.status(500).json({ error: 'Failed to fetch submissions', details: error.message });
  }
});

/**
 * GET /api/agents/submissions
 * List all submissions (admin only)
 */
router.get('/submissions', async (req, res) => {
  try {
    const userRole = req.user?.role;
    if (userRole !== 'admin' && userRole !== 'super_admin') {
      return res.status(403).json({ error: 'Admin access required' });
    }

    const { status } = req.query;
    const submissions = agentSubmissionDb.listAll(status || null);
    res.json({ submissions });
  } catch (error) {
    console.error('Error fetching submissions:', error);
    res.status(500).json({ error: 'Failed to fetch submissions', details: error.message });
  }
});

/**
 * GET /api/agents/submissions/:id/contents
 * Return the text content of files inside the submission ZIP (admin only)
 */
router.get('/submissions/:id/contents', async (req, res) => {
  try {
    const userRole = req.user?.role;
    if (userRole !== 'admin' && userRole !== 'super_admin') {
      return res.status(403).json({ error: 'Admin access required' });
    }

    const submission = agentSubmissionDb.getById(req.params.id);
    if (!submission) return res.status(404).json({ error: 'Submission not found' });

    let zipBuffer;
    try {
      zipBuffer = await fs.readFile(submission.zip_path);
    } catch {
      return res.status(404).json({ error: 'Submission ZIP file not found' });
    }

    const zip = new AdmZip(zipBuffer);
    const files = [];
    for (const entry of zip.getEntries()) {
      if (entry.isDirectory) continue;
      const name = entry.entryName;
      const content = entry.getData().toString('utf-8');
      files.push({ name, content });
    }
    // Sort: agent.yaml first, CLAUDE.md second, rest alphabetically
    files.sort((a, b) => {
      const order = { 'agent.yaml': 0, 'CLAUDE.md': 1 };
      const oa = order[a.name] ?? 99;
      const ob = order[b.name] ?? 99;
      if (oa !== ob) return oa - ob;
      return a.name.localeCompare(b.name);
    });

    res.json({ files });
  } catch (error) {
    console.error('Error reading submission contents:', error);
    res.status(500).json({ error: 'Failed to read submission contents', details: error.message });
  }
});

/**
 * POST /api/agents/submissions/:id/approve
 * Approve a submission and publish to git repo (admin only)
 */
router.post('/submissions/:id/approve', async (req, res) => {
  try {
    const userRole = req.user?.role;
    const reviewerId = req.user?.id;
    if (userRole !== 'admin' && userRole !== 'super_admin') {
      return res.status(403).json({ error: 'Admin access required' });
    }

    const { updateAllUsers = false } = req.body;

    const submission = agentSubmissionDb.getById(req.params.id);
    if (!submission) return res.status(404).json({ error: 'Submission not found' });
    if (submission.status !== 'pending') {
      return res.status(400).json({ error: 'Submission is not pending' });
    }

    // Read ZIP and extract
    let zipBuffer;
    try {
      zipBuffer = await fs.readFile(submission.zip_path);
    } catch {
      return res.status(500).json({ error: 'Could not read submission ZIP' });
    }

    const zip = new AdmZip(zipBuffer);
    const publicPaths = getPublicPaths();
    const extractDir = path.join(publicPaths.agentSubmissionsDir, 'extracted', String(submission.id));
    await fs.mkdir(extractDir, { recursive: true });
    zip.extractAllTo(extractDir, true);

    // Determine new version
    const agents = await scanAgents();
    const existing = agents.find(a => a.dirName === submission.agent_name || a.name === submission.agent_name);
    const newVersion = existing ? incrementPatchVersion(existing.version) : '1.0.0';

    // Get submitter name for commit message
    const submitter = submission.username || submission.email || `user-${submission.user_id}`;

    // Get submitter's UUID for skill/MCP operations
    const allUsers = userDb.getAllUsers();
    const submitterUser = allUsers.find(u => u.id === submission.user_id);
    const submitterUuid = submitterUser?.uuid;

    // Migrate local skills and user-configured MCPs before publishing
    const agentYamlPath = path.join(extractDir, 'agent.yaml');
    let agentYamlContent;
    try {
      agentYamlContent = await fs.readFile(agentYamlPath, 'utf-8');
    } catch {
      agentYamlContent = null;
    }

    if (agentYamlContent && submitterUuid) {
      // === A. Migrate local skills (repo is absolute path starting with "/") ===
      const yamlSkills = parseYamlSkills(agentYamlContent);
      const localSkills = yamlSkills.filter(s => s.repo.startsWith('/'));

      if (localSkills.length > 0) {
        let skillRepoPath;
        try {
          skillRepoPath = await ensureSystemRepo(); // git pull or clone
        } catch (e) {
          console.error('[AgentApprove] Failed to ensure system skill repo:', e.message);
        }

        if (skillRepoPath) {
          for (const skill of localSkills) {
            try {
              // Copy skill folder from extracted ZIP to system skill repo
              const srcSkillDir = path.join(extractDir, '_skill_files', skill.name);
              const destSkillDir = path.join(skillRepoPath, skill.name);

              // Verify source exists in ZIP
              try {
                await fs.access(srcSkillDir);
              } catch {
                console.warn(`[AgentApprove] Skill files for "${skill.name}" not found in submission ZIP, skipping`);
                continue;
              }

              // Remove old version in repo if it exists
              await fs.rm(destSkillDir, { recursive: true, force: true });
              await copyDirRecursive(srcSkillDir, destSkillDir);
              console.log(`[AgentApprove] Copied skill "${skill.name}" to system skill repo`);

              // Git: add, commit, push
              await runGit(['add', skill.name], skillRepoPath);
              await runGit(
                ['commit', '-m', `feat: add skill ${skill.name} from approved agent submission #${submission.id}`],
                skillRepoPath
              );
              await runGit(['push'], skillRepoPath);
              console.log(`[AgentApprove] Pushed skill "${skill.name}" to remote`);

              // Clean up submitter's skills-import directory for this skill
              const userPaths = getUserPaths(submitterUuid);
              try {
                await fs.rm(path.join(userPaths.skillsImportDir, skill.name), { recursive: true, force: true });
              } catch {}

              // Re-install skill for submitter from the system repo
              await installSkill(skill.name, SYSTEM_REPO_URL, submitterUuid);
              console.log(`[AgentApprove] Re-installed skill "${skill.name}" for submitter from system repo`);

              // Update agent.yaml: replace absolute path with system repo git URL
              const escapedName = skill.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
              agentYamlContent = agentYamlContent.replace(
                new RegExp(`(- name: "${escapedName}"\\s*\\n\\s+repo: )"[^"]*"`, 'gm'),
                `$1"${SYSTEM_REPO_URL}"`
              );
            } catch (e) {
              console.error(`[AgentApprove] Failed to migrate skill "${skill.name}":`, e.message);
              // Continue with other skills even if one fails
            }
          }
        }
      }

      // === B. Migrate user-configured MCPs (repo is empty, config in _mcp_configs/) ===
      const yamlMcps = parseYamlMcps(agentYamlContent);
      const localMcps = yamlMcps.filter(m => !m.repo);

      if (localMcps.length > 0) {
        let mcpRepoPath;
        try {
          mcpRepoPath = await ensureSystemMcpRepo(); // git pull or clone
        } catch (e) {
          console.error('[AgentApprove] Failed to ensure system MCP repo:', e.message);
        }

        if (mcpRepoPath) {
          for (const mcp of localMcps) {
            try {
              // Read captured MCP config from ZIP extraction
              const mcpConfigPath = path.join(extractDir, '_mcp_configs', mcp.name, 'mcp.json');
              let mcpConfig;
              try {
                mcpConfig = JSON.parse(await fs.readFile(mcpConfigPath, 'utf-8'));
              } catch {
                console.warn(`[AgentApprove] No captured config for MCP "${mcp.name}", skipping`);
                continue;
              }

              // Create MCP folder in system MCP repo
              const mcpFolderPath = path.join(mcpRepoPath, mcp.name);
              await fs.mkdir(mcpFolderPath, { recursive: true });

              // Write mcp.json
              await fs.writeFile(path.join(mcpFolderPath, 'mcp.json'), JSON.stringify(mcpConfig, null, 2));

              // Write mcp.yaml with basic metadata
              const mcpYaml = `name: "${mcp.name}"\ndescription: "MCP service migrated from shared project '${submission.display_name}' (submission #${submission.id})"\n`;
              await fs.writeFile(path.join(mcpFolderPath, 'mcp.yaml'), mcpYaml);
              console.log(`[AgentApprove] Created MCP "${mcp.name}" in system MCP repo`);

              // Git: add, commit, push
              await runGit(['add', mcp.name], mcpRepoPath);
              await runGit(
                ['commit', '-m', `feat: add mcp ${mcp.name} from approved agent submission #${submission.id}`],
                mcpRepoPath
              );
              await runGit(['push'], mcpRepoPath);
              console.log(`[AgentApprove] Pushed MCP "${mcp.name}" to remote`);

              // Remove MCP from submitter's .claude.json (user-scoped or project-scoped)
              const userPaths = getUserPaths(submitterUuid);
              const claudeJsonPath = path.join(userPaths.claudeDir, '.claude.json');
              try {
                const claudeConfig = JSON.parse(await fs.readFile(claudeJsonPath, 'utf-8'));
                let changed = false;
                // Remove from user-scoped
                if (claudeConfig.mcpServers) {
                  for (const serverKey of Object.keys(mcpConfig)) {
                    if (serverKey in claudeConfig.mcpServers) {
                      delete claudeConfig.mcpServers[serverKey];
                      changed = true;
                    }
                  }
                }
                // Remove from all project-scoped entries
                if (claudeConfig.projects) {
                  for (const projectConfig of Object.values(claudeConfig.projects)) {
                    if (projectConfig?.mcpServers) {
                      for (const serverKey of Object.keys(mcpConfig)) {
                        if (serverKey in projectConfig.mcpServers) {
                          delete projectConfig.mcpServers[serverKey];
                          changed = true;
                        }
                      }
                    }
                  }
                }
                if (changed) {
                  await fs.writeFile(claudeJsonPath, JSON.stringify(claudeConfig, null, 2), 'utf-8');
                }
              } catch (e) {
                console.warn(`[AgentApprove] Could not remove MCP "${mcp.name}" from submitter .claude.json:`, e.message);
              }

              // Re-install MCP for submitter from system repo
              await installMcp(mcp.name, SYSTEM_MCP_REPO_URL, submitterUuid);
              console.log(`[AgentApprove] Re-installed MCP "${mcp.name}" for submitter from system repo`);

              // Update agent.yaml: replace empty repo with system MCP repo git URL
              const escapedName = mcp.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
              agentYamlContent = agentYamlContent.replace(
                new RegExp(`(- name: "${escapedName}"\\s*\\n\\s+repo: )""`, 'gm'),
                `$1"${SYSTEM_MCP_REPO_URL}"`
              );
            } catch (e) {
              console.error(`[AgentApprove] Failed to migrate MCP "${mcp.name}":`, e.message);
              // Continue with other MCPs even if one fails
            }
          }
        }
      }

      // Write updated agent.yaml back to extractDir so publishAgentToRepo uses correct repo URLs
      if (localSkills.length > 0 || localMcps.length > 0) {
        try {
          await fs.writeFile(agentYamlPath, agentYamlContent, 'utf-8');
        } catch (e) {
          console.error('[AgentApprove] Failed to write updated agent.yaml:', e.message);
        }
      }

      // === C. Handle git repo dependencies ===
      const yamlGitRepos = agentYamlContent ? parseYamlGitRepos(agentYamlContent) : [];
      if (yamlGitRepos.length > 0) {
        for (const gitRepo of yamlGitRepos) {
          if (!gitRepo.name || !gitRepo.repo) continue;
          try {
            // Try to promote the submitter's existing local clone to shared location first
            // (avoids a full network re-clone if the user already has the repo cloned)
            let sharedPath = null;
            if (submitterUuid) {
              const submitterProjectDir = path.join(getUserPaths(submitterUuid).projectsDir, submission.agent_name);
              const localRepoPath = path.join(submitterProjectDir, gitRepo.name);
              sharedPath = await tryPromoteLocalRepoToShared(localRepoPath, gitRepo.repo, gitRepo.branch || 'main');
              if (sharedPath) {
                console.log(`[AgentApprove] Promoted local repo "${gitRepo.name}" to shared (no re-clone needed)`);
              }
            }

            // Fall back to normal clone/update if promotion was not possible
            if (!sharedPath) {
              sharedPath = await ensureSharedGitRepo(gitRepo.repo, gitRepo.branch || 'main');
            }
            console.log(`[AgentApprove] Ensured shared git repo "${gitRepo.name}" at ${sharedPath}`);

            // Replace submitter's original subdirectory with symlink to shared repo
            if (submitterUuid) {
              const submitterProjectDir = path.join(getUserPaths(submitterUuid).projectsDir, submission.agent_name);
              try {
                await linkGitRepoToProject(submitterProjectDir, gitRepo.name, sharedPath);
                console.log(`[AgentApprove] Linked git repo "${gitRepo.name}" for submitter`);
              } catch (e) {
                console.warn(`[AgentApprove] Could not link git repo "${gitRepo.name}" for submitter:`, e.message);
              }
            }
          } catch (e) {
            console.error(`[AgentApprove] Failed to setup shared git repo "${gitRepo.name}":`, e.message);
          }
        }

        // Update submitter's agentInfo with git repos
        if (submitterUuid) {
          try {
            const submitterConfig = await loadProjectConfig(submitterUuid);
            const submitterProjectDir = path.join(getUserPaths(submitterUuid).projectsDir, submission.agent_name);
            const submitterKey = submitterProjectDir.replace(/\//g, '-');
            if (submitterConfig[submitterKey]?.agentInfo) {
              submitterConfig[submitterKey].agentInfo.gitRepos = yamlGitRepos.filter(g => g.name && g.repo).map(g => ({
                name: g.name, repo: g.repo, branch: g.branch || 'main'
              }));
              await saveProjectConfig(submitterConfig, submitterUuid);
            }
          } catch (e) {
            console.warn('[AgentApprove] Could not update submitter gitRepos config:', e.message);
          }
        }
      }

      // Remove temporary migration directories from extractDir before publishing to agent repo
      await fs.rm(path.join(extractDir, '_skill_files'), { recursive: true, force: true });
      await fs.rm(path.join(extractDir, '_mcp_configs'), { recursive: true, force: true });
    }

    // Publish to git repo
    try {
      await publishAgentToRepo(submission.agent_name, extractDir, newVersion, submitter);
    } catch (err) {
      return res.status(500).json({ error: 'Failed to publish to git repo', details: err.message });
    }

    // Update DB
    agentSubmissionDb.updateStatus(submission.id, 'approved', {
      version: newVersion,
      reviewedBy: reviewerId
    });

    let updatedUsersCount = 0;

    if (updateAllUsers) {
      // Force-update all users who have this agent installed
      try {
        const freshAgents = await scanAgents();
        const publishedAgent = freshAgents.find(a =>
          a.dirName === submission.agent_name || a.name === submission.agent_name
        );

        const allUsers = userDb.getAllUsers();
        for (const user of allUsers) {
          if (!user.uuid) continue;
          try {
            const config = await loadProjectConfig(user.uuid);
            const userPaths = getUserPaths(user.uuid);
            let userUpdated = false;

            for (const [, entry] of Object.entries(config)) {
              if (!entry.agentInfo?.isAgent || entry.agentInfo.agentName !== submission.agent_name) continue;

              const projectDir = path.join(userPaths.projectsDir, submission.agent_name);
              let newClaudeMdHash = entry.agentInfo.claudeMdHash;
              let newInstalledFiles = entry.agentInfo.installedFiles;

              // Force-copy agent files from repo to user's project directory
              if (publishedAgent?.path) {
                try {
                  // Compute new file list from the published agent
                  const newFileList = await listAgentFiles(publishedAgent.path);

                  // Remove files that were in the old version but are not in the new version.
                  // Only remove files we know were installed by the agent (tracked in installedFiles).
                  // This preserves any files the user created themselves.
                  const oldFileList = entry.agentInfo.installedFiles || [];
                  const filesToRemove = oldFileList.filter(f => !newFileList.includes(f));
                  for (const relPath of filesToRemove) {
                    await fs.rm(path.join(projectDir, relPath), { force: true }).catch(() => {});
                  }

                  // Clean up directories that became empty after file removal.
                  // Collect unique parent dirs, sort deepest-first so nested empties are handled.
                  if (filesToRemove.length > 0) {
                    const parentDirs = [...new Set(
                      filesToRemove.map(f => path.dirname(f)).filter(d => d !== '.')
                    )].sort((a, b) => b.split(path.sep).length - a.split(path.sep).length);
                    for (const relDir of parentDirs) {
                      try {
                        const remaining = await fs.readdir(path.join(projectDir, relDir));
                        if (remaining.length === 0) {
                          await fs.rmdir(path.join(projectDir, relDir));
                        }
                      } catch {}
                    }
                  }

                  // Copy new agent files (overwriting tracked agent files)
                  const agentEntries = await fs.readdir(publishedAgent.path, { withFileTypes: true });
                  for (const fileEntry of agentEntries) {
                    if (fileEntry.name === 'agent.yaml') continue;
                    const src = path.join(publishedAgent.path, fileEntry.name);
                    const dst = path.join(projectDir, fileEntry.name);
                    if (fileEntry.isDirectory()) {
                      await fs.cp(src, dst, { recursive: true, force: true });
                    } else {
                      await fs.copyFile(src, dst);
                      if (fileEntry.name === 'CLAUDE.md') {
                        try {
                          const content = await fs.readFile(dst, 'utf-8');
                          newClaudeMdHash = createHash('sha256').update(content).digest('hex');
                        } catch {}
                      }
                    }
                  }

                  newInstalledFiles = newFileList;
                } catch (copyErr) {
                  console.error(`[AgentApprove] Failed to copy files for user ${user.uuid}:`, copyErr.message);
                }
              }

              entry.agentInfo.installedVersion = newVersion;
              entry.agentInfo.installedAt = new Date().toISOString();
              entry.agentInfo.claudeMdHash = newClaudeMdHash;
              entry.agentInfo.installedFiles = newInstalledFiles;

              if (publishedAgent) {
                // Update git repo dependencies
                const newGitRepos = (publishedAgent.git_repos || []).filter(g => g.name && g.repo);
                const oldGitRepos = entry.agentInfo.gitRepos || [];
                const oldGitByName = new Map(oldGitRepos.map(g => [g.name, g]));
                const newGitByName = new Map(newGitRepos.map(g => [g.name, g]));

                for (const old of oldGitRepos) {
                  if (!newGitByName.has(old.name)) {
                    try {
                      const linkPath = path.join(projectDir, old.name);
                      await fs.unlink(linkPath).catch(() => {});
                      console.log(`[AgentApprove] Removed git repo symlink "${old.name}" for user ${user.uuid}`);
                    } catch {}
                  }
                }

                for (const gitRepo of newGitRepos) {
                  const old = oldGitByName.get(gitRepo.name);
                  const branchChanged = !old || old.branch !== gitRepo.branch;
                  const repoChanged = !old || old.repo !== gitRepo.repo;

                  if (!old || branchChanged || repoChanged) {
                    try {
                      const sharedPath = await ensureSharedGitRepo(gitRepo.repo, gitRepo.branch || 'main');
                      await linkGitRepoToProject(projectDir, gitRepo.name, sharedPath);
                      console.log(`[AgentApprove] Updated git repo "${gitRepo.name}" for user ${user.uuid}`);
                    } catch (e) {
                      console.warn(`[AgentApprove] Failed to update git repo "${gitRepo.name}" for user ${user.uuid}:`, e.message);
                    }
                  } else {
                    try { await ensureSharedGitRepo(gitRepo.repo, gitRepo.branch || 'main'); } catch {}
                  }
                }

                entry.agentInfo.gitRepos = newGitRepos.map(g => ({
                  name: g.name, repo: g.repo, branch: g.branch || 'main'
                }));

                // Update skill dependencies
                const newSkills = (publishedAgent.skills || []).filter(s => s.name && s.repo);
                const oldSkills = entry.agentInfo.installedSkills;
                const newSkillNames = new Set(newSkills.map(s => s.name));

                if (oldSkills?.length > 0) {
                  // Precise diff using recorded data
                  for (const old of oldSkills) {
                    if (!newSkillNames.has(old.name)) {
                      try {
                        await uninstallSkill(old.name, user.uuid, projectDir);
                        console.log(`[AgentApprove] Removed old skill "${old.name}" for user ${user.uuid}`);
                      } catch (e) {
                        console.warn(`[AgentApprove] Could not remove old skill "${old.name}" for user ${user.uuid}:`, e.message);
                      }
                    }
                  }
                } else {
                  // No recorded data — fallback: scan actual skills dir
                  try {
                    const skillsDir = path.join(projectDir, '.claude', 'skills');
                    const entries = await fs.readdir(skillsDir).catch(() => []);
                    for (const name of entries) {
                      if (!newSkillNames.has(name)) {
                        try {
                          await fs.unlink(path.join(skillsDir, name));
                          console.log(`[AgentApprove] Removed residual skill "${name}" for user ${user.uuid} (fallback cleanup)`);
                        } catch (e) {
                          console.warn(`[AgentApprove] Could not remove residual skill "${name}" for user ${user.uuid}:`, e.message);
                        }
                      }
                    }
                  } catch {}
                }

                const newInstalledSkills = [];
                for (const skill of newSkills) {
                  try {
                    const ok = await installSkill(skill.name, skill.repo, user.uuid, projectDir);
                    if (ok) newInstalledSkills.push({ name: skill.name, repo: skill.repo });
                  } catch (e) {
                    console.warn(`[AgentApprove] Could not install skill "${skill.name}" for user ${user.uuid}:`, e.message);
                  }
                }
                entry.agentInfo.installedSkills = newInstalledSkills;

                // Update MCP dependencies
                const newMcps = (publishedAgent.mcps || []).filter(m => m.name && m.repo);
                const oldMcps = entry.agentInfo.installedMcps;
                const newMcpNames = new Set(newMcps.map(m => m.name));

                if (oldMcps?.length > 0) {
                  // Precise diff using recorded data (includes exact server keys)
                  for (const old of oldMcps) {
                    if (!newMcpNames.has(old.name)) {
                      try {
                        await uninstallMcp(old.keys, user.uuid, projectDir);
                        console.log(`[AgentApprove] Removed old MCP "${old.name}" for user ${user.uuid}`);
                      } catch (e) {
                        console.warn(`[AgentApprove] Could not remove old MCP "${old.name}" for user ${user.uuid}:`, e.message);
                      }
                    }
                  }
                } else {
                  // No recorded data — fallback: remove ALL project-scoped MCP keys (will be reinstalled)
                  try {
                    const userPaths = getUserPaths(user.uuid);
                    const claudeJsonPath = path.join(userPaths.claudeDir, '.claude.json');
                    const claudeConfig = JSON.parse(await fs.readFile(claudeJsonPath, 'utf-8'));
                    const projectMcpKeys = Object.keys(claudeConfig.projects?.[projectDir]?.mcpServers || {});
                    if (projectMcpKeys.length > 0) {
                      await uninstallMcp(projectMcpKeys, user.uuid, projectDir);
                      console.log(`[AgentApprove] Removed ${projectMcpKeys.length} residual project MCPs for user ${user.uuid} (fallback cleanup)`);
                    }
                  } catch {}
                }

                const newInstalledMcps = [];
                for (const mcp of newMcps) {
                  try {
                    const result = await installMcp(mcp.name, mcp.repo, user.uuid, projectDir);
                    if (result.success) newInstalledMcps.push({ name: mcp.name, repo: mcp.repo, keys: result.keys });
                  } catch (e) {
                    console.warn(`[AgentApprove] Could not install MCP "${mcp.name}" for user ${user.uuid}:`, e.message);
                  }
                }
                entry.agentInfo.installedMcps = newInstalledMcps;
              }

              userUpdated = true;
            }

            if (userUpdated) {
              await saveProjectConfig(config, user.uuid);
              updatedUsersCount++;
            }
          } catch (err) {
            console.error(`[AgentApprove] Failed to update agent for user ${user.uuid}:`, err.message);
          }
        }
        console.log(`[AgentApprove] Force-updated ${updatedUsersCount} users for agent "${submission.agent_name}" v${newVersion}`);
      } catch (err) {
        console.error('[AgentApprove] Failed to update all users:', err.message);
      }
    } else {
      // Auto-update only the submitter's installed agent version
      try {
        const submitterUser = userDb.getUserById(submission.user_id);
        if (submitterUser?.uuid) {
          const config = await loadProjectConfig(submitterUser.uuid);
          let updated = false;
          for (const [, entry] of Object.entries(config)) {
            if (entry.agentInfo?.isAgent && entry.agentInfo.agentName === submission.agent_name) {
              entry.agentInfo.installedVersion = newVersion;
              entry.agentInfo.installedAt = new Date().toISOString();
              updated = true;
            }
          }
          if (updated) {
            await saveProjectConfig(config, submitterUser.uuid);
          }
        }
      } catch (err) {
        // Non-critical: don't fail the approval if auto-update fails
        console.error('[AgentApprove] Failed to auto-update submitter config:', err.message);
      }
    }

    // Cleanup extracted dir
    fs.rm(extractDir, { recursive: true, force: true }).catch(() => {});

    res.json({ success: true, version: newVersion, updatedUsersCount });
  } catch (error) {
    console.error('Error approving submission:', error);
    res.status(500).json({ error: 'Failed to approve submission', details: error.message });
  }
});

/**
 * POST /api/agents/submissions/:id/reject
 * Reject a submission (admin only)
 */
router.post('/submissions/:id/reject', async (req, res) => {
  try {
    const userRole = req.user?.role;
    const reviewerId = req.user?.id;
    if (userRole !== 'admin' && userRole !== 'super_admin') {
      return res.status(403).json({ error: 'Admin access required' });
    }

    const submission = agentSubmissionDb.getById(req.params.id);
    if (!submission) return res.status(404).json({ error: 'Submission not found' });
    if (submission.status !== 'pending') {
      return res.status(400).json({ error: 'Submission is not pending' });
    }

    const { reason } = req.body;

    agentSubmissionDb.updateStatus(submission.id, 'rejected', {
      rejectReason: reason || '',
      reviewedBy: reviewerId
    });

    res.json({ success: true });
  } catch (error) {
    console.error('Error rejecting submission:', error);
    res.status(500).json({ error: 'Failed to reject submission', details: error.message });
  }
});

export default router;
