const fs = require("fs");
const path = require("path");
const { writeJsonAtomicSync } = require("../../../src/services/safe-files");
const { resolveData } = require("./paths");
const { registerManagedTool } = require("./tool_registry");

class ToolLoader {
  constructor(options = {}) {
    this.registry = options.registry;
    this.installer = options.installer;
    this.settings = options.settings;
    this.lumiAiVersion = options.lumiAiVersion || "0.0.0";
    this.lumiVersion = options.lumiVersion || "0.0.0";
    this.stateFile = options.stateFile || resolveData("tools", "enabled.json");
    this.loaded = new Map();
    this.statuses = new Map();
  }

  async loadEnabled() {
    return this.reconcile();
  }

  async reconcile({ reload = false } = {}) {
    const state = this.readState();
    const locals = this.installer.scanLocal();
    const localMap = new Map(locals.map((local) => [local.tool_id, local]));
    for (const local of locals) {
      if (local.valid && state.enabled[local.tool_id] == null && local.metadata.default_enabled === true) {
        state.enabled[local.tool_id] = true;
        this.writeState(state);
      }
    }
    for (const toolId of [...this.loaded.keys()]) {
      if (!localMap.has(toolId) || state.enabled[toolId] !== true) {
        await this.disable(toolId, { persist: false });
      }
    }
    for (const local of locals) {
      if (!local.valid) {
        this.registry.unregisterOwner(local.tool_id);
        this.loaded.delete(local.tool_id);
        this.setStatus(local.tool_id, "unavailable", local.error, { blocking: ["schema_invalid"], optional: [] });
        continue;
      }
      if (state.enabled[local.tool_id] !== true) {
        this.setStatus(local.tool_id, "disabled", "");
        continue;
      }
      const dependencies = this.inspectDependencies(local.metadata, local.dir);
      if (dependencies.blocking.length) {
        await this.disable(local.tool_id, { persist: false });
        this.setStatus(
          local.tool_id,
          "unavailable",
          `Unavailable: ${dependencies.blocking.join("; ")}`,
          dependencies
        );
        continue;
      }
      let signature;
      try {
        signature = sourceSignature(local);
      } catch (error) {
        await this.disable(local.tool_id, { persist: false });
        this.setStatus(local.tool_id, "unavailable", error.message, dependencies);
        continue;
      }
      if (!reload && this.loaded.get(local.tool_id)?.source_signature === signature) continue;
      try { await this.enable(local.tool_id, { persist: false }); }
      catch (error) { this.setStatus(local.tool_id, "unavailable", error.message); }
    }
    return this.diagnostics();
  }

  async enable(toolId, options = {}) {
    const local = this.installer.local(toolId);
    if (!local?.valid) throw new Error(local?.error || "Installed AI tool metadata is invalid.");
    const dependencies = this.inspectDependencies(local.metadata, local.dir);
    if (dependencies.blocking.length) {
      await this.disable(toolId, { persist: false });
      const message = `Unavailable: ${dependencies.blocking.join("; ")}`;
      this.setStatus(toolId, "unavailable", message, dependencies);
      if (options.persist !== false) this.setEnabled(toolId, true);
      return { loaded: false, unavailable: true, message, dependencies };
    }
    await this.disable(toolId, { persist: false });
    let backend;
    try {
      backend = backendEntrypoint(local.metadata, local.dir);
    } catch (error) {
      this.setStatus(toolId, "unavailable", error.message, dependencies);
      if (options.persist !== false) this.setEnabled(toolId, true);
      return { loaded: false, unavailable: true, message: error.message, dependencies };
    }
    const registered = [];
    let cleanup = null;
    let runtimeDiagnostics = null;
    let availabilityMessage = "";
    if (backend) {
      clearRequireCache(local.dir);
      try {
        const module = require(backend);
        const register = module?.register || module?.init;
        if (typeof register !== "function") throw new Error("Backend entrypoint must export register() or init().");
        const context = {
          metadata: Object.freeze({ ...local.metadata }),
          registerTool: (definition) => {
            const unregister = registerManagedTool(this.registry, local.metadata, definition);
            registered.push({ id: definition.tool_id, unregister });
            return unregister;
          },
          paths: Object.freeze({
            root: local.dir,
            data: path.join(local.dir, "data"),
            config: path.join(local.dir, "config")
          }),
          assetUrl: (relative = "") => `/plugins/lumi_ai/tools/${toolId}/assets/${String(relative).replace(/^\/+/, "")}`
        };
        if (typeof module.checkAvailability === "function") {
          const availability = await module.checkAvailability(context);
          if (availability?.available === false) {
            this.loaded.set(toolId, {
              cleanup: null,
              registered,
              metadata: local.metadata,
              dir: local.dir,
              source_signature: sourceSignature(local)
            });
            this.setStatus(toolId, "unavailable", String(availability.message || "Tool configuration is incomplete."), dependencies);
            if (options.persist !== false) this.setEnabled(toolId, true);
            return { loaded: false, unavailable: true, message: availability.message, dependencies };
          }
          availabilityMessage = String(availability?.message || "");
        }
        const result = await register(context);
        cleanup = typeof result === "function" ? result : typeof result?.stop === "function" ? () => result.stop() : null;
        runtimeDiagnostics = typeof module.diagnostics === "function"
          ? () => module.diagnostics(context)
          : null;
      } catch (error) {
        this.registry.unregisterOwner(toolId);
        this.setStatus(toolId, "unavailable", error.message, dependencies);
        if (options.persist !== false) this.setEnabled(toolId, true);
        return { loaded: false, unavailable: true, message: error.message, dependencies };
      }
    }
    this.loaded.set(toolId, {
      cleanup,
      registered,
      metadata: local.metadata,
      dir: local.dir,
      diagnostics: runtimeDiagnostics,
      source_signature: sourceSignature(local)
    });
    this.setStatus(
      toolId,
      "enabled",
      dependencies.optional.length
        ? `Enabled with limitations: ${dependencies.optional.join("; ")}`
        : availabilityMessage,
      dependencies
    );
    if (options.persist !== false) this.setEnabled(toolId, true);
    return { loaded: true, registered: registered.map((entry) => entry.id), dependencies };
  }

  async disable(toolId, options = {}) {
    const loaded = this.loaded.get(toolId);
    if (loaded) {
      try { await loaded.cleanup?.(); } catch {}
      this.registry.unregisterOwner(toolId);
      this.loaded.delete(toolId);
    } else {
      this.registry.unregisterOwner(toolId);
    }
    if (options.persist !== false) this.setEnabled(toolId, false);
    this.setStatus(toolId, "disabled", "");
    return { disabled: true };
  }

  async stopAll() {
    for (const toolId of [...this.loaded.keys()]) await this.disable(toolId, { persist: false });
  }

  isEnabled(toolId) {
    return this.readState().enabled[toolId] === true;
  }

  status(toolId) {
    return this.statuses.get(toolId) || { state: this.isEnabled(toolId) ? "pending" : "disabled", message: "" };
  }

  diagnostics() {
    return this.installer.scanLocal().map((local) => {
      const enabled = this.isEnabled(local.tool_id);
      const status = this.status(local.tool_id);
      return {
        tool_id: local.tool_id,
        installed: true,
        enabled,
        valid: local.valid,
        state: status.state,
        message: status.message || local.error || "",
        dependencies: status.dependencies || (local.valid
          ? this.inspectDependencies(local.metadata, local.dir)
          : { blocking: ["schema_invalid"], optional: [] }),
        registered_tools: [...this.registry.tools.values()]
          .filter((definition) => definition.owning_plugin === local.tool_id)
          .map((definition) => definition.tool_id),
        runtime_details: safeDiagnostics(this.loaded.get(local.tool_id)?.diagnostics)
      };
    });
  }

  inspectDependencies(metadata, toolDir) {
    const blocking = [];
    const optional = [];
    for (const pluginId of metadata.required_plugins || []) {
      if (/^lumi_ai_.+/i.test(pluginId)) {
        blocking.push(`AI tool plugins cannot depend on AI tool plugin ${pluginId}`);
      } else if (!["core", "lumi_ai"].includes(pluginId) && !fs.existsSync(path.resolve(toolDir, "..", pluginId))) {
        blocking.push(`required plugin ${pluginId} is missing`);
      }
    }
    for (const platform of metadata.required_platforms || []) {
      if (this.settings?.getSetting?.(`platform_${platform}_enabled`, false) !== true) {
        blocking.push(`required platform ${platform} is disabled`);
      }
    }
    for (const dependency of metadata.dependencies || []) {
      try { require.resolve(dependency, { paths: [toolDir] }); }
      catch { optional.push(`optional dependency ${dependency} is missing`); }
    }
    if (metadata.minimum_lumi_ai_version && compareVersions(this.lumiAiVersion, metadata.minimum_lumi_ai_version) < 0) {
      blocking.push(`requires Lumi AI ${metadata.minimum_lumi_ai_version}`);
    }
    if (metadata.minimum_lumi_version && compareVersions(this.lumiVersion, metadata.minimum_lumi_version) < 0) {
      blocking.push(`requires Lumi ${metadata.minimum_lumi_version}`);
    }
    return { blocking, optional };
  }

  resolveAsset(toolId, relative) {
    const loaded = this.loaded.get(toolId);
    if (!loaded || !this.isEnabled(toolId)) return null;
    const roots = assetRoots(loaded.metadata, loaded.dir);
    for (const root of roots) {
      const candidate = path.resolve(root, String(relative || ""));
      if ((candidate === root || candidate.startsWith(`${root}${path.sep}`)) && fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
        return candidate;
      }
    }
    return null;
  }

  setStatus(toolId, state, message, dependencies = null) {
    this.statuses.set(toolId, { state, message: String(message || ""), dependencies });
  }

  readState() {
    try {
      const value = JSON.parse(fs.readFileSync(this.stateFile, "utf8"));
      return { enabled: value.enabled && typeof value.enabled === "object" ? value.enabled : {} };
    } catch {
      return { enabled: {} };
    }
  }

  setEnabled(toolId, enabled) {
    const state = this.readState();
    state.enabled[toolId] = Boolean(enabled);
    this.writeState(state);
  }

  writeState(state) {
    writeJsonAtomicSync(this.stateFile, state);
  }
}

function backendEntrypoint(metadata, toolDir) {
  const configured = typeof metadata.entrypoints === "string"
    ? metadata.entrypoints
    : metadata.entrypoints?.backend;
  const relative = configured || (fs.existsSync(path.join(toolDir, "index.js")) ? "index.js" : "");
  if (!relative) return null;
  const target = path.resolve(toolDir, relative);
  if (!target.startsWith(`${path.resolve(toolDir)}${path.sep}`) || !fs.existsSync(target)) {
    throw new Error("Backend entrypoint is missing or unsafe.");
  }
  return target;
}

function assetRoots(metadata, toolDir) {
  const entries = Array.isArray(metadata.frontend_assets)
    ? metadata.frontend_assets
    : metadata.frontend_assets ? [metadata.frontend_assets] : ["public"];
  return entries.map((entry) => path.resolve(toolDir, entry))
    .filter((entry) => entry.startsWith(`${path.resolve(toolDir)}${path.sep}`) && fs.existsSync(entry));
}

function clearRequireCache(directory) {
  const prefix = `${path.resolve(directory)}${path.sep}`;
  for (const key of Object.keys(require.cache)) if (key.startsWith(prefix)) delete require.cache[key];
}

function compareVersions(left, right) {
  const parse = (value) => String(value || "0.0.0").split(/[+-]/)[0].split(".").map((part) => Number.parseInt(part, 10) || 0);
  const a = parse(left);
  const b = parse(right);
  for (let index = 0; index < Math.max(a.length, b.length, 3); index += 1) {
    if ((a[index] || 0) !== (b[index] || 0)) return (a[index] || 0) > (b[index] || 0) ? 1 : -1;
  }
  return 0;
}

function sourceSignature(local) {
  const entry = backendEntrypoint(local.metadata, local.dir);
  const entryMtime = entry && fs.existsSync(entry) ? fs.statSync(entry).mtimeMs : 0;
  const metadataFile = path.join(local.dir, "tool_info.json");
  const metadataMtime = fs.existsSync(metadataFile) ? fs.statSync(metadataFile).mtimeMs : 0;
  return `${local.metadata.version}:${metadataMtime}:${entryMtime}`;
}

function safeDiagnostics(callback) {
  if (typeof callback !== "function") return null;
  try {
    const value = callback();
    return value && typeof value === "object" && !Array.isArray(value) ? value : null;
  } catch (error) {
    return { error: String(error?.message || "Tool diagnostics failed.") };
  }
}

module.exports = {
  ToolLoader,
  assetRoots,
  backendEntrypoint,
  clearRequireCache,
  compareVersions,
  safeDiagnostics,
  sourceSignature
};
