import express from 'express';
import { promises as fs } from 'fs';
import path from 'path';
import { spawn } from 'child_process';
import { getUserPaths, getPublicPaths } from '../services/user-directories.js';
import { SYSTEM_MCP_REPO_OWNER, SYSTEM_MCP_REPO_NAME } from '../services/system-mcp-repo.js';
import { addToCodexConfig, removeFromCodexConfig } from '../services/codex-mcp.js';

const router = express.Router();

// Trusted git hosting domains
const TRUSTED_GIT_HOSTS = ['github.com', 'gitlab.com', 'bitbucket.org', 'git.amberweather.com'];

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 pathParts = pathPart.split('/');

  if (pathParts.length >= 2) {
    return { host, owner: pathParts[0], repo: pathParts[1] };
  }
  return { host, owner: null, repo: null };
}

function validateGitUrl(url) {
  if (!url || typeof url !== 'string') {
    return { valid: false, error: 'Repository URL is required' };
  }

  if (isSshUrl(url)) {
    const parsed = parseSshUrl(url);
    if (!parsed) {
      return { valid: false, error: 'Invalid SSH URL format' };
    }
    if (!TRUSTED_GIT_HOSTS.includes(parsed.host)) {
      return { valid: false, error: `Only trusted git hosts are allowed: ${TRUSTED_GIT_HOSTS.join(', ')}` };
    }
    return { valid: true, isSsh: true };
  }

  let parsedUrl;
  try {
    parsedUrl = new URL(url);
  } catch {
    return { valid: false, error: 'Invalid URL format' };
  }

  const host = parsedUrl.hostname.toLowerCase();
  const allowedProtocols = ['https:', 'http:'];

  if (host === 'git.amberweather.com') {
    if (!allowedProtocols.includes(parsedUrl.protocol)) {
      return { valid: false, error: 'Only HTTPS or HTTP URLs are allowed for this host' };
    }
  } else {
    if (parsedUrl.protocol !== 'https:') {
      return { valid: false, error: 'Only HTTPS URLs are allowed' };
    }
  }

  if (!TRUSTED_GIT_HOSTS.includes(host)) {
    return { valid: false, error: `Only trusted git hosts are allowed: ${TRUSTED_GIT_HOSTS.join(', ')}` };
  }

  return { valid: true };
}

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;
  }

  const parsedUrl = new URL(url);
  const pathParts = parsedUrl.pathname.replace(/^\//, '').replace(/\.git$/, '').split('/');
  if (pathParts.length >= 2) {
    return { owner: pathParts[0], repo: pathParts[1] };
  }
  return null;
}

function cloneRepository(url, destinationPath) {
  return new Promise((resolve, reject) => {
    const gitProcess = spawn('git', ['clone', '--depth', '1', url, destinationPath], {
      stdio: ['ignore', 'pipe', 'pipe'],
      env: { ...process.env, GIT_TERMINAL_PROMPT: '0' }
    });

    let stderr = '';
    gitProcess.stderr.on('data', (data) => { stderr += data.toString(); });
    gitProcess.on('close', (code) => {
      if (code === 0) resolve();
      else reject(new Error(stderr || `Git clone failed with code ${code}`));
    });
    gitProcess.on('error', (err) => reject(err));
  });
}

function updateRepository(repoPath) {
  return new Promise((resolve, reject) => {
    const gitProcess = spawn('git', ['pull', '--ff-only'], {
      cwd: repoPath,
      stdio: ['ignore', 'pipe', 'pipe'],
      env: { ...process.env, GIT_TERMINAL_PROMPT: '0' }
    });

    let stderr = '';
    gitProcess.stderr.on('data', (data) => { stderr += data.toString(); });
    gitProcess.on('close', (code) => {
      if (code === 0) resolve();
      else reject(new Error(stderr || `Git pull failed with code ${code}`));
    });
    gitProcess.on('error', (err) => reject(err));
  });
}

/**
 * Parse mcp.yaml metadata from an MCP service directory
 */
async function parseMcpMetadata(servicePath) {
  try {
    const yamlPath = path.join(servicePath, 'mcp.yaml');
    const content = await fs.readFile(yamlPath, 'utf-8');

    let name = path.basename(servicePath);
    let description = '';

    const nameMatch = content.match(/^name:\s*(.+)$/m);
    const descMatch = content.match(/^description:\s*(.+)$/m);
    if (nameMatch) name = nameMatch[1].trim();
    if (descMatch) description = descMatch[1].trim();

    return { name, description };
  } catch {
    return { name: path.basename(servicePath), description: '' };
  }
}

/**
 * Check if a directory is a valid MCP service (must contain mcp.json)
 */
async function isValidMcpService(servicePath) {
  try {
    const stat = await fs.stat(servicePath);
    if (!stat.isDirectory()) return false;
    await fs.access(path.join(servicePath, 'mcp.json'));
    return true;
  } catch {
    return false;
  }
}

/**
 * Scan a repository directory for MCP service subdirectories
 */
async function scanRepoForMcpServices(repoPath, repository, installedServerNames) {
  const found = [];

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

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

    const servicePath = path.join(repoPath, entry.name);
    if (await isValidMcpService(servicePath)) {
      const metadata = await parseMcpMetadata(servicePath);

      // Read mcp.json to get server names and type info
      let mcpJson = {};
      let serverNames = [];
      let serviceType = 'stdio';
      try {
        const jsonContent = await fs.readFile(path.join(servicePath, 'mcp.json'), 'utf-8');
        mcpJson = JSON.parse(jsonContent);
        serverNames = Object.keys(mcpJson);
        // Detect type from first server config
        if (serverNames.length > 0) {
          const firstConfig = mcpJson[serverNames[0]];
          serviceType = firstConfig.type || 'stdio';
        }
      } catch {
        // ignore parse errors
      }

      const installed = serverNames.length > 0 && serverNames.every(n => installedServerNames.has(n));

      found.push({
        dirName: entry.name,
        name: metadata.name,
        description: metadata.description,
        repository,
        type: serviceType,
        serverNames,
        installed,
        path: servicePath
      });
    }
  }

  return found;
}

/**
 * Read installed MCP server names from user's .claude.json
 */
async function getInstalledMcpServerNames(userUuid) {
  try {
    const userPaths = getUserPaths(userUuid);
    const claudeJsonPath = path.join(userPaths.claudeDir, '.claude.json');
    const content = await fs.readFile(claudeJsonPath, 'utf-8');
    const config = JSON.parse(content);
    return new Set(Object.keys(config.mcpServers || {}));
  } catch {
    return new Set();
  }
}

/**
 * GET /api/mcp-repos
 * List user's MCP repositories
 */
router.get('/', async (req, res) => {
  try {
    const userUuid = req.user?.uuid;
    if (!userUuid) return res.status(401).json({ error: 'User authentication required' });

    const userPaths = getUserPaths(userUuid);
    await fs.mkdir(userPaths.mcpRepoDir, { recursive: true });

    const entries = await fs.readdir(userPaths.mcpRepoDir, { withFileTypes: true });
    const repos = [];

    for (const ownerEntry of entries) {
      if (ownerEntry.name.startsWith('.') || !ownerEntry.isDirectory()) continue;

      const ownerPath = path.join(userPaths.mcpRepoDir, ownerEntry.name);
      const repoEntries = await fs.readdir(ownerPath, { withFileTypes: true });

      for (const repoEntry of repoEntries) {
        if (repoEntry.name.startsWith('.') || (!repoEntry.isDirectory() && !repoEntry.isSymbolicLink())) continue;

        const repoPath = path.join(ownerPath, repoEntry.name);
        const services = await scanRepoForMcpServices(repoPath, `${ownerEntry.name}/${repoEntry.name}`, new Set());

        repos.push({
          owner: ownerEntry.name,
          repo: repoEntry.name,
          serviceCount: services.length,
          path: repoPath,
          isSystem: ownerEntry.name === SYSTEM_MCP_REPO_OWNER && repoEntry.name === SYSTEM_MCP_REPO_NAME
        });
      }
    }

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

/**
 * POST /api/mcp-repos
 * Add (clone) a new MCP repository
 */
router.post('/', async (req, res) => {
  try {
    const userUuid = req.user?.uuid;
    if (!userUuid) return res.status(401).json({ error: 'User authentication required' });

    let { url, branch = 'main' } = req.body;

    // Support short format: owner/repo → GitHub URL
    if (url && !url.includes('://') && !isSshUrl(url) && url.includes('/')) {
      url = `https://github.com/${url}`;
    }

    const validation = validateGitUrl(url);
    if (!validation.valid) {
      return res.status(400).json({ error: validation.error });
    }

    const parsed = parseGitUrl(url);
    if (!parsed) {
      return res.status(400).json({ error: 'Could not parse repository owner/name from URL' });
    }

    const { owner, repo } = parsed;
    const publicPaths = getPublicPaths();
    const publicRepoPath = path.join(publicPaths.mcpRepoDir, owner, repo);

    // Clone into shared public dir if not already there
    let alreadyCloned = false;
    try {
      await fs.access(publicRepoPath);
      alreadyCloned = true;
    } catch {
      // not cloned yet
    }

    if (!alreadyCloned) {
      await fs.mkdir(path.join(publicPaths.mcpRepoDir, owner), { recursive: true });
      console.log(`[MCP Repos] Cloning ${url} → ${publicRepoPath}`);
      await cloneRepository(url, publicRepoPath);
    }

    // Create per-user symlink
    const userPaths = getUserPaths(userUuid);
    const userOwnerDir = path.join(userPaths.mcpRepoDir, owner);
    await fs.mkdir(userOwnerDir, { recursive: true });

    const userRepoLink = path.join(userOwnerDir, repo);
    try {
      await fs.lstat(userRepoLink);
      // symlink already exists - that's fine
    } catch {
      await fs.symlink(publicRepoPath, userRepoLink);
    }

    const services = await scanRepoForMcpServices(publicRepoPath, `${owner}/${repo}`, new Set());

    res.json({ success: true, owner, repo, serviceCount: services.length });
  } catch (error) {
    console.error('Error adding MCP repo:', error);
    res.status(500).json({ error: 'Failed to add MCP repo', details: error.message });
  }
});

/**
 * POST /api/mcp-repos/refresh
 * Pull updates for all user repos
 */
router.post('/refresh', async (req, res) => {
  try {
    const userUuid = req.user?.uuid;
    if (!userUuid) return res.status(401).json({ error: 'User authentication required' });

    const publicPaths = getPublicPaths();
    const publicMcpRepoDir = publicPaths.mcpRepoDir;

    let updated = 0;
    let failed = 0;

    try {
      const ownerEntries = await fs.readdir(publicMcpRepoDir, { withFileTypes: true });
      for (const ownerEntry of ownerEntries) {
        if (!ownerEntry.isDirectory() || ownerEntry.name.startsWith('.')) continue;
        const ownerPath = path.join(publicMcpRepoDir, ownerEntry.name);
        const repoEntries = await fs.readdir(ownerPath, { withFileTypes: true });
        for (const repoEntry of repoEntries) {
          if (!repoEntry.isDirectory() || repoEntry.name.startsWith('.')) continue;
          const repoPath = path.join(ownerPath, repoEntry.name);
          try {
            await updateRepository(repoPath);
            updated++;
          } catch (err) {
            console.error(`[MCP Repos] Failed to update ${ownerEntry.name}/${repoEntry.name}:`, err.message);
            failed++;
          }
        }
      }
    } catch {
      // public dir may not exist yet
    }

    res.json({ success: true, updated, failed });
  } catch (error) {
    console.error('Error refreshing MCP repos:', error);
    res.status(500).json({ error: 'Failed to refresh repos', details: error.message });
  }
});

/**
 * GET /api/mcp-repos/available
 * Scan all user repos for available MCP services
 */
router.get('/available', async (req, res) => {
  try {
    const userUuid = req.user?.uuid;
    if (!userUuid) return res.status(401).json({ error: 'User authentication required' });

    const userPaths = getUserPaths(userUuid);
    await fs.mkdir(userPaths.mcpRepoDir, { recursive: true });

    const installedNames = await getInstalledMcpServerNames(userUuid);
    const services = [];

    const ownerEntries = await fs.readdir(userPaths.mcpRepoDir, { withFileTypes: true });
    for (const ownerEntry of ownerEntries) {
      if (ownerEntry.name.startsWith('.') || !ownerEntry.isDirectory()) continue;
      const ownerPath = path.join(userPaths.mcpRepoDir, ownerEntry.name);
      const repoEntries = await fs.readdir(ownerPath, { withFileTypes: true });
      for (const repoEntry of repoEntries) {
        if (repoEntry.name.startsWith('.') || (!repoEntry.isDirectory() && !repoEntry.isSymbolicLink())) continue;
        const repoPath = path.join(ownerPath, repoEntry.name);
        const found = await scanRepoForMcpServices(repoPath, `${ownerEntry.name}/${repoEntry.name}`, installedNames);
        services.push(...found);
      }
    }

    res.json({ services });
  } catch (error) {
    console.error('Error scanning MCP repos:', error);
    res.status(500).json({ error: 'Failed to scan repos', details: error.message });
  }
});

/**
 * Run claude mcp add-json for a single server entry
 * Format: claude mcp add-json --scope <scope> <name> '<json>'
 */
function addMcpServerViaJson(name, config, scope, claudeDir) {
  return new Promise((resolve, reject) => {
    const jsonString = JSON.stringify(config);
    const proc = spawn('claude', ['mcp', 'add-json', '--scope', scope, name, jsonString], {
      stdio: ['ignore', 'pipe', 'pipe'],
      env: { ...process.env, CLAUDE_CONFIG_DIR: claudeDir }
    });

    let stderr = '';
    proc.stderr.on('data', (d) => { stderr += d.toString(); });
    proc.on('close', (code) => {
      if (code === 0) resolve();
      else reject(new Error(stderr || `claude mcp add-json failed with code ${code}`));
    });
    proc.on('error', (err) => reject(err));
  });
}

/**
 * POST /api/mcp-repos/install
 * Install an MCP service (add its config to user's .claude.json via claude mcp add-json)
 */
router.post('/install', async (req, res) => {
  try {
    const userUuid = req.user?.uuid;
    if (!userUuid) return res.status(401).json({ error: 'User authentication required' });

    const { servicePath, scope = 'user', projectPath } = req.body;
    if (!servicePath) return res.status(400).json({ error: 'servicePath is required' });

    // Security: ensure servicePath is within mcp-repo directory (public or user symlink dir)
    const publicPaths = getPublicPaths();
    const userPaths = getUserPaths(userUuid);
    const realServicePath = path.resolve(servicePath);
    const realMcpRepoDir = path.resolve(publicPaths.mcpRepoDir);
    const realUserMcpRepoDir = path.resolve(userPaths.mcpRepoDir);
    if (!realServicePath.startsWith(realMcpRepoDir) && !realServicePath.startsWith(realUserMcpRepoDir)) {
      return res.status(403).json({ error: 'Invalid service path' });
    }

    // Read mcp.json
    const mcpJsonPath = path.join(servicePath, 'mcp.json');
    let mcpJson;
    try {
      const content = await fs.readFile(mcpJsonPath, 'utf-8');
      mcpJson = JSON.parse(content);
    } catch (err) {
      return res.status(400).json({ error: 'Failed to read mcp.json', details: err.message });
    }
    const installedServers = [];
    const errors = [];

    // Install each server entry
    for (const [serverName, serverConfig] of Object.entries(mcpJson)) {
      try {
        if (scope === 'local' && projectPath) {
          // Bypass Claude CLI for local scope: directly write to .claude.json projects[projectPath].mcpServers
          // (Claude CLI --scope local uses git root as key, which breaks in multi-user deployments)
          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 (!claudeConfig.projects) claudeConfig.projects = {};
          if (!claudeConfig.projects[projectPath]) claudeConfig.projects[projectPath] = {};
          if (!claudeConfig.projects[projectPath].mcpServers) claudeConfig.projects[projectPath].mcpServers = {};
          claudeConfig.projects[projectPath].mcpServers[serverName] = serverConfig;
          await fs.writeFile(claudeJsonPath, JSON.stringify(claudeConfig, null, 2), 'utf-8');
          installedServers.push(serverName);
        } else {
          await addMcpServerViaJson(serverName, serverConfig, scope, userPaths.claudeDir);
          installedServers.push(serverName);
        }
      } catch (err) {
        console.error(`[MCP Repos] CLI install failed for "${serverName}", using fallback:`, err.message);
        // Fallback: directly write to .claude.json
        try {
          const claudeJsonPath = path.join(userPaths.claudeDir, '.claude.json');
          let claudeConfig = {};
          try {
            claudeConfig = JSON.parse(await fs.readFile(claudeJsonPath, 'utf-8'));
          } catch {
            claudeConfig = { hasCompletedOnboarding: true };
          }
          claudeConfig.mcpServers = { ...(claudeConfig.mcpServers || {}), [serverName]: serverConfig };
          await fs.writeFile(claudeJsonPath, JSON.stringify(claudeConfig, null, 2), 'utf-8');
          installedServers.push(serverName);
        } catch (fallbackErr) {
          errors.push(`${serverName}: ${fallbackErr.message}`);
        }
      }
    }

    if (errors.length > 0 && installedServers.length === 0) {
      return res.status(500).json({ error: 'Failed to install MCP service', details: errors.join('; ') });
    }

    // Sync to Codex config.toml
    const codexConfigPath = path.join(userPaths.codexHomeDir, '.codex', 'config.toml');
    try {
      await addToCodexConfig(codexConfigPath, mcpJson);
    } catch (err) {
      console.error('[MCP Repos] Failed to sync to Codex config.toml:', err.message);
    }

    res.json({ success: true, servers: installedServers });
  } catch (error) {
    console.error('Error installing MCP service:', error);
    res.status(500).json({ error: 'Failed to install MCP service', details: error.message });
  }
});

/**
 * DELETE /api/mcp-repos/uninstall/:name
 * Uninstall an MCP server (remove from .claude.json via claude mcp remove)
 */
router.delete('/uninstall/:name', async (req, res) => {
  try {
    const userUuid = req.user?.uuid;
    if (!userUuid) return res.status(401).json({ error: 'User authentication required' });

    const { name } = req.params;
    const { scope = 'user' } = req.query;

    const userPaths = getUserPaths(userUuid);

    await new Promise((resolve, reject) => {
      const proc = spawn('claude', ['mcp', 'remove', '--scope', scope, name], {
        stdio: ['ignore', 'pipe', 'pipe'],
        env: { ...process.env, CLAUDE_CONFIG_DIR: userPaths.claudeDir }
      });

      let stderr = '';
      proc.stderr.on('data', (d) => { stderr += d.toString(); });
      proc.on('close', (code) => {
        if (code === 0) resolve();
        else reject(new Error(stderr || `claude mcp remove failed with code ${code}`));
      });
      proc.on('error', (err) => reject(err));
    });

    // Sync removal to Codex config.toml
    const codexConfigPath = path.join(userPaths.codexHomeDir, '.codex', 'config.toml');
    try {
      await removeFromCodexConfig(codexConfigPath, name);
    } catch (err) {
      console.error('[MCP Repos] Failed to sync removal to Codex config.toml:', err.message);
    }

    res.json({ success: true });
  } catch (error) {
    console.error('Error uninstalling MCP server:', error);

    // Fallback: directly remove from .claude.json
    try {
      const { name } = req.params;
      const userPaths = getUserPaths(req.user.uuid);
      const claudeJsonPath = path.join(userPaths.claudeDir, '.claude.json');
      let claudeConfig = JSON.parse(await fs.readFile(claudeJsonPath, 'utf-8'));
      if (claudeConfig.mcpServers) {
        delete claudeConfig.mcpServers[name];
      }
      await fs.writeFile(claudeJsonPath, JSON.stringify(claudeConfig, null, 2), 'utf-8');

      // Sync removal to Codex config.toml
      const codexConfigPath = path.join(userPaths.codexHomeDir, '.codex', 'config.toml');
      try {
        await removeFromCodexConfig(codexConfigPath, name);
      } catch (err) {
        console.error('[MCP Repos] Failed to sync removal to Codex config.toml:', err.message);
      }

      return res.json({ success: true });
    } catch (fallbackError) {
      return res.status(500).json({ error: 'Failed to uninstall MCP server', details: error.message });
    }
  }
});

/**
 * DELETE /api/mcp-repos/:owner/:repo
 * Remove a repository (delete user's symlink)
 */
router.delete('/:owner/:repo', async (req, res) => {
  try {
    const userUuid = req.user?.uuid;
    if (!userUuid) return res.status(401).json({ error: 'User authentication required' });

    const { owner, repo } = req.params;

    // Validate names to prevent path traversal
    if (!/^[a-zA-Z0-9_.-]+$/.test(owner) || !/^[a-zA-Z0-9_.-]+$/.test(repo)) {
      return res.status(400).json({ error: 'Invalid owner or repo name' });
    }

    // System repo cannot be removed
    if (owner === SYSTEM_MCP_REPO_OWNER && repo === SYSTEM_MCP_REPO_NAME) {
      return res.status(403).json({ error: '内置仓库不能删除' });
    }

    const userPaths = getUserPaths(userUuid);
    const userRepoLink = path.join(userPaths.mcpRepoDir, owner, repo);

    try {
      const stat = await fs.lstat(userRepoLink);
      if (stat.isSymbolicLink() || stat.isDirectory()) {
        await fs.rm(userRepoLink, { recursive: true, force: true });
      }
    } catch {
      // Already gone
    }

    res.json({ success: true });
  } catch (error) {
    console.error('Error removing MCP repo:', error);
    res.status(500).json({ error: 'Failed to remove repo', details: error.message });
  }
});

export default router;
