const fs = require("fs");
const path = require("path");
const crypto = require("crypto");
const { resolveData } = require("./paths");
const { assertToolId } = require("./tool_repo_client");
const { runFileOperationWithRetries } = require("../../../src/services/safe-files");

const REQUIRED_FIELDS = Object.freeze([
  "tool_id",
  "display_name",
  "version",
  "description",
  "scope",
  "permissions",
  "capabilities",
  "limitations"
]);

class ToolInstaller {
  constructor(options = {}) {
    this.repoClient = options.repoClient;
    this.pluginsDir = options.pluginsDir || path.resolve(__dirname, "..", "..");
    this.stagingRoot = options.stagingRoot || resolveData("tools", "staging");
  }

  scanLocal() {
    if (!fs.existsSync(this.pluginsDir)) return [];
    return fs.readdirSync(this.pluginsDir, { withFileTypes: true })
      .filter((entry) => entry.isDirectory() && /^lumi_ai_[a-z0-9_-]+$/i.test(entry.name))
      .map((entry) => {
        const dir = path.join(this.pluginsDir, entry.name);
        try {
          return { tool_id: entry.name, dir, metadata: validateToolDirectory(dir, entry.name), valid: true };
        } catch (error) {
          return { tool_id: entry.name, dir, metadata: fallbackMetadata(entry.name), valid: false, error: error.message };
        }
      });
  }

  local(toolId) {
    assertToolId(toolId);
    return this.scanLocal().find((entry) => entry.tool_id === toolId) || null;
  }

  async install(toolId) {
    if (this.local(toolId)) throw new Error("AI tool plugin is already installed.");
    return this.stageAndSwap(toolId, false);
  }

  async update(toolId, options = {}) {
    if (!this.local(toolId)) throw new Error("AI tool plugin is not installed.");
    return this.stageAndSwap(toolId, true, options);
  }

  async stageAndSwap(toolId, updating, options = {}) {
    assertToolId(toolId);
    const id = crypto.randomUUID();
    const stageBase = path.join(this.stagingRoot, id);
    const stageDir = path.join(stageBase, toolId);
    const target = path.join(this.pluginsDir, toolId);
    const backup = path.join(this.pluginsDir, `.${toolId}.backup-${id}`);
    fs.mkdirSync(stageDir, { recursive: true });
    try {
      await this.repoClient.downloadTool(toolId, stageDir);
      const remoteMetadata = validateToolDirectory(stageDir, toolId);
      if (updating) preserveConfiguredPaths(target, stageDir, remoteMetadata);
      let movedCurrent = false;
      let installedReplacement = false;
      try {
        if (updating) {
          fileOperation(() => fs.renameSync(target, backup), target, "tool_backup");
          movedCurrent = true;
        }
        fileOperation(() => fs.renameSync(stageDir, target), target, "tool_install");
        installedReplacement = true;
        if (typeof options.afterSwap === "function") await options.afterSwap({ target, backup, metadata: remoteMetadata });
      } catch (error) {
        if (installedReplacement && fs.existsSync(target)) {
          try { fileOperation(() => fs.rmSync(target, { recursive: true, force: true }), target, "tool_remove_failed_replacement"); }
          catch (cleanupError) { error.message = `${error.message} The failed replacement remains at ${target}: ${cleanupError.message}`; }
        }
        if (movedCurrent && fs.existsSync(backup) && !fs.existsSync(target)) {
          try {
            fileOperation(() => fs.renameSync(backup, target), target, "tool_restore");
            movedCurrent = false;
          } catch (restoreError) {
            error.message = `${error.message} The previous tool remains recoverable at ${backup}: ${restoreError.message}`;
          }
        }
        throw error;
      }
      if (fs.existsSync(backup)) fileOperation(() => fs.rmSync(backup, { recursive: true, force: true }), backup, "tool_commit_cleanup");
      return { tool_id: toolId, metadata: remoteMetadata, installed: true, updated: updating };
    } finally {
      fs.rmSync(stageBase, { recursive: true, force: true });
      if (fs.existsSync(backup) && !fs.existsSync(target)) {
        try { fileOperation(() => fs.renameSync(backup, target), target, "tool_final_restore"); } catch {}
      }
    }
  }

  delete(toolId) {
    assertToolId(toolId);
    const target = path.resolve(this.pluginsDir, toolId);
    if (path.dirname(target) !== path.resolve(this.pluginsDir)) throw new Error("Tool deletion path is unsafe.");
    if (!fs.existsSync(target)) return false;
    fs.rmSync(target, { recursive: true, force: true });
    return true;
  }
}

function fileOperation(operation, target, operationName) {
  return runFileOperationWithRetries(operation, {
    target,
    operation: operationName,
    attempts: 8,
    delayMs: 15
  });
}

function validateToolDirectory(directory, folderName = path.basename(directory)) {
  const metadataFile = path.join(directory, "tool_info.json");
  const readmeFile = path.join(directory, "readme.md");
  if (!fs.existsSync(metadataFile)) throw new Error("tool_info.json is required.");
  if (!fs.existsSync(readmeFile)) throw new Error("readme.md is required.");
  let metadata;
  try {
    metadata = JSON.parse(fs.readFileSync(metadataFile, "utf8"));
  } catch {
    throw new Error("tool_info.json is invalid JSON.");
  }
  for (const field of REQUIRED_FIELDS) {
    if (metadata[field] == null || metadata[field] === "") throw new Error(`tool_info.json is missing ${field}.`);
  }
  assertToolId(metadata.tool_id);
  if (metadata.tool_id !== folderName) throw new Error("tool_id must match the plugin folder name.");
  if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(String(metadata.version))) {
    throw new Error("tool_info.json version must be semantic version text.");
  }
  for (const field of ["capabilities", "limitations"]) {
    if (!Array.isArray(metadata[field])) throw new Error(`${field} must be an array.`);
  }
  if (!["string", "object"].includes(typeof metadata.permissions)) throw new Error("permissions must be a string, array, or object.");
  if (metadata.settings_schema != null && (!metadata.settings_schema || typeof metadata.settings_schema !== "object" || Array.isArray(metadata.settings_schema))) {
    throw new Error("settings_schema must be an object.");
  }
  validateRelativeEntrypoints(metadata.entrypoints);
  return normalizeMetadata(metadata);
}

function normalizeMetadata(metadata) {
  return {
    ...metadata,
    tool_type: String(metadata.tool_type || "general"),
    owning_plugin: String(metadata.owning_plugin || metadata.tool_id),
    capabilities: metadata.capabilities.map(String),
    limitations: metadata.limitations.map(String),
    required_plugins: arrayOfStrings(metadata.required_plugins),
    required_platforms: arrayOfStrings(metadata.required_platforms),
    dependencies: arrayOfStrings(metadata.dependencies),
    data_paths: arrayOfStrings(metadata.data_paths),
    risk_level: String(metadata.risk_level || "sensitive"),
    confirmation_required: metadata.confirmation_required !== false
  };
}

function preserveConfiguredPaths(source, destination, remoteMetadata) {
  if (!fs.existsSync(source) || remoteMetadata.preserve_on_update === false) return;
  let paths = ["data", "config"];
  try {
    const localMetadata = validateToolDirectory(source, path.basename(source));
    paths.push(...localMetadata.data_paths);
    if (Array.isArray(localMetadata.preserve_on_update)) paths.push(...localMetadata.preserve_on_update);
  } catch {}
  if (Array.isArray(remoteMetadata.preserve_on_update)) paths.push(...remoteMetadata.preserve_on_update);
  for (const relative of [...new Set(paths.map(safeRelativePath).filter(Boolean))]) {
    const current = path.join(source, relative);
    const staged = path.join(destination, relative);
    if (!fs.existsSync(current)) continue;
    fs.rmSync(staged, { recursive: true, force: true });
    copyPath(current, staged);
  }
}

function copyPath(source, destination) {
  const stat = fs.statSync(source);
  if (stat.isDirectory()) {
    fs.mkdirSync(destination, { recursive: true });
    for (const entry of fs.readdirSync(source)) copyPath(path.join(source, entry), path.join(destination, entry));
  } else if (stat.isFile()) {
    fs.mkdirSync(path.dirname(destination), { recursive: true });
    fs.copyFileSync(source, destination);
  }
}

function validateRelativeEntrypoints(entrypoints) {
  if (!entrypoints) return;
  const values = typeof entrypoints === "string" ? [entrypoints] : Object.values(entrypoints).flat();
  for (const value of values) {
    if (typeof value !== "string" || !safeRelativePath(value)) throw new Error("Entrypoints must be safe relative paths.");
  }
}

function safeRelativePath(value) {
  const normalized = String(value || "").replaceAll("\\", "/").replace(/^\/+/, "");
  if (!normalized || normalized.split("/").includes("..") || path.isAbsolute(normalized)) return null;
  return normalized;
}

function arrayOfStrings(value) {
  return Array.isArray(value) ? value.map(String).map((entry) => entry.trim()).filter(Boolean) : [];
}

function fallbackMetadata(toolId) {
  return {
    tool_id: toolId,
    display_name: toolId,
    version: "0.0.0",
    description: "",
    scope: "unavailable",
    permissions: [],
    capabilities: [],
    limitations: []
  };
}

module.exports = {
  REQUIRED_FIELDS,
  ToolInstaller,
  arrayOfStrings,
  fallbackMetadata,
  normalizeMetadata,
  preserveConfiguredPaths,
  safeRelativePath,
  validateToolDirectory
};
