import {
  mkdir,
  cp,
  access,
  readdir,
  symlink,
  lstat,
  rm,
  readlink,
  writeFile,
  stat,
  realpath,
} from 'fs/promises';
import { join, basename, normalize, resolve, sep, relative, dirname } from 'path';
import { homedir, platform } from 'os';
import type { Skill, AgentType, MintlifySkill, RemoteSkill } from './types';
import type { WellKnownSkill } from './providers/wellknown';
import { agents, detectInstalledAgents, isUniversalAgent } from './agents';
import { AGENTS_DIR, SKILLS_SUBDIR } from './constants';
import { parseSkillMd } from './skills';

export type InstallMode = 'symlink' | 'copy';

interface InstallResult {
  success: boolean;
  path: string;
  canonicalPath?: string;
  mode: InstallMode;
  symlinkFailed?: boolean;
  error?: string;
}

/**
 * Sanitizes a filename/directory name to prevent path traversal attacks
 * and ensures it follows kebab-case convention
 * @param name - The name to sanitize
 * @returns Sanitized name safe for use in file paths
 */
export function sanitizeName(name: string): string {
  const sanitized = name
    .toLowerCase()
    // Replace any sequence of characters that are NOT lowercase letters (a-z),
    // digits (0-9), dots (.), or underscores (_) with a single hyphen.
    // This converts spaces, special chars, and path traversal attempts (../) into hyphens.
    .replace(/[^a-z0-9._]+/g, '-')
    // Remove leading/trailing dots and hyphens to prevent hidden files (.) and
    // ensure clean directory names. The pattern matches:
    // - ^[.\-]+ : one or more dots or hyphens at the start
    // - [.\-]+$ : one or more dots or hyphens at the end
    .replace(/^[.\-]+|[.\-]+$/g, '');

  // Limit to 255 chars (common filesystem limit), fallback to 'unnamed-skill' if empty
  return sanitized.substring(0, 255) || 'unnamed-skill';
}

/**
 * Validates that a path is within an expected base directory
 * @param basePath - The expected base directory
 * @param targetPath - The path to validate
 * @returns true if targetPath is within basePath
 */
function isPathSafe(basePath: string, targetPath: string): boolean {
  const normalizedBase = normalize(resolve(basePath));
  const normalizedTarget = normalize(resolve(targetPath));

  return normalizedTarget.startsWith(normalizedBase + sep) || normalizedTarget === normalizedBase;
}

/**
 * Gets the canonical .agents/skills directory path
 * @param global - Whether to use global (home) or project-level location
 * @param cwd - Current working directory for project-level installs
 */
export function getCanonicalSkillsDir(global: boolean, cwd?: string): string {
  const baseDir = global ? homedir() : cwd || process.cwd();
  return join(baseDir, AGENTS_DIR, SKILLS_SUBDIR);
}

function resolveSymlinkTarget(linkPath: string, linkTarget: string): string {
  return resolve(dirname(linkPath), linkTarget);
}

/**
 * Cleans and recreates a directory for skill installation.
 *
 * This ensures:
 * 1. Renamed/deleted files from previous installs are removed
 * 2. Symlinks (including self-referential ones causing ELOOP) are handled
 *    when canonical and agent paths resolve to the same location
 */
async function cleanAndCreateDirectory(path: string): Promise<void> {
  try {
    await rm(path, { recursive: true, force: true });
  } catch {
    // Ignore cleanup errors - mkdir will fail if there's a real problem
  }
  await mkdir(path, { recursive: true });
}

/**
 * Resolve a path's parent directory through symlinks, keeping the final component.
 * This handles the case where a parent directory (e.g., ~/.claude/skills) is a symlink
 * to another location (e.g., ~/.agents/skills). In that case, computing relative paths
 * from the symlink path produces broken symlinks.
 *
 * Returns the real path of the parent + the original basename.
 * If realpath fails (parent doesn't exist), returns the original resolved path.
 */
async function resolveParentSymlinks(path: string): Promise<string> {
  const resolved = resolve(path);
  const dir = dirname(resolved);
  const base = basename(resolved);
  try {
    const realDir = await realpath(dir);
    return join(realDir, base);
  } catch {
    return resolved;
  }
}

/**
 * Creates a symlink, handling cross-platform differences
 * Returns true if symlink was created, false if fallback to copy is needed
 */
async function createSymlink(target: string, linkPath: string): Promise<boolean> {
  try {
    const resolvedTarget = resolve(target);
    const resolvedLinkPath = resolve(linkPath);

    if (resolvedTarget === resolvedLinkPath) {
      return true;
    }

    // Also check with symlinks resolved in parent directories.
    // This handles cases where e.g. ~/.claude/skills is a symlink to ~/.agents/skills,
    // so ~/.claude/skills/<skill> and ~/.agents/skills/<skill> are physically the same.
    const realTarget = await resolveParentSymlinks(target);
    const realLinkPath = await resolveParentSymlinks(linkPath);

    if (realTarget === realLinkPath) {
      return true;
    }

    try {
      const stats = await lstat(linkPath);
      if (stats.isSymbolicLink()) {
        const existingTarget = await readlink(linkPath);
        if (resolveSymlinkTarget(linkPath, existingTarget) === resolvedTarget) {
          return true;
        }
        await rm(linkPath);
      } else {
        await rm(linkPath, { recursive: true });
      }
    } catch (err: unknown) {
      // ELOOP = circular symlink, ENOENT = doesn't exist
      // For ELOOP, try to remove the broken symlink
      if (err && typeof err === 'object' && 'code' in err && err.code === 'ELOOP') {
        try {
          await rm(linkPath, { force: true });
        } catch {
          // If we can't remove it, symlink creation will fail and trigger copy fallback
        }
      }
      // For ENOENT or other errors, continue to symlink creation
    }

    const linkDir = dirname(linkPath);
    await mkdir(linkDir, { recursive: true });

    // Use the real (symlink-resolved) parent directory for computing the relative path.
    // This ensures the symlink target is correct even when the link's parent dir is a symlink.
    const realLinkDir = await resolveParentSymlinks(linkDir);
    const relativePath = relative(realLinkDir, target);
    const symlinkType = platform() === 'win32' ? 'junction' : undefined;

    await symlink(relativePath, linkPath, symlinkType);
    return true;
  } catch {
    return false;
  }
}

export async function installSkillForAgent(
  skill: Skill,
  agentType: AgentType,
  options: { global?: boolean; cwd?: string; mode?: InstallMode } = {}
): Promise<InstallResult> {
  const agent = agents[agentType];
  const isGlobal = options.global ?? false;
  const cwd = options.cwd || process.cwd();

  // Check if agent supports global installation
  if (isGlobal && agent.globalSkillsDir === undefined) {
    return {
      success: false,
      path: '',
      mode: options.mode ?? 'symlink',
      error: `${agent.displayName} does not support global skill installation`,
    };
  }

  // Sanitize skill name to prevent directory traversal
  const rawSkillName = skill.name || basename(skill.path);
  const skillName = sanitizeName(rawSkillName);

  // Canonical location: .agents/skills/<skill-name>
  const canonicalBase = getCanonicalSkillsDir(isGlobal, cwd);
  const canonicalDir = join(canonicalBase, skillName);

  // Agent-specific location (for symlink)
  const agentBase = isGlobal ? agent.globalSkillsDir! : join(cwd, agent.skillsDir);
  const agentDir = join(agentBase, skillName);

  const installMode = options.mode ?? 'symlink';

  // Validate paths
  if (!isPathSafe(canonicalBase, canonicalDir)) {
    return {
      success: false,
      path: agentDir,
      mode: installMode,
      error: 'Invalid skill name: potential path traversal detected',
    };
  }

  if (!isPathSafe(agentBase, agentDir)) {
    return {
      success: false,
      path: agentDir,
      mode: installMode,
      error: 'Invalid skill name: potential path traversal detected',
    };
  }

  try {
    // For copy mode, skip canonical directory and copy directly to agent location
    if (installMode === 'copy') {
      await cleanAndCreateDirectory(agentDir);
      await copyDirectory(skill.path, agentDir);

      return {
        success: true,
        path: agentDir,
        mode: 'copy',
      };
    }

    // Symlink mode: copy to canonical location and symlink to agent location
    await cleanAndCreateDirectory(canonicalDir);
    await copyDirectory(skill.path, canonicalDir);

    // For universal agents with global install, the skill is already in the canonical
    // ~/.agents/skills directory. Skip creating a symlink to the agent-specific global dir
    // (e.g. ~/.copilot/skills) to avoid duplicates.
    if (isGlobal && isUniversalAgent(agentType)) {
      return {
        success: true,
        path: canonicalDir,
        canonicalPath: canonicalDir,
        mode: 'symlink',
      };
    }

    const symlinkCreated = await createSymlink(canonicalDir, agentDir);

    if (!symlinkCreated) {
      // Symlink failed, fall back to copy
      await cleanAndCreateDirectory(agentDir);
      await copyDirectory(skill.path, agentDir);

      return {
        success: true,
        path: agentDir,
        canonicalPath: canonicalDir,
        mode: 'symlink',
        symlinkFailed: true,
      };
    }

    return {
      success: true,
      path: agentDir,
      canonicalPath: canonicalDir,
      mode: 'symlink',
    };
  } catch (error) {
    return {
      success: false,
      path: agentDir,
      mode: installMode,
      error: error instanceof Error ? error.message : 'Unknown error',
    };
  }
}

const EXCLUDE_FILES = new Set(['README.md', 'metadata.json']);
const EXCLUDE_DIRS = new Set(['.git']);

const isExcluded = (name: string, isDirectory: boolean = false): boolean => {
  if (EXCLUDE_FILES.has(name)) {return true;}
  if (name.startsWith('_')) {return true;}
  if (isDirectory && EXCLUDE_DIRS.has(name)) {return true;}
  return false;
};

async function copyDirectory(src: string, dest: string): Promise<void> {
  await mkdir(dest, { recursive: true });

  const entries = await readdir(src, { withFileTypes: true });

  // Copy files and directories in parallel
  await Promise.all(
    entries
      .filter((entry) => !isExcluded(entry.name, entry.isDirectory()))
      .map(async (entry) => {
        const srcPath = join(src, entry.name);
        const destPath = join(dest, entry.name);

        if (entry.isDirectory()) {
          await copyDirectory(srcPath, destPath);
        } else {
          await cp(srcPath, destPath, {
            // If the file is a symlink to elsewhere in a remote skill, it may not
            // resolve correctly once it has been copied to the local location.
            // `dereference: true` tells Node to copy the file instead of copying
            // the symlink. `recursive: true` handles symlinks pointing to directories.
            dereference: true,
            recursive: true,
          });
        }
      })
  );
}

export async function isSkillInstalled(
  skillName: string,
  agentType: AgentType,
  options: { global?: boolean; cwd?: string } = {}
): Promise<boolean> {
  const agent = agents[agentType];
  const sanitized = sanitizeName(skillName);

  // Agent doesn't support global installation
  if (options.global && agent.globalSkillsDir === undefined) {
    return false;
  }

  const targetBase = options.global
    ? agent.globalSkillsDir!
    : join(options.cwd || process.cwd(), agent.skillsDir);

  const skillDir = join(targetBase, sanitized);

  if (!isPathSafe(targetBase, skillDir)) {
    return false;
  }

  try {
    await access(skillDir);
    return true;
  } catch {
    return false;
  }
}

export function getInstallPath(
  skillName: string,
  agentType: AgentType,
  options: { global?: boolean; cwd?: string } = {}
): string {
  const agent = agents[agentType];
  const cwd = options.cwd || process.cwd();
  const sanitized = sanitizeName(skillName);

  // Agent doesn't support global installation, fall back to project path
  const targetBase =
    options.global && agent.globalSkillsDir !== undefined
      ? agent.globalSkillsDir
      : join(cwd, agent.skillsDir);

  const installPath = join(targetBase, sanitized);

  if (!isPathSafe(targetBase, installPath)) {
    throw new Error('Invalid skill name: potential path traversal detected');
  }

  return installPath;
}

/**
 * Gets the canonical .agents/skills/<skill> path
 */
export function getCanonicalPath(
  skillName: string,
  options: { global?: boolean; cwd?: string } = {}
): string {
  const sanitized = sanitizeName(skillName);
  const canonicalBase = getCanonicalSkillsDir(options.global ?? false, options.cwd);
  const canonicalPath = join(canonicalBase, sanitized);

  if (!isPathSafe(canonicalBase, canonicalPath)) {
    throw new Error('Invalid skill name: potential path traversal detected');
  }

  return canonicalPath;
}

/**
 * Install a Mintlify skill from a direct URL
 * The skill name is derived from the mintlify-proj frontmatter
 * Supports symlink mode (writes to canonical location and symlinks to agent dirs)
 * or copy mode (writes directly to each agent dir).
 * @deprecated Use installRemoteSkillForAgent instead
 */
export async function installMintlifySkillForAgent(
  skill: MintlifySkill,
  agentType: AgentType,
  options: { global?: boolean; cwd?: string; mode?: InstallMode } = {}
): Promise<InstallResult> {
  const agent = agents[agentType];
  const isGlobal = options.global ?? false;
  const cwd = options.cwd || process.cwd();
  const installMode = options.mode ?? 'symlink';

  // Check if agent supports global installation
  if (isGlobal && agent.globalSkillsDir === undefined) {
    return {
      success: false,
      path: '',
      mode: installMode,
      error: `${agent.displayName} does not support global skill installation`,
    };
  }

  // Use mintlify-proj as the skill directory name (e.g., "bun.com")
  const skillName = sanitizeName(skill.mintlifySite);

  // Canonical location: .agents/skills/<skill-name>
  const canonicalBase = getCanonicalSkillsDir(isGlobal, cwd);
  const canonicalDir = join(canonicalBase, skillName);

  // Agent-specific location (for symlink)
  const agentBase = isGlobal ? agent.globalSkillsDir! : join(cwd, agent.skillsDir);
  const agentDir = join(agentBase, skillName);

  // Validate paths
  if (!isPathSafe(canonicalBase, canonicalDir)) {
    return {
      success: false,
      path: agentDir,
      mode: installMode,
      error: 'Invalid skill name: potential path traversal detected',
    };
  }

  if (!isPathSafe(agentBase, agentDir)) {
    return {
      success: false,
      path: agentDir,
      mode: installMode,
      error: 'Invalid skill name: potential path traversal detected',
    };
  }

  try {
    // For copy mode, write directly to agent location
    if (installMode === 'copy') {
      await cleanAndCreateDirectory(agentDir);
      const skillMdPath = join(agentDir, 'SKILL.md');
      await writeFile(skillMdPath, skill.content, 'utf-8');

      return {
        success: true,
        path: agentDir,
        mode: 'copy',
      };
    }

    // Symlink mode: write to canonical location and symlink to agent location
    await cleanAndCreateDirectory(canonicalDir);
    const skillMdPath = join(canonicalDir, 'SKILL.md');
    await writeFile(skillMdPath, skill.content, 'utf-8');

    // For universal agents with global install, skip creating agent-specific symlink
    if (isGlobal && isUniversalAgent(agentType)) {
      return {
        success: true,
        path: canonicalDir,
        canonicalPath: canonicalDir,
        mode: 'symlink',
      };
    }

    const symlinkCreated = await createSymlink(canonicalDir, agentDir);

    if (!symlinkCreated) {
      // Symlink failed, fall back to copy
      await cleanAndCreateDirectory(agentDir);
      const agentSkillMdPath = join(agentDir, 'SKILL.md');
      await writeFile(agentSkillMdPath, skill.content, 'utf-8');

      return {
        success: true,
        path: agentDir,
        canonicalPath: canonicalDir,
        mode: 'symlink',
        symlinkFailed: true,
      };
    }

    return {
      success: true,
      path: agentDir,
      canonicalPath: canonicalDir,
      mode: 'symlink',
    };
  } catch (error) {
    return {
      success: false,
      path: agentDir,
      mode: installMode,
      error: error instanceof Error ? error.message : 'Unknown error',
    };
  }
}

/**
 * Install a remote skill from any host provider.
 * The skill directory name is derived from the installName field.
 * Supports symlink mode (writes to canonical location and symlinks to agent dirs)
 * or copy mode (writes directly to each agent dir).
 */
export async function installRemoteSkillForAgent(
  skill: RemoteSkill,
  agentType: AgentType,
  options: { global?: boolean; cwd?: string; mode?: InstallMode } = {}
): Promise<InstallResult> {
  const agent = agents[agentType];
  const isGlobal = options.global ?? false;
  const cwd = options.cwd || process.cwd();
  const installMode = options.mode ?? 'symlink';

  // Check if agent supports global installation
  if (isGlobal && agent.globalSkillsDir === undefined) {
    return {
      success: false,
      path: '',
      mode: installMode,
      error: `${agent.displayName} does not support global skill installation`,
    };
  }

  // Use installName as the skill directory name
  const skillName = sanitizeName(skill.installName);

  // Canonical location: .agents/skills/<skill-name>
  const canonicalBase = getCanonicalSkillsDir(isGlobal, cwd);
  const canonicalDir = join(canonicalBase, skillName);

  // Agent-specific location (for symlink)
  const agentBase = isGlobal ? agent.globalSkillsDir! : join(cwd, agent.skillsDir);
  const agentDir = join(agentBase, skillName);

  // Validate paths
  if (!isPathSafe(canonicalBase, canonicalDir)) {
    return {
      success: false,
      path: agentDir,
      mode: installMode,
      error: 'Invalid skill name: potential path traversal detected',
    };
  }

  if (!isPathSafe(agentBase, agentDir)) {
    return {
      success: false,
      path: agentDir,
      mode: installMode,
      error: 'Invalid skill name: potential path traversal detected',
    };
  }

  try {
    // For copy mode, write directly to agent location
    if (installMode === 'copy') {
      await cleanAndCreateDirectory(agentDir);
      const skillMdPath = join(agentDir, 'SKILL.md');
      await writeFile(skillMdPath, skill.content, 'utf-8');

      return {
        success: true,
        path: agentDir,
        mode: 'copy',
      };
    }

    // Symlink mode: write to canonical location and symlink to agent location
    await cleanAndCreateDirectory(canonicalDir);
    const skillMdPath = join(canonicalDir, 'SKILL.md');
    await writeFile(skillMdPath, skill.content, 'utf-8');

    // For universal agents with global install, skip creating agent-specific symlink
    if (isGlobal && isUniversalAgent(agentType)) {
      return {
        success: true,
        path: canonicalDir,
        canonicalPath: canonicalDir,
        mode: 'symlink',
      };
    }

    const symlinkCreated = await createSymlink(canonicalDir, agentDir);

    if (!symlinkCreated) {
      // Symlink failed, fall back to copy
      await cleanAndCreateDirectory(agentDir);
      const agentSkillMdPath = join(agentDir, 'SKILL.md');
      await writeFile(agentSkillMdPath, skill.content, 'utf-8');

      return {
        success: true,
        path: agentDir,
        canonicalPath: canonicalDir,
        mode: 'symlink',
        symlinkFailed: true,
      };
    }

    return {
      success: true,
      path: agentDir,
      canonicalPath: canonicalDir,
      mode: 'symlink',
    };
  } catch (error) {
    return {
      success: false,
      path: agentDir,
      mode: installMode,
      error: error instanceof Error ? error.message : 'Unknown error',
    };
  }
}

/**
 * Install a well-known skill with multiple files.
 * The skill directory name is derived from the installName field.
 * All files from the skill's files map are written to the installation directory.
 * Supports symlink mode (writes to canonical location and symlinks to agent dirs)
 * or copy mode (writes directly to each agent dir).
 */
export async function installWellKnownSkillForAgent(
  skill: WellKnownSkill,
  agentType: AgentType,
  options: { global?: boolean; cwd?: string; mode?: InstallMode } = {}
): Promise<InstallResult> {
  const agent = agents[agentType];
  const isGlobal = options.global ?? false;
  const cwd = options.cwd || process.cwd();
  const installMode = options.mode ?? 'symlink';

  // Check if agent supports global installation
  if (isGlobal && agent.globalSkillsDir === undefined) {
    return {
      success: false,
      path: '',
      mode: installMode,
      error: `${agent.displayName} does not support global skill installation`,
    };
  }

  // Use installName as the skill directory name
  const skillName = sanitizeName(skill.installName);

  // Canonical location: .agents/skills/<skill-name>
  const canonicalBase = getCanonicalSkillsDir(isGlobal, cwd);
  const canonicalDir = join(canonicalBase, skillName);

  // Agent-specific location (for symlink)
  const agentBase = isGlobal ? agent.globalSkillsDir! : join(cwd, agent.skillsDir);
  const agentDir = join(agentBase, skillName);

  // Validate paths
  if (!isPathSafe(canonicalBase, canonicalDir)) {
    return {
      success: false,
      path: agentDir,
      mode: installMode,
      error: 'Invalid skill name: potential path traversal detected',
    };
  }

  if (!isPathSafe(agentBase, agentDir)) {
    return {
      success: false,
      path: agentDir,
      mode: installMode,
      error: 'Invalid skill name: potential path traversal detected',
    };
  }

  /**
   * Write all skill files to a directory (assumes directory already exists)
   */
  async function writeSkillFiles(targetDir: string): Promise<void> {
    for (const [filePath, content] of skill.files) {
      // Validate file path doesn't escape the target directory
      const fullPath = join(targetDir, filePath);
      if (!isPathSafe(targetDir, fullPath)) {
        continue; // Skip files that would escape the directory
      }

      // Create parent directories if needed
      const parentDir = dirname(fullPath);
      if (parentDir !== targetDir) {
        await mkdir(parentDir, { recursive: true });
      }

      await writeFile(fullPath, content, 'utf-8');
    }
  }

  try {
    // For copy mode, write directly to agent location
    if (installMode === 'copy') {
      await cleanAndCreateDirectory(agentDir);
      await writeSkillFiles(agentDir);

      return {
        success: true,
        path: agentDir,
        mode: 'copy',
      };
    }

    // Symlink mode: write to canonical location and symlink to agent location
    await cleanAndCreateDirectory(canonicalDir);
    await writeSkillFiles(canonicalDir);

    // For universal agents with global install, skip creating agent-specific symlink
    if (isGlobal && isUniversalAgent(agentType)) {
      return {
        success: true,
        path: canonicalDir,
        canonicalPath: canonicalDir,
        mode: 'symlink',
      };
    }

    const symlinkCreated = await createSymlink(canonicalDir, agentDir);

    if (!symlinkCreated) {
      // Symlink failed, fall back to copy
      await cleanAndCreateDirectory(agentDir);
      await writeSkillFiles(agentDir);

      return {
        success: true,
        path: agentDir,
        canonicalPath: canonicalDir,
        mode: 'symlink',
        symlinkFailed: true,
      };
    }

    return {
      success: true,
      path: agentDir,
      canonicalPath: canonicalDir,
      mode: 'symlink',
    };
  } catch (error) {
    return {
      success: false,
      path: agentDir,
      mode: installMode,
      error: error instanceof Error ? error.message : 'Unknown error',
    };
  }
}

export interface InstalledSkill {
  name: string;
  description: string;
  path: string;
  canonicalPath: string;
  scope: 'project' | 'global';
  agents: AgentType[];
}

/**
 * Lists all installed skills from canonical locations
 * @param options - Options for listing skills
 * @returns Array of installed skills with metadata
 */
export async function listInstalledSkills(
  options: {
    global?: boolean;
    cwd?: string;
    agentFilter?: AgentType[];
  } = {}
): Promise<InstalledSkill[]> {
  const cwd = options.cwd || process.cwd();
  // Use a Map to deduplicate skills by scope:name
  const skillsMap: Map<string, InstalledSkill> = new Map();
  const scopes: Array<{ global: boolean; path: string; agentType?: AgentType }> = [];

  // Detect which agents are actually installed
  const detectedAgents = await detectInstalledAgents();
  const agentFilter = options.agentFilter;
  const agentsToCheck = agentFilter
    ? detectedAgents.filter((a) => agentFilter.includes(a))
    : detectedAgents;

  // Determine which scopes to scan
  const scopeTypes: Array<{ global: boolean }> = [];
  if (options.global === undefined) {
    scopeTypes.push({ global: false }, { global: true });
  } else {
    scopeTypes.push({ global: options.global });
  }

  // Build list of directories to scan: canonical + each installed agent's directory
  //
  // Scanning workflow:
  //
  //   detectInstalledAgents()
  //            │
  //            ▼
  //   for each scope (project / global)
  //            │
  //            ├──▶ scan canonical dir ──▶ .agents/skills, ~/.agents/skills
  //            │
  //            ├──▶ scan each installed agent's dir ──▶ .cursor/skills, .claude/skills, ...
  //            │
  //            ▼
  //   deduplicate by skill name
  //
  // Trade-off: More readdir() calls, but most non-existent dirs fail fast.
  // Skills in agent-specific dirs skip the expensive "check all agents" loop.
  //
  for (const { global: isGlobal } of scopeTypes) {
    // Add canonical directory
    scopes.push({ global: isGlobal, path: getCanonicalSkillsDir(isGlobal, cwd) });

    // Add each installed agent's skills directory
    for (const agentType of agentsToCheck) {
      const agent = agents[agentType];
      if (isGlobal && agent.globalSkillsDir === undefined) {
        continue;
      }
      const agentDir = isGlobal ? agent.globalSkillsDir! : join(cwd, agent.skillsDir);
      // Avoid duplicate paths
      if (!scopes.some((s) => s.path === agentDir && s.global === isGlobal)) {
        scopes.push({ global: isGlobal, path: agentDir, agentType });
      }
    }
  }

  for (const scope of scopes) {
    try {
      const entries = await readdir(scope.path, { withFileTypes: true });

      for (const entry of entries) {
        if (!entry.isDirectory()) {
          continue;
        }

        const skillDir = join(scope.path, entry.name);
        const skillMdPath = join(skillDir, 'SKILL.md');

        // Check if SKILL.md exists
        try {
          await stat(skillMdPath);
        } catch {
          // SKILL.md doesn't exist, skip this directory
          continue;
        }

        // Parse the skill
        const skill = await parseSkillMd(skillMdPath);
        if (!skill) {
          continue;
        }

        const scopeKey = scope.global ? 'global' : 'project';
        const skillKey = `${scopeKey}:${skill.name}`;

        // If scanning an agent-specific directory, attribute directly to that agent
        if (scope.agentType) {
          if (skillsMap.has(skillKey)) {
            const existing = skillsMap.get(skillKey)!;
            if (!existing.agents.includes(scope.agentType)) {
              existing.agents.push(scope.agentType);
            }
          } else {
            skillsMap.set(skillKey, {
              name: skill.name,
              description: skill.description,
              path: skillDir,
              canonicalPath: skillDir,
              scope: scopeKey,
              agents: [scope.agentType],
            });
          }
          continue;
        }

        // For canonical directory, check which agents have this skill
        const sanitizedSkillName = sanitizeName(skill.name);
        const installedAgents: AgentType[] = [];

        for (const agentType of agentsToCheck) {
          const agent = agents[agentType];

          if (scope.global && agent.globalSkillsDir === undefined) {
            continue;
          }

          const agentBase = scope.global ? agent.globalSkillsDir! : join(cwd, agent.skillsDir);
          let found = false;

          // Try exact directory name matches
          const possibleNames = Array.from(
            new Set([
              entry.name,
              sanitizedSkillName,
              skill.name
                .toLowerCase()
                .replace(/\s+/g, '-')
                .replace(/[\/\\:\0]/g, ''),
            ])
          );

          for (const possibleName of possibleNames) {
            const agentSkillDir = join(agentBase, possibleName);
            if (!isPathSafe(agentBase, agentSkillDir)) {continue;}

            try {
              await access(agentSkillDir);
              found = true;
              break;
            } catch {
              // Try next name
            }
          }

          // Fallback: scan all directories and check SKILL.md files
          // Handles cases where directory names don't match (e.g., "git-review" vs "Git Review Before Commit")
          if (!found) {
            try {
              const agentEntries = await readdir(agentBase, { withFileTypes: true });
              for (const agentEntry of agentEntries) {
                if (!agentEntry.isDirectory()) {continue;}

                const candidateDir = join(agentBase, agentEntry.name);
                if (!isPathSafe(agentBase, candidateDir)) {continue;}

                try {
                  const candidateSkillMd = join(candidateDir, 'SKILL.md');
                  await stat(candidateSkillMd);
                  const candidateSkill = await parseSkillMd(candidateSkillMd);
                  if (candidateSkill && candidateSkill.name === skill.name) {
                    found = true;
                    break;
                  }
                } catch {
                  // Not a valid skill directory
                }
              }
            } catch {
              // Agent base directory doesn't exist
            }
          }

          if (found) {
            installedAgents.push(agentType);
          }
        }

        if (skillsMap.has(skillKey)) {
          // Merge agents
          const existing = skillsMap.get(skillKey)!;
          for (const agent of installedAgents) {
            if (!existing.agents.includes(agent)) {
              existing.agents.push(agent);
            }
          }
        } else {
          skillsMap.set(skillKey, {
            name: skill.name,
            description: skill.description,
            path: skillDir,
            canonicalPath: skillDir,
            scope: scopeKey,
            agents: installedAgents,
          });
        }
      }
    } catch {
      // Directory doesn't exist, skip
    }
  }

  return Array.from(skillsMap.values());
}

async function findSkillDirectoriesByName(basePath: string, skillName: string): Promise<string[]> {
  try {
    const entries = await readdir(basePath, { withFileTypes: true });
    const matchingDirectories: string[] = [];

    for (const entry of entries) {
      if (!entry.isDirectory()) {
        continue;
      }

      const skillDir = join(basePath, entry.name);
      if (!isPathSafe(basePath, skillDir)) {
        continue;
      }

      const skillMdPath = join(skillDir, 'SKILL.md');
      try {
        await stat(skillMdPath);
      } catch {
        continue;
      }

      const parsedSkill = await parseSkillMd(skillMdPath, { includeInternal: true });
      if (parsedSkill?.name === skillName) {
        matchingDirectories.push(skillDir);
      }
    }

    return matchingDirectories;
  } catch {
    return [];
  }
}

async function removeSkillDirectory(path: string): Promise<void> {
  try {
    const current = await lstat(path);
    if (current.isSymbolicLink()) {
      await rm(path, { force: true });
      return;
    }

    await rm(path, { recursive: true, force: true });
  } catch (error) {
    if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
      return;
    }
    throw error;
  }
}

async function listAgentsWithInstalledSkill(
  skillName: string,
  options: { global: boolean; cwd: string }
): Promise<AgentType[]> {
  const detectedAgents = await detectInstalledAgents();
  const installedAgents: AgentType[] = [];

  for (const agentType of detectedAgents) {
    const agent = agents[agentType];
    if (options.global && agent.globalSkillsDir === undefined) {
      continue;
    }

    const basePath = options.global ? agent.globalSkillsDir! : join(options.cwd, agent.skillsDir);
    const matchingDirs = await findSkillDirectoriesByName(basePath, skillName);
    if (matchingDirs.length > 0) {
      installedAgents.push(agentType);
    }
  }

  return installedAgents;
}

export interface UninstallRecord {
  agent: AgentType;
  paths: string[];
  error?: string;
}

export interface SkillsUninstallResult {
  skillName: string;
  scope: 'project' | 'global';
  removed: UninstallRecord[];
  failed: UninstallRecord[];
  remainingAgents: AgentType[];
}

export async function uninstallInstalledSkill(
  options: {
    skillName: string;
    agents: AgentType[];
    global?: boolean;
    cwd?: string;
  }
): Promise<SkillsUninstallResult> {
  const scope = options.global ? 'global' : 'project';
  const cwd = options.cwd || process.cwd();
  const requestedAgents = Array.from(new Set(options.agents));

  const discoveryErrors = new Map<AgentType, string>();
  const agentToPaths = new Map<AgentType, string[]>();
  const pathOwners = new Map<string, AgentType[]>();

  for (const agentType of requestedAgents) {
    const agent = agents[agentType];
    if (!agent) {
      discoveryErrors.set(agentType, `Unknown agent: ${agentType}`);
      continue;
    }

    if (options.global && agent.globalSkillsDir === undefined) {
      discoveryErrors.set(
        agentType,
        `${agent.displayName} does not support global skill uninstallation`
      );
      continue;
    }

    const basePath = options.global ? agent.globalSkillsDir! : join(cwd, agent.skillsDir);
    const matchingPaths = await findSkillDirectoriesByName(basePath, options.skillName);
    agentToPaths.set(agentType, matchingPaths);

    for (const path of matchingPaths) {
      const owners = pathOwners.get(path) ?? [];
      if (!owners.includes(agentType)) {
        owners.push(agentType);
      }
      pathOwners.set(path, owners);
    }
  }

  const removalErrors = new Map<string, string>();
  for (const path of pathOwners.keys()) {
    try {
      await removeSkillDirectory(path);
    } catch (error) {
      removalErrors.set(path, error instanceof Error ? error.message : 'Failed to remove skill path');
    }
  }

  const removed: UninstallRecord[] = [];
  const failed: UninstallRecord[] = [];

  for (const agentType of requestedAgents) {
    const paths = agentToPaths.get(agentType) ?? [];
    const discoveryError = discoveryErrors.get(agentType);
    if (discoveryError) {
      failed.push({
        agent: agentType,
        paths,
        error: discoveryError,
      });
      continue;
    }

    const failedPath = paths.find((path) => removalErrors.has(path));
    if (failedPath) {
      failed.push({
        agent: agentType,
        paths,
        error: removalErrors.get(failedPath),
      });
      continue;
    }

    removed.push({
      agent: agentType,
      paths,
    });
  }

  const canonicalBase = getCanonicalSkillsDir(options.global ?? false, cwd);
  const canonicalMatches = await findSkillDirectoriesByName(canonicalBase, options.skillName);
  const remainingAgents = await listAgentsWithInstalledSkill(options.skillName, {
    global: options.global ?? false,
    cwd,
  });

  if (remainingAgents.length === 0) {
    for (const canonicalPath of canonicalMatches) {
      try {
        await removeSkillDirectory(canonicalPath);
      } catch {
        // Canonical cleanup is best-effort once all agent references are gone.
      }
    }
  }

  return {
    skillName: options.skillName,
    scope,
    removed,
    failed,
    remainingAgents,
  };
}
