import { Command } from "commander";
import pc from "picocolors";
import ora from "ora";
import { readdir, rm } from "fs/promises";
import { join } from "path";

import { parseSkillInput } from "../utils/parse-input.js";
import {
  listProjectSkills,
  searchSkills,
  suggestSkills,
  downloadSkill,
  getSkill,
} from "../utils/api.js";
import { log } from "../utils/logger.js";
import {
  promptForInstallTargets,
  promptForSingleTarget,
  getTargetDirs,
  getTargetDirFromSelection,
  getSelectedIdes,
  hasExplicitIdeOption,
} from "../utils/ide.js";
import {
  checkboxWithHover,
  terminalLink,
  formatPopularity,
  formatTrust,
  formatInstallRange,
  getTrustLabel,
} from "../utils/prompts.js";
import { installSkillFiles, symlinkSkill } from "../utils/installer.js";
import { assertSkillNameInRoot } from "../utils/skill-name.js";
import { listSkillsFromGitHub, getSkillFromGitHub } from "../utils/github.js";
import { trackEvent } from "../utils/tracking.js";
import { registerGenerateCommand } from "./generate.js";
import type {
  Skill,
  SkillSearchResult,
  AddOptions,
  ListOptions,
  RemoveOptions,
  SuggestOptions,
  InstallTargets,
  Scope,
} from "../types.js";
import {
  IDE_NAMES,
  IDE_PATHS,
  IDE_GLOBAL_PATHS,
  UNIVERSAL_SKILLS_PATH,
  UNIVERSAL_SKILLS_GLOBAL_PATH,
  UNIVERSAL_AGENTS_LABEL,
  VENDOR_SPECIFIC_AGENTS,
} from "../types.js";
import { homedir } from "os";
import { detectProjectDependencies } from "../utils/deps.js";
import { loadTokens, isTokenExpired } from "../utils/auth.js";

function logInstallSummary(
  targets: InstallTargets,
  targetDirs: string[],
  skillNames: string[]
): void {
  log.blank();
  const hasUniversal = targets.ides.some((ide) => ide === "universal");
  const vendorIdes = targets.ides.filter((ide) => ide !== "universal");

  let dirIndex = 0;
  if (hasUniversal && dirIndex < targetDirs.length) {
    log.plain(`${pc.bold("Universal")} ${pc.dim(targetDirs[dirIndex])}`);
    for (const name of skillNames) {
      log.itemAdd(name);
    }
    dirIndex++;
  }

  for (const ide of vendorIdes) {
    if (dirIndex >= targetDirs.length) break;
    log.plain(`${pc.bold(IDE_NAMES[ide])} ${pc.dim(targetDirs[dirIndex])}`);
    for (const name of skillNames) {
      log.itemAdd(name);
    }
    dirIndex++;
  }

  log.blank();
}

export function registerSkillCommands(program: Command): void {
  const skill = program.command("skills").alias("skill").description("Manage AI coding skills");

  // Register generate subcommand
  registerGenerateCommand(skill);

  skill
    .command("install")
    .alias("i")
    .alias("add")
    .argument("<repository>", "GitHub repository (/owner/repo)")
    .argument("[skill]", "Specific skill name to install")
    .option("--all", "Install all skills without prompting")
    .option("--all-agents", "Install to all supported agent locations")
    .option("-y, --yes", "Skip confirmation prompts")
    .option("--global", "Install globally instead of current directory")
    .option("--claude", "Claude Code (.claude/skills/)")
    .option("--cursor", "Cursor (.cursor/skills/)")
    .option("--universal", "Universal (.agents/skills/)")
    .option("--antigravity", "Antigravity (.agent/skills/)")
    .description("Install skills from a repository")
    .action(async (project: string, skillName: string | undefined, options: AddOptions) => {
      await installCommand(project, skillName, options);
    });

  skill
    .command("search")
    .alias("s")
    .argument("<keywords...>", "Search keywords")
    .description("Search for skills across all indexed repositories")
    .action(async (keywords: string[]) => {
      await searchCommand(keywords.join(" "));
    });

  skill
    .command("list")
    .alias("ls")
    .option("--global", "List global skills")
    .option("--claude", "Claude Code (.claude/skills/)")
    .option("--cursor", "Cursor (.cursor/skills/)")
    .option("--universal", "Universal (.agents/skills/)")
    .option("--antigravity", "Antigravity (.agent/skills/)")
    .description("List installed skills")
    .action(async (options: ListOptions) => {
      await listCommand(options);
    });

  skill
    .command("remove")
    .alias("rm")
    .alias("delete")
    .argument("<name>", "Skill name to remove")
    .option("--global", "Remove from global skills")
    .option("--claude", "Claude Code (.claude/skills/)")
    .option("--cursor", "Cursor (.cursor/skills/)")
    .option("--universal", "Universal (.agents/skills/)")
    .option("--antigravity", "Antigravity (.agent/skills/)")
    .description("Remove an installed skill")
    .action(async (name: string, options: RemoveOptions) => {
      await removeCommand(name, options);
    });

  skill
    .command("info")
    .argument("<repository>", "GitHub repository (/owner/repo)")
    .description("Show skills in a repository")
    .action(async (project: string) => {
      await infoCommand(project);
    });

  skill
    .command("suggest")
    .option("--global", "Install globally instead of current directory")
    .option("--claude", "Claude Code (.claude/skills/)")
    .option("--cursor", "Cursor (.cursor/skills/)")
    .option("--universal", "Universal (.agents/skills/)")
    .option("--antigravity", "Antigravity (.agent/skills/)")
    .description("Suggest skills based on your project dependencies")
    .action(async (options: SuggestOptions) => {
      await suggestCommand(options);
    });
}

export function registerSkillAliases(program: Command): void {
  program
    .command("si", { hidden: true })
    .argument("<repository>", "GitHub repository (/owner/repo)")
    .argument("[skill]", "Specific skill name to install")
    .option("--all", "Install all skills without prompting")
    .option("--all-agents", "Install to all supported agent locations")
    .option("-y, --yes", "Skip confirmation prompts")
    .option("--global", "Install globally instead of current directory")
    .option("--claude", "Claude Code (.claude/skills/)")
    .option("--cursor", "Cursor (.cursor/skills/)")
    .option("--universal", "Universal (.agents/skills/)")
    .option("--antigravity", "Antigravity (.agent/skills/)")
    .description("Install skills (alias for: skills install)")
    .action(async (project: string, skillName: string | undefined, options: AddOptions) => {
      await installCommand(project, skillName, options);
    });

  program
    .command("ss", { hidden: true })
    .argument("<keywords...>", "Search keywords")
    .description("Search for skills (alias for: skills search)")
    .action(async (keywords: string[]) => {
      await searchCommand(keywords.join(" "));
    });

  program
    .command("ssg", { hidden: true })
    .option("--global", "Install globally instead of current directory")
    .option("--claude", "Claude Code (.claude/skills/)")
    .option("--cursor", "Cursor (.cursor/skills/)")
    .option("--universal", "Universal (.agents/skills/)")
    .option("--antigravity", "Antigravity (.agent/skills/)")
    .description("Suggest skills (alias for: skills suggest)")
    .action(async (options: SuggestOptions) => {
      await suggestCommand(options);
    });
}

async function installCommand(
  input: string,
  skillName: string | undefined,
  options: AddOptions
): Promise<void> {
  trackEvent("command", { name: "install" });
  const parsed = parseSkillInput(input);
  if (!parsed) {
    log.error(`Invalid input format: ${input}`);
    log.info(`Expected: /owner/repo or full GitHub URL`);
    log.info(`Example: ctx7 skills install /anthropics/skills pdf`);
    log.blank();
    return;
  }
  const repo = `/${parsed.owner}/${parsed.repo}`;

  log.blank();
  const spinner = ora(`Fetching skills from ${repo}...`).start();

  let selectedSkills: (Skill & { project: string })[];

  // When a specific skill name is provided, fetch only that skill
  if (skillName) {
    spinner.text = `Fetching skill: ${skillName}...`;
    const skillData = await getSkill(repo, skillName);

    if (skillData.error || !skillData.name) {
      if (skillData.error === "prompt_injection_detected") {
        spinner.fail(pc.red(`Prompt injection detected in skill: ${skillName}`));
        log.warn("This skill contains potentially malicious content and cannot be installed.");
        return;
      }

      spinner.text = `Fetching skill from GitHub: ${skillName}...`;
      const ghResult = await getSkillFromGitHub(repo, skillName);
      if (ghResult.status === "repo_not_found") {
        spinner.fail(pc.red(`Repository not found: ${repo}`));
        return;
      }
      if (ghResult.status !== "ok" || !ghResult.skill) {
        spinner.fail(pc.red(`Skill not found: ${skillName}`));
        return;
      }

      spinner.succeed(`Found skill: ${skillName}`);
      selectedSkills = [ghResult.skill];
    } else {
      spinner.succeed(`Found skill: ${skillName}`);
      selectedSkills = [
        {
          name: skillData.name,
          description: skillData.description,
          url: skillData.url,
          project: repo,
        },
      ];
    }
  } else {
    // Fetch all skills when no specific names provided
    let data = await listProjectSkills(repo);

    if ((data.error || !data.skills || data.skills.length === 0) && !data.blockedSkillsCount) {
      spinner.text = `Fetching skills from GitHub...`;
      const ghResult = await listSkillsFromGitHub(repo);
      if (ghResult.status === "repo_not_found") {
        spinner.fail(pc.red(`Repository not found: ${repo}`));
        return;
      }
      if (ghResult.status === "ok" && ghResult.skills.length > 0) {
        data = { project: repo, skills: ghResult.skills };
      }
    }

    if (data.error && (!data.skills || data.skills.length === 0)) {
      spinner.fail(pc.red(`Error: ${data.message || data.error}`));
      return;
    }

    if (!data.skills || data.skills.length === 0) {
      spinner.warn(pc.yellow(`No skills found in ${repo}`));
      return;
    }

    const skillsWithRepo = data.skills
      .map((s) => ({ ...s, project: repo }))
      .sort((a, b) => (b.installCount ?? 0) - (a.installCount ?? 0));

    spinner.succeed(`Found ${data.skills.length} skill(s)`);

    if (data.blockedSkillsCount && data.blockedSkillsCount > 0) {
      log.blank();
      log.error(
        `${data.blockedSkillsCount} skill(s) blocked due to prompt injection and not shown.`
      );
      log.warn("Review other skills from this repository carefully before installing.");
    }

    if (options.all || data.skills.length === 1) {
      selectedSkills = skillsWithRepo;
    } else {
      const indexWidth = data.skills.length.toString().length;
      const maxNameLen = Math.max(...data.skills.map((s) => s.name.length));
      const popularityColWidth = 13;
      const choices = skillsWithRepo.map((s, index) => {
        const indexStr = pc.dim(`${(index + 1).toString().padStart(indexWidth)}.`);
        const paddedName = s.name.padEnd(maxNameLen);
        const popularity = formatPopularity(s.installCount) + " ".repeat(popularityColWidth - 4);
        const trust = formatTrust(s.trustScore);

        const skillUrl = `https://context7.com/skills${s.project}/${s.name}`;
        const skillLink = terminalLink(s.name, skillUrl, pc.white);
        const repoLink = terminalLink(s.project, `https://github.com${s.project}`, pc.white);
        const metadataLines = [
          pc.dim("─".repeat(50)),
          "",
          `${pc.yellow("Skill:")}       ${skillLink}`,
          `${pc.yellow("Repo:")}        ${repoLink}`,
          `${pc.yellow("Installs:")}    ${pc.white(formatInstallRange(s.installCount))}`,
          `${pc.yellow("Trust:")}       ${s.trustScore !== undefined && s.trustScore >= 0 ? pc.white(s.trustScore.toFixed(1)) : pc.dim("-")}`,
          `${pc.yellow("Description:")}`,
          pc.white(s.description || "No description"),
        ];

        return {
          name: `${indexStr} ${paddedName} ${popularity}${trust}`,
          value: s,
          description: metadataLines.join("\n"),
        };
      });

      log.blank();

      const checkboxPrefixWidth = 3;
      const headerPad = " ".repeat(checkboxPrefixWidth + indexWidth + 1 + 1 + maxNameLen + 1);
      const headerLine =
        headerPad + pc.dim("Popularity".padEnd(popularityColWidth)) + pc.dim("Trust");

      try {
        selectedSkills = await checkboxWithHover({
          message: `Select skills to install:\n${headerLine}`,
          choices,
          pageSize: 15,
          loop: false,
          theme: {
            style: {
              message: (text: string, status: string) => {
                if (status === "done") return pc.dim(text.split("\n")[0]);
                return pc.bold(text);
              },
            },
          },
        });
      } catch {
        log.warn("Installation cancelled");
        return;
      }
    }
  }

  if (selectedSkills.length === 0) {
    log.warn("No skills selected");
    return;
  }

  const targets = await promptForInstallTargets(options);
  if (!targets) {
    log.warn("Installation cancelled");
    return;
  }

  const targetDirs = getTargetDirs(targets);

  const installSpinner = ora("Installing skills...").start();

  let permissionError = false;
  const failedDirs: Set<string> = new Set();
  const installedSkills: string[] = [];

  for (const skill of selectedSkills) {
    try {
      installSpinner.text = `Downloading ${skill.name}...`;
      const downloadData = await downloadSkill(skill.project, skill.name);

      if (downloadData.error) {
        log.warn(`Failed to download ${skill.name}: ${downloadData.error}`);
        continue;
      }

      installSpinner.text = `Installing ${skill.name}...`;

      const [primaryDir, ...symlinkDirs] = targetDirs;

      try {
        await installSkillFiles(skill.name, downloadData.files, primaryDir);
      } catch (dirErr) {
        const error = dirErr as NodeJS.ErrnoException;
        if (error.code === "EACCES" || error.code === "EPERM") {
          permissionError = true;
          failedDirs.add(primaryDir);
        }
        throw dirErr;
      }

      const primarySkillDir = join(primaryDir, skill.name);
      for (const targetDir of symlinkDirs) {
        try {
          await symlinkSkill(skill.name, primarySkillDir, targetDir);
        } catch (dirErr) {
          const error = dirErr as NodeJS.ErrnoException;
          if (error.code === "EACCES" || error.code === "EPERM") {
            permissionError = true;
            failedDirs.add(targetDir);
          }
          throw dirErr;
        }
      }

      installedSkills.push(`${skill.project}/${skill.name}`);
    } catch (err) {
      const error = err as NodeJS.ErrnoException;
      if (error.code === "EACCES" || error.code === "EPERM") {
        continue;
      }
      const errMsg = err instanceof Error ? err.message : String(err);
      log.warn(`Failed to install ${skill.name}: ${errMsg}`);
    }
  }

  if (permissionError) {
    installSpinner.fail("Permission denied");
    log.blank();
    log.warn("Fix permissions with:");
    for (const dir of failedDirs) {
      const parentDir = join(dir, "..");
      log.dim(`  sudo chown -R $(whoami) "${parentDir}"`);
    }
    log.blank();
    return;
  }

  installSpinner.succeed(`Installed ${installedSkills.length} skill(s)`);
  trackEvent("install", { skills: installedSkills, ides: targets.ides });

  const installedNames = selectedSkills.map((s) => s.name);
  logInstallSummary(targets, targetDirs, installedNames);
}

async function searchCommand(query: string): Promise<void> {
  trackEvent("command", { name: "search" });
  log.blank();
  const spinner = ora(`Searching for "${query}"...`).start();

  let data;
  try {
    data = await searchSkills(query);
  } catch (err) {
    spinner.fail(pc.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
    return;
  }

  if (data.error) {
    spinner.fail(pc.red(`Error: ${data.message || data.error}`));
    return;
  }

  if (!data.results || data.results.length === 0) {
    spinner.warn(pc.yellow(`No skills found matching "${query}"`));
    return;
  }

  spinner.succeed(`Found ${data.results.length} skill(s)`);
  trackEvent("search_query", { query, resultCount: data.results.length });
  log.blank();

  const indexWidth = data.results.length.toString().length;
  const nameWithRepo = (s: SkillSearchResult) => `${s.name} ${pc.dim(`(${s.project})`)}`;
  const nameWithRepoLen = (s: SkillSearchResult) => `${s.name} (${s.project})`.length;
  const maxNameLen = Math.max(...data.results.map(nameWithRepoLen));
  const popularityColWidth = 13;
  const choices = data.results.map((s, index) => {
    const indexStr = pc.dim(`${(index + 1).toString().padStart(indexWidth)}.`);
    const rawLen = nameWithRepoLen(s);
    const displayName = nameWithRepo(s) + " ".repeat(maxNameLen - rawLen);
    const popularity = formatPopularity(s.installCount) + " ".repeat(popularityColWidth - 4);
    const trust = formatTrust(s.trustScore);

    const skillLink = terminalLink(
      s.name,
      `https://context7.com/skills${s.project}/${s.name}`,
      pc.white
    );
    const repoLink = terminalLink(s.project, `https://github.com${s.project}`, pc.white);
    const metadataLines = [
      pc.dim("─".repeat(50)),
      "",
      `${pc.yellow("Skill:")}       ${skillLink}`,
      `${pc.yellow("Repo:")}        ${repoLink}`,
      `${pc.yellow("Installs:")}    ${pc.white(formatInstallRange(s.installCount))}`,
      `${pc.yellow("Trust:")}       ${s.trustScore !== undefined && s.trustScore >= 0 ? pc.white(s.trustScore.toFixed(1)) : pc.dim("-")}`,
      `${pc.yellow("Description:")}`,
      pc.white(s.description || "No description"),
    ];

    return {
      name: `${indexStr} ${displayName} ${popularity}${trust}`,
      value: s,
      description: metadataLines.join("\n"),
    };
  });

  const checkboxPrefixWidth = 3; // "❯◯ " or " ◯ "
  const headerPad = " ".repeat(checkboxPrefixWidth + indexWidth + 1 + 1 + maxNameLen + 1);
  const headerLine = headerPad + pc.dim("Popularity".padEnd(popularityColWidth)) + pc.dim("Trust");

  let selectedSkills: SkillSearchResult[];
  try {
    selectedSkills = await checkboxWithHover({
      message: `Select skills to install:\n${headerLine}`,
      choices,
      pageSize: 15,
      loop: false,
      theme: {
        style: {
          message: (text: string, status: string) => {
            if (status === "done") return pc.dim(text.split("\n")[0]);
            return pc.bold(text);
          },
        },
      },
    });
  } catch {
    log.warn("Installation cancelled");
    return;
  }

  const uniqueSkills = selectedSkills;

  if (uniqueSkills.length === 0) {
    log.warn("No skills selected");
    return;
  }

  const targets = await promptForInstallTargets({});
  if (!targets) {
    log.warn("Installation cancelled");
    return;
  }

  const targetDirs = getTargetDirs(targets);

  const installSpinner = ora("Installing skills...").start();

  let permissionError = false;
  const failedDirs: Set<string> = new Set();
  const installedSkills: string[] = [];

  for (const skill of uniqueSkills) {
    try {
      installSpinner.text = `Downloading ${skill.name}...`;
      const downloadData = await downloadSkill(skill.project, skill.name);

      if (downloadData.error) {
        log.warn(`Failed to download ${skill.name}: ${downloadData.error}`);
        continue;
      }

      installSpinner.text = `Installing ${skill.name}...`;

      const [primaryDir, ...symlinkDirs] = targetDirs;

      try {
        await installSkillFiles(skill.name, downloadData.files, primaryDir);
      } catch (dirErr) {
        const error = dirErr as NodeJS.ErrnoException;
        if (error.code === "EACCES" || error.code === "EPERM") {
          permissionError = true;
          failedDirs.add(primaryDir);
        }
        throw dirErr;
      }

      const primarySkillDir = join(primaryDir, skill.name);
      for (const targetDir of symlinkDirs) {
        try {
          await symlinkSkill(skill.name, primarySkillDir, targetDir);
        } catch (dirErr) {
          const error = dirErr as NodeJS.ErrnoException;
          if (error.code === "EACCES" || error.code === "EPERM") {
            permissionError = true;
            failedDirs.add(targetDir);
          }
          throw dirErr;
        }
      }

      installedSkills.push(`${skill.project}/${skill.name}`);
    } catch (err) {
      const error = err as NodeJS.ErrnoException;
      if (error.code === "EACCES" || error.code === "EPERM") {
        continue;
      }
      const errMsg = err instanceof Error ? err.message : String(err);
      log.warn(`Failed to install ${skill.name}: ${errMsg}`);
    }
  }

  if (permissionError) {
    installSpinner.fail("Permission denied");
    log.blank();
    log.warn("Fix permissions with:");
    for (const dir of failedDirs) {
      const parentDir = join(dir, "..");
      log.dim(`  sudo chown -R $(whoami) "${parentDir}"`);
    }
    log.blank();
    return;
  }

  installSpinner.succeed(`Installed ${installedSkills.length} skill(s)`);
  trackEvent("install", { skills: installedSkills, ides: targets.ides });

  const installedNames = uniqueSkills.map((s) => s.name);
  logInstallSummary(targets, targetDirs, installedNames);
}

async function listCommand(options: ListOptions): Promise<void> {
  trackEvent("command", { name: "list" });
  const scope: Scope = options.global ? "global" : "project";
  const baseDir = scope === "global" ? homedir() : process.cwd();

  const results: { label: string; path: string; skills: string[] }[] = [];

  // Helper to scan a skills directory
  async function scanDir(dir: string): Promise<string[]> {
    try {
      const entries = await readdir(dir, { withFileTypes: true });
      return entries.filter((e) => e.isDirectory() || e.isSymbolicLink()).map((e) => e.name);
    } catch {
      return [];
    }
  }

  if (hasExplicitIdeOption(options)) {
    // Explicit flag mode — check the specific IDE paths
    const ides = getSelectedIdes(options);
    for (const ide of ides) {
      const dir =
        ide === "universal"
          ? join(baseDir, scope === "global" ? UNIVERSAL_SKILLS_GLOBAL_PATH : UNIVERSAL_SKILLS_PATH)
          : join(baseDir, (scope === "global" ? IDE_GLOBAL_PATHS : IDE_PATHS)[ide]);
      const label = ide === "universal" ? UNIVERSAL_AGENTS_LABEL : IDE_NAMES[ide];
      const skills = await scanDir(dir);
      if (skills.length > 0) {
        results.push({ label, path: dir, skills });
      }
    }
  } else {
    // Default: check universal + vendor-specific
    const universalPath = scope === "global" ? UNIVERSAL_SKILLS_GLOBAL_PATH : UNIVERSAL_SKILLS_PATH;
    const universalDir = join(baseDir, universalPath);
    const universalSkills = await scanDir(universalDir);
    if (universalSkills.length > 0) {
      results.push({ label: UNIVERSAL_AGENTS_LABEL, path: universalPath, skills: universalSkills });
    }

    for (const ide of VENDOR_SPECIFIC_AGENTS) {
      const pathMap = scope === "global" ? IDE_GLOBAL_PATHS : IDE_PATHS;
      const dir = join(baseDir, pathMap[ide]);
      const skills = await scanDir(dir);
      if (skills.length > 0) {
        results.push({ label: IDE_NAMES[ide], path: pathMap[ide], skills });
      }
    }
  }

  if (results.length === 0) {
    log.warn("No skills installed");
    return;
  }

  log.blank();

  for (const { label, path, skills } of results) {
    log.plain(`${pc.bold(label)} ${pc.dim(path)}`);
    for (const skill of skills) {
      log.plain(`  ${pc.green(skill)}`);
    }
    log.blank();
  }
}

async function removeCommand(name: string, options: RemoveOptions): Promise<void> {
  trackEvent("command", { name: "remove" });
  const target = await promptForSingleTarget(options);
  if (!target) {
    log.warn("Cancelled");
    return;
  }

  const skillsDir = getTargetDirFromSelection(target.ide, target.scope);
  let skillPath: string;
  try {
    skillPath = assertSkillNameInRoot(skillsDir, name);
  } catch {
    log.error(`Invalid skill name: ${name}`);
    return;
  }

  try {
    await rm(skillPath, { recursive: true });
    log.success(`Removed skill: ${name}`);
  } catch (err) {
    const error = err as NodeJS.ErrnoException;
    if (error.code === "ENOENT") {
      log.error(`Skill not found: ${name}`);
    } else if (error.code === "EACCES" || error.code === "EPERM") {
      log.error(`Permission denied. Try: sudo rm -rf "${skillPath}"`);
    } else {
      log.error(`Failed to remove skill: ${error.message}`);
    }
  }
}

async function infoCommand(input: string): Promise<void> {
  trackEvent("command", { name: "info" });
  const parsed = parseSkillInput(input);
  if (!parsed) {
    log.blank();
    log.error(`Invalid input format: ${input}`);
    log.info(`Expected: /owner/repo or full GitHub URL`);
    log.blank();
    return;
  }
  const repo = `/${parsed.owner}/${parsed.repo}`;

  log.blank();
  const spinner = ora(`Fetching skills from ${repo}...`).start();

  const data = await listProjectSkills(repo);

  if (data.error) {
    spinner.fail(pc.red(`Error: ${data.message || data.error}`));
    return;
  }

  if (!data.skills || data.skills.length === 0) {
    spinner.warn(pc.yellow(`No skills found in ${repo}`));
    return;
  }

  spinner.succeed(`Found ${data.skills.length} skill(s)`);

  log.blank();
  for (const skill of data.skills) {
    log.item(skill.name);
    log.dim(`    ${skill.description || "No description"}`);
    log.dim(`    URL: ${skill.url}`);
    log.blank();
  }

  log.plain(
    `${pc.bold("Quick commands:")}\n` +
      `  Install all: ${pc.cyan(`ctx7 skills install ${repo} --all`)}\n` +
      `  Install one: ${pc.cyan(`ctx7 skills install ${repo} ${data.skills[0]?.name}`)}\n`
  );
}

async function suggestCommand(options: SuggestOptions): Promise<void> {
  trackEvent("command", { name: "suggest" });
  log.blank();

  // Step 1: Detect dependencies
  const scanSpinner = ora("Scanning project dependencies...").start();
  const deps = await detectProjectDependencies(process.cwd());

  if (deps.length === 0) {
    scanSpinner.warn(pc.yellow("No dependencies detected"));
    log.info(`Try ${pc.cyan("ctx7 skills search <keyword>")} to search manually`);
    return;
  }

  scanSpinner.succeed(`Found ${deps.length} dependencies`);

  // Step 2: Single API call to backend
  const searchSpinner = ora("Finding matching skills...").start();

  const tokens = loadTokens();
  const accessToken = tokens && !isTokenExpired(tokens) ? tokens.access_token : undefined;

  let data;
  try {
    data = await suggestSkills(deps, accessToken);
  } catch {
    searchSpinner.fail(pc.red("Failed to connect to Context7"));
    return;
  }

  if (data.error) {
    searchSpinner.fail(pc.red(`Error: ${data.message || data.error}`));
    return;
  }

  const skills = data.skills;

  if (skills.length === 0) {
    searchSpinner.warn(pc.yellow("No matching skills found for your dependencies"));
    return;
  }

  searchSpinner.succeed(`Found ${skills.length} relevant skill(s)`);
  trackEvent("suggest_results", { depCount: deps.length, skillCount: skills.length });
  log.blank();

  const nameWithRepo = (s: SkillSearchResult) => `${s.name} ${pc.dim(`(${s.project})`)}`;
  const nameWithRepoLen = (s: SkillSearchResult) => `${s.name} (${s.project})`.length;
  const maxNameLen = Math.max(...skills.map(nameWithRepoLen));
  const popularityColWidth = 13;
  const trustColWidth = 8;
  const maxMatchedLen = Math.max(...skills.map((s) => s.matchedDep.length));
  const indexWidth = skills.length.toString().length;

  const choices = skills.map((s, index) => {
    const indexStr = pc.dim(`${(index + 1).toString().padStart(indexWidth)}.`);
    const rawLen = nameWithRepoLen(s);
    const displayName = nameWithRepo(s) + " ".repeat(maxNameLen - rawLen);
    const popularity = formatPopularity(s.installCount) + " ".repeat(popularityColWidth - 4);
    const trustLabel = getTrustLabel(s.trustScore);
    const trust = formatTrust(s.trustScore) + " ".repeat(trustColWidth - trustLabel.length);
    const matched = pc.yellow(s.matchedDep.padEnd(maxMatchedLen));

    const skillLink = terminalLink(
      s.name,
      `https://context7.com/skills${s.project}/${s.name}`,
      pc.white
    );
    const repoLink = terminalLink(s.project, `https://github.com${s.project}`, pc.white);
    const metadataLines = [
      pc.dim("─".repeat(50)),
      "",
      `${pc.yellow("Skill:")}       ${skillLink}`,
      `${pc.yellow("Repo:")}        ${repoLink}`,
      `${pc.yellow("Installs:")}    ${pc.white(formatInstallRange(s.installCount))}`,
      `${pc.yellow("Trust:")}       ${s.trustScore !== undefined && s.trustScore >= 0 ? pc.white(s.trustScore.toFixed(1)) : pc.dim("-")}`,
      `${pc.yellow("Relevant:")}    ${pc.white(s.matchedDep)}`,
      `${pc.yellow("Description:")}`,
      pc.white(s.description || "No description"),
    ];

    return {
      name: `${indexStr} ${displayName} ${popularity}${trust}${matched}`,
      value: s,
      description: metadataLines.join("\n"),
    };
  });

  const checkboxPrefixWidth = 3; // "❯◯ " or " ◯ "
  const headerPad = " ".repeat(checkboxPrefixWidth + indexWidth + 1 + 1 + maxNameLen + 1);
  const headerLine =
    headerPad +
    pc.dim("Popularity".padEnd(popularityColWidth)) +
    pc.dim("Trust".padEnd(trustColWidth)) +
    pc.dim("Relevant");

  let selectedSkills: SkillSearchResult[];
  try {
    selectedSkills = await checkboxWithHover({
      message: `Select skills to install:\n${headerLine}`,
      choices,
      pageSize: 15,
      loop: false,
      theme: {
        style: {
          message: (text: string, status: string) => {
            if (status === "done") return pc.dim(text.split("\n")[0]);
            return pc.bold(text);
          },
        },
      },
    });
  } catch {
    log.warn("Installation cancelled");
    return;
  }

  if (selectedSkills.length === 0) {
    log.warn("No skills selected");
    return;
  }

  // Step 4: Install (same pattern as searchCommand)
  const targets = await promptForInstallTargets(options);
  if (!targets) {
    log.warn("Installation cancelled");
    return;
  }

  const targetDirs = getTargetDirs(targets);
  const installSpinner = ora("Installing skills...").start();

  let permissionError = false;
  const failedDirs: Set<string> = new Set();
  const installedSkills: string[] = [];

  for (const skill of selectedSkills) {
    try {
      installSpinner.text = `Downloading ${skill.name}...`;
      const downloadData = await downloadSkill(skill.project, skill.name);

      if (downloadData.error) {
        log.warn(`Failed to download ${skill.name}: ${downloadData.error}`);
        continue;
      }

      installSpinner.text = `Installing ${skill.name}...`;

      const [primaryDir, ...symlinkDirs] = targetDirs;

      try {
        await installSkillFiles(skill.name, downloadData.files, primaryDir);
      } catch (dirErr) {
        const error = dirErr as NodeJS.ErrnoException;
        if (error.code === "EACCES" || error.code === "EPERM") {
          permissionError = true;
          failedDirs.add(primaryDir);
        }
        throw dirErr;
      }

      const primarySkillDir = join(primaryDir, skill.name);
      for (const targetDir of symlinkDirs) {
        try {
          await symlinkSkill(skill.name, primarySkillDir, targetDir);
        } catch (dirErr) {
          const error = dirErr as NodeJS.ErrnoException;
          if (error.code === "EACCES" || error.code === "EPERM") {
            permissionError = true;
            failedDirs.add(targetDir);
          }
          throw dirErr;
        }
      }

      installedSkills.push(`${skill.project}/${skill.name}`);
    } catch (err) {
      const error = err as NodeJS.ErrnoException;
      if (error.code === "EACCES" || error.code === "EPERM") {
        continue;
      }
      const errMsg = err instanceof Error ? err.message : String(err);
      log.warn(`Failed to install ${skill.name}: ${errMsg}`);
    }
  }

  if (permissionError) {
    installSpinner.fail("Permission denied");
    log.blank();
    log.warn("Fix permissions with:");
    for (const dir of failedDirs) {
      const parentDir = join(dir, "..");
      log.dim(`  sudo chown -R $(whoami) "${parentDir}"`);
    }
    log.blank();
    return;
  }

  installSpinner.succeed(`Installed ${installedSkills.length} skill(s)`);
  trackEvent("suggest_install", { skills: installedSkills, ides: targets.ides });

  const installedNames = selectedSkills.map((s) => s.name);
  logInstallSummary(targets, targetDirs, installedNames);
}
