const fs = require("node:fs");
const path = require("node:path");
const { stripBom, sha256File, listFiles } = require("./plugin-fs-util");
const { runCommand, createCommandInvocation, resolveCommandPath, isWindowsCommandScript, isPathLikeCommand, commandOutput, escapeRegExp } = require("./plugin-command");
const { codexHomeForTarget, openCodeConfigPathsForTarget, openCodeProjectRootForTarget, defaultPayloadRoot } = require("./plugin-agent-paths");

const { marketplaceId, marketplacePluginRef } = require("./plugin-builder");

function planPluginUninstall(pluginNames, targets, options = {}) {
  const operations = [];
  for (const pluginName of pluginNames) {
    for (const target of targets) {
      const record = readPluginRecord(target.dir, pluginName, target.agent);
      const payloadRoot = record && record.payloadRoot
        ? record.payloadRoot
        : defaultPayloadRoot(target, pluginName);
      const payloadExists = fs.existsSync(payloadRoot);
      const action = record || (options.force && payloadExists) ? "uninstall" : "missing";
      operations.push({
        action,
        plugin: { name: pluginName },
        target,
        record,
        payloadRoot,
        managedFiles: record && Array.isArray(record.files) ? record.files : [],
        payloadExists,
      });
    }
  }
  return { operations };
}

function applyPluginUninstallPlan(plan, options = {}) {
  const applied = [];
  for (const operation of plan.operations) {
    if (operation.action !== "uninstall") {
      applied.push(operation);
      continue;
    }
    const result = applyPluginUninstallOperation(operation, options);
    applied.push(result);
  }
  return applied;
}

function applyPluginUninstallOperation(operation, options = {}) {
  const { payloadRoot, pluginRoot } = managedRootsForOperation(operation);
  const validation = validateManagedFiles(pluginRoot, operation.managedFiles);
  if (validation.mismatched.length && !options.force) {
    return {
      ...operation,
      action: "blocked",
      validation,
      filesRemoved: [],
      registration: { skipped: true, reason: "checksum mismatch; rerun with --force to remove" },
    };
  }

  const filesRemoved = removeManagedFiles(pluginRoot, payloadRoot, operation.managedFiles, { force: options.force });
  if (options.force && fs.existsSync(payloadRoot)) {
    fs.rmSync(payloadRoot, { recursive: true, force: true });
  } else {
    removeEmptyDirectories(payloadRoot);
  }
  removeEmptyDirectories(pluginRoot);

  const registration = options.skipHostRegistration
    ? { skipped: true }
    : unregisterHostPlugin(operation, options);
  removePluginRecord(operation.target.dir, operation.plugin.name, operation.target.agent);
  return {
    ...operation,
    validation,
    filesRemoved,
    registration,
  };
}

function unregisterHostPlugin(operation, options = {}) {
  if (operation.target.agent === "claude") {
    return unregisterClaudePlugin(operation, options);
  }
  if (operation.target.agent === "codex") {
    return unregisterCodexPlugin(operation, options);
  }
  if (operation.target.agent === "opencode") {
    return unregisterOpenCodePlugin(operation);
  }
  return { skipped: true, reason: `unsupported agent: ${operation.target.agent}` };
}

function unregisterClaudePlugin(operation, options = {}) {
  const commandRunner = options.runCommand || runCommand;
  const marketplace = marketplaceId(operation.plugin);
  const uninstall = commandRunner("claude", ["plugin", "uninstall", marketplacePluginRef(operation.plugin)]);
  const remove = commandRunner("claude", ["plugin", "marketplace", "remove", marketplace]);
  const success = isBenignClaudeUninstall(uninstall) && isBenignClaudeMarketplaceRemove(remove);
  return {
    success,
    command: "claude plugin uninstall/remove",
    message: `${commandOutput(uninstall)}\n${commandOutput(remove)}`.trim(),
    nextCommand: "restart Claude or open a new Claude session",
  };
}

function unregisterCodexPlugin(operation, options = {}) {
  const commandRunner = options.runCommand || runCommand;
  const marketplace = marketplaceId(operation.plugin);
  const codexHome = codexHomeForTarget(operation.target);
  fs.mkdirSync(codexHome, { recursive: true });
  const commandOptions = { env: { ...process.env, CODEX_HOME: codexHome } };
  const remove = commandRunner("codex", ["plugin", "marketplace", "remove", marketplace], commandOptions);
  const configUpdate = disableCodexPlugin(codexHome, operation.plugin);
  return {
    success: remove.status === 0,
    command: "codex plugin marketplace remove",
    message: commandOutput(remove),
    configUpdate,
    nextCommand: "restart Codex or open a new Codex task",
  };
}

function unregisterOpenCodePlugin(operation) {
  const updates = removeOpenCodePluginReferences(operation);
  return {
    success: updates.every((item) => item.success !== false),
    command: "opencode config update",
    message: updates.map((item) => `${item.changed ? "updated" : "unchanged"} ${item.path}`).join("\n"),
    configUpdate: updates,
    nextCommand: "restart OpenCode or open a new OpenCode session",
  };
}

function disableCodexPlugin(codexHome, plugin) {
  const configPath = path.join(codexHome, "config.toml");
  if (!fs.existsSync(configPath)) {
    return { changed: false, path: configPath };
  }
  const existing = fs.readFileSync(configPath, "utf8");
  const withoutPlugin = removeTomlSection(existing, `[plugins."${plugin.name}@${marketplaceId(plugin)}"]`);
  const withoutMarketplace = removeTomlSection(withoutPlugin, `[marketplaces.${marketplaceId(plugin)}]`);
  if (withoutMarketplace === existing) {
    return { changed: false, path: configPath };
  }
  fs.writeFileSync(configPath, withoutMarketplace, "utf8");
  return { changed: true, path: configPath };
}

function removeOpenCodePluginReferences(operation) {
  return openCodeConfigPathsForTarget(operation.target)
    .filter((item, index, all) => all.indexOf(item) === index)
    .filter((configPath) => fs.existsSync(configPath))
    .map((configPath) => removeOpenCodePluginReference(configPath, operation.payloadRoot));
}

function removeOpenCodePluginReference(configPath, payloadRoot) {
  const parsed = readJsoncFile(configPath);
  if (!parsed) {
    return { path: configPath, changed: false, success: false };
  }
  const skillsDir = path.join(payloadRoot, "skills");
  // Field isolation: remove our OWN entry from each plugin field independently,
  // preserving every other entry (e.g. oh-langfuse) in place. Never merge one
  // field's entries into the other — that would pollute a clean V2 `plugins`
  // field or smuggle other tools' entries into the legacy `plugin` field.
  const isOwn = (item) => path.resolve(item) === path.resolve(payloadRoot);
  let changed = false;
  const next = { ...parsed };
  for (const field of ["plugin", "plugins"]) {
    if (!Array.isArray(parsed[field])) continue;
    const filtered = parsed[field].filter((item) => !isOwn(item));
    if (filtered.length === parsed[field].length) continue;
    changed = true;
    if (filtered.length) next[field] = filtered;
    else delete next[field];
  }
  let nextSkillsValue;
  let skillsChanged = false;
  if (Array.isArray(parsed.skills)) {
    nextSkillsValue = parsed.skills.filter((item) => path.resolve(item) !== path.resolve(skillsDir));
    skillsChanged = nextSkillsValue.length !== parsed.skills.length;
  } else if (parsed.skills && Array.isArray(parsed.skills.paths)) {
    const nextPaths = parsed.skills.paths.filter((item) => path.resolve(item) !== path.resolve(skillsDir));
    nextSkillsValue = { ...parsed.skills, paths: nextPaths };
    skillsChanged = nextPaths.length !== parsed.skills.paths.length;
  }
  if (skillsChanged) {
    changed = true;
    next.skills = nextSkillsValue;
  }
  if (!changed) {
    return { path: configPath, changed: false, success: true };
  }
  fs.writeFileSync(configPath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
  return { path: configPath, changed: true, success: true };
}

function validateManagedFiles(payloadRoot, files) {
  const missing = [];
  const matched = [];
  const mismatched = [];
  for (const file of files) {
    const filePath = path.resolve(file.path);
    assertInside(payloadRoot, filePath);
    if (!fs.existsSync(filePath)) {
      missing.push(filePath);
      continue;
    }
    const actual = sha256File(filePath);
    if (actual === file.sha256) {
      matched.push(filePath);
    } else {
      mismatched.push({ path: filePath, expected: file.sha256, actual });
    }
  }
  return { matched, missing, mismatched };
}

function removeManagedFiles(pluginRoot, payloadRoot, files, options = {}) {
  const removed = [];
  for (const file of files) {
    const filePath = path.resolve(file.path);
    assertInside(pluginRoot, filePath);
    if (!fs.existsSync(filePath)) continue;
    fs.rmSync(filePath, { force: true });
    removed.push(filePath);
  }
  if (options.force && fs.existsSync(payloadRoot)) {
    const payloadFiles = listFiles(payloadRoot);
    fs.rmSync(payloadRoot, { recursive: true, force: true });
    for (const file of payloadFiles) {
      if (!removed.includes(file)) {
        removed.push(file);
      }
    }
  }
  return removed;
}

function removeEmptyDirectories(root) {
  if (!fs.existsSync(root)) return;
  const entries = fs.readdirSync(root, { withFileTypes: true });
  for (const entry of entries) {
    if (entry.isDirectory()) {
      removeEmptyDirectories(path.join(root, entry.name));
    }
  }
  if (fs.existsSync(root) && fs.readdirSync(root).length === 0) {
    fs.rmdirSync(root);
  }
}

function managedRootsForOperation(operation) {
  const payloadRoot = path.resolve(operation.payloadRoot);
  const expected = path.resolve(defaultPayloadRoot(operation.target, operation.plugin.name));
  if (payloadRoot !== expected) {
    throw new Error(`Refusing to uninstall plugin payload outside managed root: ${payloadRoot}`);
  }
  return { payloadRoot, pluginRoot: path.dirname(payloadRoot) };
}

function readInstallRecord(targetDir) {
  const recordPath = path.join(targetDir, ".oh-skillhub", "installed.json");
  if (!fs.existsSync(recordPath)) {
    return { skills: {}, plugins: {} };
  }
  try {
    const parsed = JSON.parse(stripBom(fs.readFileSync(recordPath, "utf8")));
    return { ...parsed, skills: parsed.skills || {}, plugins: parsed.plugins || {} };
  } catch {
    return { skills: {}, plugins: {} };
  }
}

function readPluginRecord(targetDir, pluginName, agent) {
  const record = readInstallRecord(targetDir);
  const key = pluginRecordKey(pluginName, agent);
  return record.plugins && record.plugins[key] ? record.plugins[key] : null;
}

function removePluginRecord(targetDir, pluginName, agent) {
  const recordDir = path.join(targetDir, ".oh-skillhub");
  const recordPath = path.join(recordDir, "installed.json");
  const record = readInstallRecord(targetDir);
  const key = pluginRecordKey(pluginName, agent);
  if (!record.plugins || !record.plugins[key]) {
    return false;
  }
  delete record.plugins[key];
  record.installedAt = new Date().toISOString();
  fs.mkdirSync(recordDir, { recursive: true });
  fs.writeFileSync(recordPath, `${JSON.stringify(record, null, 2)}\n`, "utf8");
  return true;
}

function pluginRecordKey(pluginName, agent) {
  return `${pluginName}:${agent}`;
}

function removeTomlSection(contents, section) {
  const text = contents ? contents.replace(/\s*$/, "\n") : "";
  const pattern = new RegExp(`(^|\\n)${escapeRegExp(section)}\\n[\\s\\S]*?(?=\\n\\[|$)`);
  return text.replace(pattern, (match, prefix = "") => (prefix === "\n" ? "\n" : ""));
}

function isBenignClaudeUninstall(result) {
  return result.status === 0 || /not installed|not found|No plugin/i.test(commandOutput(result));
}

function isBenignClaudeMarketplaceRemove(result) {
  return result.status === 0 || /not found|does not exist|No marketplace/i.test(commandOutput(result));
}

function readJsoncFile(file) {
  try {
    return JSON.parse(stripJsonComments(stripBom(fs.readFileSync(file, "utf8"))));
  } catch {
    return null;
  }
}

function stripJsonComments(text) {
  return text
    .replace(/\/\*[\s\S]*?\*\//g, "")
    .replace(/(^|[^:])\/\/.*$/gm, "$1");
}

function assertInside(root, candidate) {
  const relative = path.relative(path.resolve(root), path.resolve(candidate));
  if (relative.startsWith("..") || path.isAbsolute(relative)) {
    throw new Error(`Refusing to uninstall outside managed plugin payload: ${candidate}`);
  }
}

module.exports = {
  applyPluginUninstallPlan,
  planPluginUninstall,
  readPluginRecord,
  removeTomlSection,
  unregisterHostPlugin,
};