import { Command } from "commander";
import pc from "picocolors";
import ora from "ora";
import { mkdir, writeFile, readFile, unlink } from "fs/promises";
import { join } from "path";
import { homedir } from "os";
import { spawn } from "child_process";
import { input, select } from "@inquirer/prompts";

import {
  searchLibraries,
  getSkillQuestions,
  generateSkillStructured,
  getSkillQuota,
} from "../utils/api.js";
import { loadTokens, isTokenExpired } from "../utils/auth.js";
import { performLogin } from "./auth.js";
import { log } from "../utils/logger.js";
import { promptForInstallTargets, getTargetDirs } from "../utils/ide.js";
import selectOrInput from "../utils/selectOrInput.js";
import { checkboxWithHover, terminalLink } from "../utils/prompts.js";
import { trackEvent } from "../utils/tracking.js";
import type {
  GenerateOptions,
  LibrarySearchResult,
  SkillAnswer,
  StructuredGenerateInput,
  GenerateStreamEvent,
  ToolResultSnippet,
} from "../types.js";

interface QueryLogEntry {
  query: string;
  libraryId?: string;
  results: ToolResultSnippet[];
}

export function registerGenerateCommand(skillCommand: Command): void {
  skillCommand
    .command("generate")
    .alias("gen")
    .alias("g")
    .option("-o, --output <dir>", "Output directory (default: current directory)")
    .option("--all", "Generate for all detected IDEs")
    .option("--global", "Generate in global skills directory")
    .option("--claude", "Claude Code (.claude/skills/)")
    .option("--cursor", "Cursor (.cursor/skills/)")
    .option("--universal", "Universal (.agents/skills/)")
    .option("--antigravity", "Antigravity (.agent/skills/)")
    .description("Generate a skill for a library using AI")
    .action(async (options: GenerateOptions) => {
      await generateCommand(options);
    });
}

async function generateCommand(options: GenerateOptions): Promise<void> {
  trackEvent("command", { name: "generate" });
  log.blank();

  let accessToken: string | null = null;
  const tokens = loadTokens();
  if (tokens && !isTokenExpired(tokens)) {
    accessToken = tokens.access_token;
  } else {
    log.info("Authentication required. Logging in...");
    log.blank();
    accessToken = await performLogin();
    if (!accessToken) {
      log.error("Login failed. Please try again.");
      return;
    }
    log.blank();
  }

  const initSpinner = ora().start();
  const quota = await getSkillQuota(accessToken);

  if (quota.error) {
    initSpinner.fail(pc.red("Failed to initialize"));
    return;
  }

  if (quota.tier !== "unlimited" && quota.remaining < 1) {
    initSpinner.fail(pc.red("Weekly skill generation limit reached"));
    log.blank();
    console.log(
      `  You've used ${pc.bold(pc.white(quota.used.toString()))}/${pc.bold(pc.white(quota.limit.toString()))} skill generations this week.`
    );
    console.log(
      `  Your quota resets on ${pc.yellow(new Date(quota.resetDate!).toLocaleDateString())}.`
    );
    log.blank();
    if (quota.tier === "free") {
      console.log(
        `  ${pc.yellow("Tip:")} Upgrade to Pro for ${pc.bold("10")} generations per week.`
      );
      console.log(`  Visit ${pc.green("https://context7.com/dashboard")} to upgrade.`);
    }
    return;
  }

  initSpinner.stop();
  initSpinner.clear();

  console.log(pc.bold("What should your agent become an expert at?\n"));
  console.log(
    pc.dim(
      "Skills should encode best practices, constraints, and decision-making —\nnot step-by-step tutorials or one-off tasks.\n"
    )
  );
  console.log(pc.yellow("Examples:"));
  // prettier-ignore
  {
    console.log(pc.red('  ✕ "Deploy a Next.js app to Vercel"'));
    console.log(pc.green('  ✓ "Best practices and constraints for deploying Next.js apps to Vercel"'));
    log.blank();
    console.log(pc.red('  ✕ "Use Tailwind for responsive design"'));
    console.log(pc.green('  ✓ "Responsive layout decision-making with Tailwind CSS"'));
    log.blank();
    console.log(pc.red('  ✕ "Build OAuth with NextAuth"'));
    console.log(pc.green('  ✓ "OAuth authentication patterns and pitfalls with NextAuth.js"'));
  }
  log.blank();

  let motivation: string;
  try {
    motivation = await input({
      message: "Describe the expertise:",
    });

    if (!motivation.trim()) {
      log.warn("Expertise description is required");
      return;
    }
    motivation = motivation.trim();
  } catch {
    log.warn("Generation cancelled");
    return;
  }

  log.blank();
  console.log(
    pc.dim(
      "To generate this skill, we will read relevant documentation and examples\nfrom Context7.\n"
    )
  );
  console.log(
    pc.dim(
      "These sources are used to:\n• extract best practices and constraints\n• compare patterns across official docs and examples\n• avoid outdated or incorrect guidance\n"
    )
  );
  console.log(pc.dim("You can adjust which sources the skill is based on.\n"));

  const searchSpinner = ora("Finding relevant sources...").start();
  const searchResult = await searchLibraries(motivation, accessToken);

  if (searchResult.error || !searchResult.results?.length) {
    searchSpinner.fail(pc.red("No sources found"));
    log.warn(searchResult.message || "Try a different description");
    return;
  }

  searchSpinner.succeed(pc.green(`Found ${searchResult.results.length} relevant sources`));
  log.blank();

  if (searchResult.searchFilterApplied) {
    log.warn(
      "Your results only include libraries matching your teamspace's library filters. To adjust quality thresholds or blocked libraries, update your filters at https://context7.com/dashboard?tab=policies"
    );
    log.blank();
  }

  let selectedLibraries: LibrarySearchResult[];
  try {
    const formatProjectId = (id: string) => {
      return id.startsWith("/") ? id.slice(1) : id;
    };

    const isGitHubRepo = (id: string): boolean => {
      const cleanId = id.startsWith("/") ? id.slice(1) : id;
      const parts = cleanId.split("/");
      if (parts.length !== 2) return false;
      const nonGitHubPrefixes = ["websites", "packages", "npm", "docs", "libraries", "llmstxt"];
      return !nonGitHubPrefixes.includes(parts[0].toLowerCase());
    };

    const libraries = searchResult.results.slice(0, 5);
    const indexWidth = libraries.length.toString().length;
    const maxNameLen = Math.max(...libraries.map((lib) => lib.title.length));

    const libraryChoices = libraries.map((lib, index) => {
      const projectId = formatProjectId(lib.id);
      const isGitHub = isGitHubRepo(lib.id);
      const indexStr = pc.dim(`${(index + 1).toString().padStart(indexWidth)}.`);
      const paddedName = lib.title.padEnd(maxNameLen);

      const libUrl = `https://context7.com${lib.id}`;
      const libLink = terminalLink(lib.title, libUrl, pc.white);
      const sourceUrl = isGitHub
        ? `https://github.com/${projectId}`
        : `https://context7.com${lib.id}`;
      const repoLink = terminalLink(projectId, sourceUrl, pc.white);

      const starsLine =
        lib.stars && isGitHub ? [`${pc.yellow("Stars:")}       ${lib.stars.toLocaleString()}`] : [];

      const metadataLines = [
        pc.dim("─".repeat(50)),
        "",
        `${pc.yellow("Library:")}     ${libLink}`,
        `${pc.yellow("Source:")}      ${repoLink}`,
        `${pc.yellow("Snippets:")}    ${lib.totalSnippets.toLocaleString()}`,
        ...starsLine,
        `${pc.yellow("Description:")}`,
        pc.white(lib.description || "No description"),
      ];

      return {
        name: `${indexStr} ${paddedName}  ${pc.dim(`(${projectId})`)}`,
        value: lib,
        description: metadataLines.join("\n"),
      };
    });

    selectedLibraries = await checkboxWithHover(
      {
        message: "Select sources:",
        choices: libraryChoices,
        pageSize: 10,
        loop: false,
      },
      { getName: (lib) => `${lib.title} (${formatProjectId(lib.id)})` }
    );

    if (!selectedLibraries || selectedLibraries.length === 0) {
      log.info("No sources selected. Try running the command again.");
      return;
    }
  } catch {
    log.warn("Generation cancelled");
    return;
  }

  log.blank();

  const questionsSpinner = ora(
    "Preparing follow-up questions to clarify scope and constraints..."
  ).start();
  const librariesInput = selectedLibraries.map((lib) => ({ id: lib.id, name: lib.title }));
  const questionsResult = await getSkillQuestions(librariesInput, motivation, accessToken);

  if (questionsResult.error || !questionsResult.questions?.length) {
    questionsSpinner.fail(pc.red("Failed to generate questions"));
    log.warn(questionsResult.message || "Please try again");
    return;
  }

  questionsSpinner.succeed(pc.green("Questions prepared"));
  log.blank();

  const answers: SkillAnswer[] = [];
  try {
    for (let i = 0; i < questionsResult.questions.length; i++) {
      const q = questionsResult.questions[i];
      const questionNum = i + 1;
      const totalQuestions = questionsResult.questions.length;

      const answer = await selectOrInput({
        message: `${pc.dim(`[${questionNum}/${totalQuestions}]`)} ${q.question}`,
        options: q.options,
        recommendedIndex: q.recommendedIndex,
      });

      answers.push({
        question: q.question,
        answer,
      });

      const linesToClear = 3 + q.options.length;
      process.stdout.write(`\x1b[${linesToClear}A\x1b[J`);

      const truncatedAnswer = answer.length > 50 ? answer.slice(0, 47) + "..." : answer;
      console.log(`${pc.green("✓")} ${pc.dim(`[${questionNum}/${totalQuestions}]`)} ${q.question}`);
      console.log(`  ${pc.cyan(truncatedAnswer)}`);
      log.blank();
    }
  } catch {
    log.warn("Generation cancelled");
    return;
  }

  let generatedContent: string | null = null;
  let skillName: string = "";
  let feedback: string | undefined;
  let previewFile: string | null = null;
  let previewFileWritten = false;

  const cleanupPreviewFile = async () => {
    if (previewFile) {
      await unlink(previewFile).catch(() => {});
    }
  };

  const queryLog: QueryLogEntry[] = [];
  let genSpinner: ReturnType<typeof ora> | null = null;

  const formatQueryLogText = (): string => {
    if (queryLog.length === 0) return "";

    const lines: string[] = [];
    const latestEntry = queryLog[queryLog.length - 1];

    lines.push("");

    for (const result of latestEntry.results.slice(0, 3)) {
      const cleanContent = result.content.replace(/Source:\s*https?:\/\/[^\s]+/gi, "").trim();
      if (cleanContent) {
        lines.push(`  ${pc.yellow("•")} ${pc.white(result.title)}`);
        const maxLen = 400;
        const content =
          cleanContent.length > maxLen ? cleanContent.slice(0, maxLen - 3) + "..." : cleanContent;
        const words = content.split(" ");
        let currentLine = "    ";
        for (const word of words) {
          if (currentLine.length + word.length > 84) {
            lines.push(pc.dim(currentLine));
            currentLine = "    " + word + " ";
          } else {
            currentLine += word + " ";
          }
        }
        if (currentLine.trim()) {
          lines.push(pc.dim(currentLine));
        }
        lines.push("");
      }
    }

    return "\n" + lines.join("\n");
  };

  let isGeneratingContent = false;
  let initialStatus = "Reading selected Context7 sources to generate the skill...";

  const handleStreamEvent = (event: GenerateStreamEvent) => {
    if (event.type === "progress") {
      if (genSpinner) {
        if (event.message.startsWith("Generating skill content...") && !isGeneratingContent) {
          isGeneratingContent = true;
          if (queryLog.length > 0) {
            genSpinner.succeed(pc.green(`Read Context7 sources`));
          } else {
            genSpinner.succeed(pc.green(`Ready to generate`));
          }
          genSpinner = ora("Generating skill content...").start();
        } else if (!isGeneratingContent) {
          genSpinner.text = initialStatus + formatQueryLogText();
        }
      }
    } else if (event.type === "tool_result") {
      queryLog.push({
        query: event.query,
        libraryId: event.libraryId,
        results: event.results,
      });
      if (genSpinner && !isGeneratingContent) {
        genSpinner.text = genSpinner.text.split("\n")[0] + formatQueryLogText();
      }
    }
  };

  while (true) {
    const generateInput: StructuredGenerateInput = {
      motivation,
      libraries: librariesInput,
      answers,
      feedback,
      previousContent: feedback && generatedContent ? generatedContent : undefined,
    };

    queryLog.length = 0;
    isGeneratingContent = false;
    previewFileWritten = false;
    initialStatus = feedback
      ? "Regenerating skill with your feedback..."
      : "Reading selected Context7 sources to generate the skill...";

    genSpinner = ora(initialStatus).start();

    const result = await generateSkillStructured(generateInput, handleStreamEvent, accessToken);

    if (result.error) {
      genSpinner.fail(pc.red(`Error: ${result.error}`));
      return;
    }

    if (!result.content) {
      genSpinner.fail(pc.red("No content generated"));
      return;
    }

    genSpinner.succeed(pc.green(`Generated skill for "${result.libraryName}"`));
    generatedContent = result.content;
    skillName = result.libraryName.toLowerCase().replace(/[^a-z0-9-]/g, "-");

    const contentLines = generatedContent.split("\n");
    const previewLineCount = 20;
    const hasMoreLines = contentLines.length > previewLineCount;
    const previewContent = contentLines.slice(0, previewLineCount).join("\n");
    const remainingLines = contentLines.length - previewLineCount;

    const showPreview = () => {
      log.blank();
      console.log(pc.dim("━".repeat(70)));
      console.log(pc.bold(`Generated Skill: `) + pc.green(pc.bold(skillName)));
      console.log(pc.dim("━".repeat(70)));
      log.blank();
      console.log(previewContent);
      if (hasMoreLines) {
        log.blank();
        console.log(pc.dim(`... ${remainingLines} more lines`));
      }
      log.blank();
      console.log(pc.dim("━".repeat(70)));
      log.blank();
    };

    const openInEditor = async () => {
      const previewDir = join(homedir(), ".context7", "previews");
      await mkdir(previewDir, { recursive: true });
      previewFile = join(previewDir, `${skillName}.md`);
      if (!previewFileWritten) {
        await writeFile(previewFile, generatedContent!, "utf-8");
        previewFileWritten = true;
      }
      const editor = process.env.EDITOR || "open";
      await new Promise<void>((resolve) => {
        const child = spawn(editor, [previewFile!], {
          stdio: "inherit",
        });
        child.on("close", () => resolve());
      });
    };

    const syncFromPreviewFile = async () => {
      if (previewFile) {
        generatedContent = await readFile(previewFile, "utf-8");
      }
    };

    showPreview();

    await new Promise((resolve) => setTimeout(resolve, 100));

    try {
      let action: string;
      while (true) {
        const choices = [
          { name: `${pc.green("✓")} Install skill (save locally)`, value: "install" },
          { name: `${pc.blue("⤢")} Edit skill in editor`, value: "view" },
          { name: `${pc.yellow("✎")} Request changes`, value: "feedback" },
          { name: `${pc.red("✕")} Cancel`, value: "cancel" },
        ];

        action = await select({
          message: "What would you like to do?",
          choices,
        });

        if (action === "view") {
          await openInEditor();
          continue;
        }
        await syncFromPreviewFile();
        break;
      }

      if (action === "install") {
        break;
      } else if (action === "cancel") {
        await cleanupPreviewFile();
        log.warn("Generation cancelled");
        return;
      } else if (action === "feedback") {
        trackEvent("gen_feedback");
        feedback = await input({
          message: "What changes would you like? (press Enter to skip)",
        });

        if (!feedback.trim()) {
          feedback = undefined;
        }
        log.blank();
      }
    } catch {
      await cleanupPreviewFile();
      log.warn("Generation cancelled");
      return;
    }
  }

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

  const targetDirs = getTargetDirs(targets);

  const writeSpinner = ora("Writing skill files...").start();

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

  for (const targetDir of targetDirs) {
    let finalDir = targetDir;
    if (options.output && !targetDir.includes("/.config/") && !targetDir.startsWith(homedir())) {
      finalDir = targetDir.replace(process.cwd(), options.output);
    }
    const skillDir = join(finalDir, skillName);
    const skillPath = join(skillDir, "SKILL.md");

    try {
      await mkdir(skillDir, { recursive: true });
      await writeFile(skillPath, generatedContent!, "utf-8");
    } catch (err) {
      const error = err as NodeJS.ErrnoException;
      if (error.code === "EACCES" || error.code === "EPERM") {
        permissionError = true;
        failedDirs.add(skillDir);
      } else {
        log.warn(`Failed to write to ${skillPath}: ${error.message}`);
      }
    }
  }

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

  writeSpinner.succeed(pc.green(`Created skill in ${targetDirs.length} location(s)`));
  trackEvent("gen_install");

  log.blank();
  console.log(pc.green("Skill saved successfully"));
  for (const targetDir of targetDirs) {
    console.log(pc.dim(`  ${targetDir}/`) + pc.green(skillName));
  }
  log.blank();

  await cleanupPreviewFile();
}
