const fs = require("fs");
const path = require("path");
const { getSetting } = require("./settings");
const { scanPluginDirectories, getPlugins, canonicalPluginId } = require("./plugins");
const {
  parseSemver,
  compareSemver,
  findSafeTarget,
  collectChangelogRange,
  normalizeVersions
} = require("./versioning");
const { listSnapshots } = require("./update-manager");
const { safeModeStatus } = require("./recovery-mode");
const {
  createMetadataReader,
  normalizeRepositoryTarget,
  normalizeRepositoryRef,
  resolveSourceBranch
} = require("./update-repository");

const repoRoot = path.join(__dirname, "..", "..");
const LUMI_AI_TOOL_ID = /^lumi_ai_[a-z0-9_-]+$/i;

function isLumiAiToolId(id) {
  return LUMI_AI_TOOL_ID.test(String(id || "")) && String(id || "") !== "lumi_ai";
}

function readJsonFromReader(reader, filePath) {
  const raw = reader.readFile(filePath);
  if (!raw) return null;
  try {
    return JSON.parse(raw);
  } catch {
    return null;
  }
}

function readLocalJson(filePath) {
  try {
    return JSON.parse(fs.readFileSync(path.join(repoRoot, filePath), "utf8"));
  } catch {
    return null;
  }
}

function parseMarkdownChangelog(raw) {
  if (!raw) return [];
  const entries = [];
  const lines = raw.split(/\r?\n/);
  let current = null;
  for (const line of lines) {
    const heading = line.match(/^#{1,3}\s+\[?v?(\d+\.\d+\.\d+)\]?(.+)?$/i);
    if (heading) {
      if (current) entries.push(current);
      current = { version: heading[1], title: line.replace(/^#+\s+/, ""), changes: [] };
    } else if (current && line.trim()) {
      current.changes.push(line.trim().replace(/^[-*]\s*/, ""));
    }
  }
  if (current) entries.push(current);
  return entries;
}

function changelogEntries(reader, basePath = "") {
  const json = readJsonFromReader(reader, path.posix.join(basePath, "changelog.json"));
  if (Array.isArray(json)) return json;
  if (Array.isArray(json?.versions)) return json.versions;
  const md = reader.readFile(path.posix.join(basePath, "CHANGELOG.md"));
  return parseMarkdownChangelog(md);
}

function manifestVersions(manifest, fallbackVersion) {
  const entries = [];
  if (Array.isArray(manifest?.versions)) entries.push(...manifest.versions);
  if (manifest?.version) entries.push({ ...manifest, version: manifest.version });
  if (fallbackVersion) entries.push({ version: fallbackVersion });
  return normalizeVersions(entries);
}

function manifestRawVersion(manifest, fallbackVersion) {
  return String(manifest?.version || fallbackVersion || "").trim();
}

function coreManifest(reader) {
  return readJsonFromReader(reader, "update-manifest.json") ||
    readJsonFromReader(reader, "lumi.manifest.json") ||
    readJsonFromReader(reader, "package.json") ||
    {};
}

function releaseCatalog(reader) {
  const raw = readJsonFromReader(reader, "release-index.json");
  const releases = Array.isArray(raw?.releases) ? raw.releases : [];
  return releases.map((release) => {
    const parsed = parseSemver(release?.version);
    if (!parsed || release?.installable === false) return null;
    try {
      return {
        ...release,
        version: parsed.raw,
        ref: normalizeRepositoryRef(release.ref)
      };
    } catch {
      return null;
    }
  }).filter(Boolean).sort((left, right) => compareSemver(right.version, left.version));
}

function availableVersionOptions(releases, kind, id = null) {
  const seen = new Set();
  const options = [];
  for (const release of releases) {
    const version = kind === "core"
      ? release.version
      : kind === "tool"
        ? release.tools?.[id]
        : release.plugins?.[id];
    if (!parseSemver(version) || seen.has(version)) continue;
    seen.add(version);
    options.push({
      version: parseSemver(version).raw,
      ref: release.ref,
      core_version: release.version,
      released_at: release.released_at || null,
      rollback_safe: release.rollback_safe !== false,
      data_policy: release.data_policy || "preserve",
      dependency_policy: release.dependency_policy || "sync_on_restart",
      migration_notes: release.migration_notes || "",
      replaces_versions: Array.isArray(release.replaces_versions) ? release.replaces_versions : []
    });
  }
  return options;
}

function localCoreVersion() {
  return readLocalJson("package.json")?.version || "0.0.0";
}

function latestEntry(entries) {
  return entries.length ? entries[entries.length - 1] : null;
}

function snapshotAvailability(kind, id = null) {
  const snapshots = listSnapshots()
    .filter((snap) => snap.type === kind || (kind === "core" && snap.type === "bot"))
    .filter((snap) => !id || snap.pluginId === id)
    .sort((a, b) => b.createdAt - a.createdAt);
  const latest = snapshots[0] || null;
  return {
    available: Boolean(latest),
    latest_snapshot_id: latest?.id || null,
    rollback_safe: latest?.rollback_safe !== false,
    latest
  };
}

function localPluginCandidates(registry) {
  const candidates = new Map();
  for (const plugin of scanPluginDirectories()) {
    const id = canonicalPluginId(plugin.id);
    candidates.set(id, { ...plugin, id, installed: true });
  }
  for (const plugin of registry.values()) {
    const id = canonicalPluginId(plugin.id);
    if (isLumiAiToolId(id)) {
      continue;
    }
    if (candidates.has(id)) {
      continue;
    }
    const pluginPath = plugin.path || "";
    const installed = Boolean(pluginPath && fs.existsSync(path.join(pluginPath, "plugin.json")));
    candidates.set(id, {
      id,
      registry_id: plugin.id,
      name: plugin.name || plugin.id,
      version: plugin.version || "0.0.0",
      description: "",
      main: "index.js",
      dir: pluginPath,
      installed
    });
  }
  return candidates;
}

function pluginRegistry() {
  const registry = new Map();
  for (const plugin of getPlugins()) {
    const id = canonicalPluginId(plugin.id);
    const existing = registry.get(id);
    if (!existing || plugin.id === id) {
      registry.set(id, { ...plugin, id, registry_id: plugin.id });
    }
  }
  return registry;
}

function localToolCandidates() {
  const tools = new Map();
  const pluginsPath = path.join(repoRoot, "plugins");
  let entries = [];
  try {
    entries = fs.readdirSync(pluginsPath, { withFileTypes: true });
  } catch {
    return tools;
  }
  for (const entry of entries) {
    if (!entry.isDirectory() || !isLumiAiToolId(entry.name)) {
      continue;
    }
    const metadataPath = path.join(pluginsPath, entry.name, "tool_info.json");
    if (!fs.existsSync(metadataPath)) {
      continue;
    }
    try {
      const metadata = JSON.parse(fs.readFileSync(metadataPath, "utf8"));
      tools.set(entry.name, {
        id: metadata.tool_id || entry.name,
        dir: path.join(pluginsPath, entry.name),
        metadata,
        installed: true
      });
    } catch {
      continue;
    }
  }
  return tools;
}

function toolDisplayName(metadata, id) {
  return metadata?.display_name || metadata?.name || id;
}

function buildToolStatus({ id, localMetadata, remoteMetadata, sourceBranch, installed = true }) {
  const metadata = remoteMetadata || localMetadata || {};
  const currentVersion = installed
    ? manifestRawVersion(localMetadata, localMetadata?.version) || "0.0.0"
    : "0.0.0";
  const targetVersion = manifestRawVersion(remoteMetadata, remoteMetadata?.version) || currentVersion;
  const currentIsVersioned = Boolean(parseSemver(currentVersion));
  const targetIsVersioned = Boolean(parseSemver(targetVersion));
  const updateAvailable = installed === false
    ? Boolean(remoteMetadata)
    : Boolean(remoteMetadata) && (
        currentIsVersioned && targetIsVersioned
          ? compareSemver(targetVersion, currentVersion) > 0
          : targetVersion !== currentVersion
      );
  const warnings = [];
  if (!remoteMetadata) warnings.push("Remote tool metadata is missing.");
  if (remoteMetadata?.remote_invalid) warnings.push(remoteMetadata.remote_error || "Remote tool metadata is invalid.");
  return {
    kind: "tool",
    id,
    tool_id: metadata.tool_id || id,
    name: toolDisplayName(metadata, id),
    installed,
    current_version: currentVersion,
    latest_available_version: targetVersion,
    safe_target_version: targetVersion || null,
    update_available: updateAvailable,
    blocked: Boolean(remoteMetadata?.remote_invalid),
    blocked_reason: remoteMetadata?.remote_error || null,
    source_branch: sourceBranch,
    channel: "lumi_ai",
    version_description: installed === false
      ? `Available to install at ${targetVersion || "unknown"}`
      : `${currentVersion} -> ${targetVersion || currentVersion}`,
    description: metadata.description || "",
    capabilities: Array.isArray(metadata.capabilities) ? metadata.capabilities : [],
    limitations: Array.isArray(metadata.limitations) ? metadata.limitations : [],
    minimum_lumi_ai_version: metadata.minimum_lumi_ai_version || "",
    repository_path: metadata.repository_path || `plugins/${id}`,
    warnings,
    dangers: remoteMetadata?.remote_invalid ? [remoteMetadata.remote_error || "Remote tool metadata is invalid."] : [],
    metadata
  };
}

function buildStatus({ kind, id, name, currentVersion, manifest, changelog, sourceBranch, channel, installed = true }) {
  const versions = manifestVersions(manifest, manifest?.version);
  const rawVersion = manifestRawVersion(manifest);
  const targetVersion = rawVersion || latestEntry(versions)?.version || "";
  const currentIsVersioned = Boolean(parseSemver(currentVersion));
  const targetIsVersioned = Boolean(parseSemver(targetVersion));
  const unversionedUpdate = !currentIsVersioned || !targetIsVersioned;
  const latest = latestEntry(versions);
  const unversionedTarget = {
    ...(latest || manifest || {}),
    version: targetVersion || "unversioned",
    rollback_safe: false,
    unversioned: true
  };
  const replacementTarget = versions.filter((entry) =>
    Array.isArray(entry.replaces_versions) && entry.replaces_versions.includes(currentVersion)
  ).at(-1);
  const targetResult = replacementTarget
    ? {
        target: replacementTarget,
        latest: latest || replacementTarget,
        blocked: false,
        replacement: true,
        warning: `Version ${currentVersion} was published with an incorrect version number; install ${replacementTarget.version} as its supported replacement.`
      }
    : unversionedUpdate
    ? {
        target: unversionedTarget,
        latest: latest || unversionedTarget,
        blocked: false,
        warning: "This update involves an unversioned source or target. It is available as a manual repo update, but version ordering, changelog range, and rollback safety cannot be verified."
      }
    : findSafeTarget(currentVersion, versions);
  const target = targetResult.target;
  const range = target && !unversionedUpdate
    ? targetResult.replacement
      ? (changelog.length ? changelog : versions).filter((entry) => entry.version === target.version)
      : collectChangelogRange(currentVersion, target.version, changelog.length ? changelog : versions)
    : changelog;
  const warnings = [];
  const dangers = [];
  if (!changelog.length) warnings.push("Changelog metadata is missing.");
  if (targetResult.warning) warnings.push(targetResult.warning);
  if (targetResult.blocked) dangers.push(targetResult.reason);
  if (target?.rollback_safe === false) warnings.push("Target metadata marks rollback as unsafe after migration.");
  return {
    kind,
    id: id || kind,
    installed,
    name: name || manifest?.name || id || "Lumi core",
    current_version: currentVersion,
    latest_available_version: latest?.version || rawVersion || currentVersion,
    safe_target_version: target?.version || null,
    update_available: Boolean(target && (targetResult.replacement || unversionedUpdate || compareSemver(target.version, currentVersion) > 0)),
    blocked: Boolean(targetResult.blocked),
    blocked_reason: targetResult.reason || null,
    source_branch: sourceBranch,
    channel: manifest?.channel || channel || "stable",
    version_description: target
      ? `${currentVersion} -> ${target.version}`
      : targetResult.reason || "No safe update target available.",
    changelog_range: range,
    size_delta: target?.size || manifest?.size || null,
    size_delta_label: target?.size || manifest?.size ? String(target.size || manifest.size) : "unknown",
    warnings,
    dangers,
    unversioned_update: unversionedUpdate,
    requires_manual_confirmation: unversionedUpdate || Boolean(targetResult.replacement),
    version_correction: Boolean(targetResult.replacement),
    requirements: target?.requirements || manifest?.requirements || [],
    migration_notes: target?.migration_notes || manifest?.migration_notes || "",
    rollback_safe: target?.rollback_safe !== false,
    major_crossing: target && !unversionedUpdate ? target.version.split(".")[0] !== String(currentVersion).split(".")[0] : false,
    snapshot: snapshotAvailability(kind === "plugin" ? "plugin" : "bot", id),
    raw_target: target || null
  };
}

function getUpdateStatus(options = {}) {
  const configuredRemote = normalizeRepositoryTarget(options.remote || getSetting("git_remote", "origin"));
  const requestedSource = options.source || "stable";
  const sourceBranch = resolveSourceBranch(configuredRemote, requestedSource);
  const reader = createMetadataReader(configuredRemote, sourceBranch);
  const remote = reader.repository || configuredRemote;
  try {
    const releases = releaseCatalog(reader);
    const core = buildStatus({
      kind: "core",
      currentVersion: localCoreVersion(),
      manifest: coreManifest(reader),
      changelog: changelogEntries(reader),
      sourceBranch,
      channel: requestedSource === "experimental" ? "experimental" : "stable"
    });
    core.available_versions = availableVersionOptions(releases, "core");
    const registry = pluginRegistry();
    const remoteDirs = new Set(reader.listPluginDirs());
    const remotePluginDirs = new Set();
    const remoteTools = new Map();
    const candidates = localPluginCandidates(registry);
    for (const pluginId of remoteDirs) {
      if (isLumiAiToolId(pluginId)) {
        const toolMetadata = readJsonFromReader(reader, `plugins/${pluginId}/tool_info.json`);
        if (toolMetadata) {
          remoteTools.set(pluginId, {
            ...toolMetadata,
            repository_path: toolMetadata.repository_path || `plugins/${pluginId}`
          });
          continue;
        }
      }
      const manifest = readJsonFromReader(reader, `plugins/${pluginId}/plugin.json`);
      if (!manifest) {
        continue;
      }
      remotePluginDirs.add(pluginId);
      const canonicalRemoteId = canonicalPluginId(pluginId);
      if (candidates.has(canonicalRemoteId)) {
        continue;
      }
      candidates.set(canonicalRemoteId, {
        id: canonicalRemoteId,
        name: manifest.name || pluginId,
        version: "0.0.0",
        description: manifest.description || "",
        main: manifest.main || "index.js",
        dir: "",
        installed: false
      });
    }
    const installed = Array.from(candidates.values());
    const plugins = installed.map((plugin) => {
      const basePath = `plugins/${plugin.id}`;
      const manifest = readJsonFromReader(reader, `${basePath}/plugin.json`) || {};
      const changelog = changelogEntries(reader, basePath);
      const dbPlugin = registry.get(plugin.id);
      const status = buildStatus({
        kind: "plugin",
        id: plugin.id,
        name: plugin.name,
        currentVersion: dbPlugin?.version || plugin.version || "0.0.0",
        manifest: remotePluginDirs.has(plugin.id) ? manifest : { version: plugin.version, name: plugin.name },
        changelog,
        sourceBranch,
        channel: requestedSource === "experimental" ? "experimental" : "stable",
        installed: plugin.installed !== false
      });
      status.available_versions = availableVersionOptions(releases, "plugin", plugin.id);
      return status;
    });
    const localTools = localToolCandidates();
    for (const [toolId, tool] of localTools.entries()) {
      if (!remoteTools.has(toolId)) {
        remoteTools.set(toolId, null);
      }
    }
    const aiTools = Array.from(new Set([
      ...Array.from(localTools.keys()),
      ...Array.from(remoteTools.keys())
    ])).sort((a, b) => a.localeCompare(b)).map((toolId) => {
      const localTool = localTools.get(toolId);
      const status = buildToolStatus({
        id: toolId,
        localMetadata: localTool?.metadata || null,
        remoteMetadata: remoteTools.get(toolId) || null,
        sourceBranch,
        installed: localTool?.installed === true
      });
      status.available_versions = availableVersionOptions(releases, "tool", toolId);
      return status;
    });
    const lumiAiPlugin = plugins.find((plugin) => plugin.id === "lumi_ai");
    if (lumiAiPlugin) {
      lumiAiPlugin.tools = aiTools;
      lumiAiPlugin.tools_summary = {
        installed_tools: aiTools.filter((tool) => tool.installed !== false).length,
        total_tools: aiTools.length,
        updatable_tools: aiTools.filter((tool) => tool.update_available && tool.installed !== false).length,
        available_tools: aiTools.filter((tool) => tool.installed === false).length,
        blocked_tools: aiTools.filter((tool) => tool.blocked).length
      };
    }
    return {
      generated_at: new Date().toISOString(),
      source_branch: sourceBranch,
      requested_source: requestedSource,
      remote,
      release_catalog: releases,
      core,
      plugins,
      ai_tools: aiTools,
      tools_summary: {
        installed_tools: aiTools.filter((tool) => tool.installed !== false).length,
        total_tools: aiTools.length,
        updatable_tools: aiTools.filter((tool) => tool.update_available && tool.installed !== false).length,
        available_tools: aiTools.filter((tool) => tool.installed === false).length,
        blocked_tools: aiTools.filter((tool) => tool.blocked).length
      },
      plugins_summary: {
        installed_plugins: installed.filter((plugin) => plugin.installed !== false).length,
        total_plugins: remotePluginDirs.size || installed.length,
        updatable_plugins: plugins.filter((plugin) => plugin.update_available).length,
        blocked_plugins: plugins.filter((plugin) => plugin.blocked).length
      },
      recovery: safeModeStatus()
    };
  } finally {
    reader.cleanup();
  }
}

module.exports = {
  availableVersionOptions,
  buildStatus,
  getUpdateStatus,
  releaseCatalog,
  resolveSourceBranch
};
