import {
  cpSync,
  existsSync,
  mkdirSync,
  readdirSync,
  readFileSync,
  rmSync,
  writeFileSync,
} from "node:fs";
import { basename, isAbsolute, join, relative, resolve } from "node:path";

function readJson(path) {
  return JSON.parse(readFileSync(path, "utf8"));
}

function assertInside(root, candidate, label) {
  const pathFromRoot = relative(root, candidate);
  if (pathFromRoot === "" || (!pathFromRoot.startsWith("..") && !isAbsolute(pathFromRoot))) return;
  throw new Error(`${label} escapes its repository root: ${candidate}`);
}

export function pluginSourceDefinition(repositoryRoot, pluginId) {
  const pluginRoot = join(repositoryRoot, "plugins", pluginId);
  const definitionPath = join(pluginRoot, "plugin-sources.json");
  if (!existsSync(definitionPath)) return null;
  const definition = readJson(definitionPath);
  if (!definition || typeof definition.skillsRepository !== "string" || !Array.isArray(definition.skills)) {
    throw new Error(`invalid plugin source definition: ${definitionPath}`);
  }
  return { pluginRoot, definitionPath, ...definition };
}

export function assemblePlugins(repositoryRoot, outputRoot, selectedPluginIds) {
  const sourcePluginRoot = join(repositoryRoot, "plugins");
  const pluginIds = selectedPluginIds ?? readdirSync(sourcePluginRoot, { withFileTypes: true })
    .filter((entry) => entry.isDirectory() && existsSync(join(sourcePluginRoot, entry.name, "plugin-sources.json")))
    .map((entry) => entry.name)
    .sort();

  mkdirSync(join(outputRoot, "plugins"), { recursive: true });
  const pluginLicense = join(sourcePluginRoot, "LICENSE");
  if (!existsSync(pluginLicense)) throw new Error(`plugin license is missing: ${pluginLicense}`);
  cpSync(pluginLicense, join(outputRoot, "plugins", "LICENSE"));
  const assembled = [];

  for (const pluginId of pluginIds) {
    const source = pluginSourceDefinition(repositoryRoot, pluginId);
    if (!source) throw new Error(`plugin has no source definition: ${pluginId}`);
    const manifestPath = join(source.pluginRoot, ".claude-plugin", "plugin.json");
    const manifest = readJson(manifestPath);
    if (manifest.name !== pluginId) throw new Error(`plugin manifest name disagrees with directory: ${manifestPath}`);
    const destination = join(outputRoot, "plugins", pluginId);
    rmSync(destination, { recursive: true, force: true });
    mkdirSync(destination, { recursive: true });
    cpSync(pluginLicense, join(destination, "LICENSE"));

    for (const entry of readdirSync(source.pluginRoot, { withFileTypes: true })) {
      if (entry.name === "skills" || entry.name === "plugin-sources.json") continue;
      cpSync(join(source.pluginRoot, entry.name), join(destination, entry.name), {
        recursive: true,
        dereference: true,
      });
    }

    const skillsRepository = resolve(repositoryRoot, source.skillsRepository);
    if (!existsSync(join(skillsRepository, ".git"))) {
      throw new Error(`Skill submodule is not initialized: ${source.skillsRepository}`);
    }
    const skillsLicense = join(skillsRepository, "LICENSE");
    if (!existsSync(skillsLicense)) throw new Error(`Skill repository license is missing: ${skillsLicense}`);
    cpSync(skillsLicense, join(destination, "SKILLS_LICENSE"));
    const names = new Set();
    for (const relativeSkill of source.skills) {
      if (typeof relativeSkill !== "string" || !relativeSkill) {
        throw new Error(`invalid Skill path in ${source.definitionPath}`);
      }
      const skillSource = resolve(skillsRepository, relativeSkill);
      assertInside(skillsRepository, skillSource, "Skill path");
      if (!existsSync(join(skillSource, "SKILL.md"))) {
        throw new Error(`Skill is missing SKILL.md: ${relativeSkill}`);
      }
      const name = basename(skillSource);
      if (names.has(name)) throw new Error(`duplicate Skill name in ${pluginId}: ${name}`);
      names.add(name);
      const skillDestination = join(destination, "skills", name);
      cpSync(skillSource, skillDestination, { recursive: true, dereference: true });
      const skillManifest = join(skillDestination, "SKILL.md");
      const manifest = readFileSync(skillManifest, "utf8")
        .replace(/^disable-model-invocation:\s*true\s*$/m, "disable-model-invocation: false");
      writeFileSync(skillManifest, manifest);
    }
    manifest.skills = [...names].map((name) => `./skills/${name}`);
    delete manifest.dependencies;
    writeFileSync(
      join(destination, ".claude-plugin", "plugin.json"),
      `${JSON.stringify(manifest, null, 2)}\n`,
    );
    assembled.push({ pluginId, destination, skills: names.size });
  }

  return assembled;
}