import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import type { LegacyConfigRule } from "../config/legacy.shared.js";
import type { OpenClawConfig } from "../config/types.js";
import { asNullableRecord } from "../shared/record-coerce.js";
import { discoverOpenClawPlugins } from "./discovery.js";
import { getCachedPluginJitiLoader, type PluginJitiLoaderCache } from "./jiti-loader-cache.js";
import { loadPluginManifestRegistry } from "./manifest-registry.js";
import { resolvePluginCacheInputs, type PluginSourceRoots } from "./roots.js";

const CONTRACT_API_EXTENSIONS = [".js", ".mjs", ".cjs", ".ts", ".mts", ".cts"] as const;
const CURRENT_MODULE_PATH = fileURLToPath(import.meta.url);
const RUNNING_FROM_BUILT_ARTIFACT =
  CURRENT_MODULE_PATH.includes(`${path.sep}dist${path.sep}`) ||
  CURRENT_MODULE_PATH.includes(`${path.sep}dist-runtime${path.sep}`);

type PluginDoctorContractModule = {
  legacyConfigRules?: unknown;
  normalizeCompatibilityConfig?: unknown;
};

type PluginDoctorCompatibilityMutation = {
  config: OpenClawConfig;
  changes: string[];
};

type PluginDoctorCompatibilityNormalizer = (params: {
  cfg: OpenClawConfig;
}) => PluginDoctorCompatibilityMutation;

type PluginDoctorContractEntry = {
  pluginId: string;
  rules: LegacyConfigRule[];
  normalizeCompatibilityConfig?: PluginDoctorCompatibilityNormalizer;
};

type PluginManifestRegistryRecord = ReturnType<
  typeof loadPluginManifestRegistry
>["plugins"][number];

const jitiLoaders: PluginJitiLoaderCache = new Map();
const doctorContractCache = new Map<string, PluginDoctorContractEntry[]>();
const doctorContractRecordCache = new Map<string, Map<string, PluginDoctorContractEntry | null>>();

function getJiti(modulePath: string) {
  return getCachedPluginJitiLoader({
    cache: jitiLoaders,
    modulePath,
    importerUrl: import.meta.url,
  });
}

function buildDoctorContractCacheKey(params: {
  workspaceDir?: string;
  env?: NodeJS.ProcessEnv;
  pluginIds?: readonly string[];
}): string {
  return JSON.stringify({
    ...resolveDoctorContractBaseCachePayload(params),
    pluginIds: [...(params.pluginIds ?? [])].toSorted(),
  });
}

function buildDoctorContractBaseCacheKey(params: {
  workspaceDir?: string;
  env?: NodeJS.ProcessEnv;
}): string {
  return JSON.stringify(resolveDoctorContractBaseCachePayload(params));
}

function resolveDoctorContractBaseCachePayload(params: {
  workspaceDir?: string;
  env?: NodeJS.ProcessEnv;
}): {
  roots: PluginSourceRoots;
  loadPaths: string[];
} {
  const { roots, loadPaths } = resolvePluginCacheInputs({
    workspaceDir: params.workspaceDir,
    env: params.env,
  });
  return { roots, loadPaths };
}

function resolveContractApiPath(rootDir: string): string | null {
  const orderedExtensions = RUNNING_FROM_BUILT_ARTIFACT
    ? CONTRACT_API_EXTENSIONS
    : ([...CONTRACT_API_EXTENSIONS.slice(3), ...CONTRACT_API_EXTENSIONS.slice(0, 3)] as const);
  for (const extension of orderedExtensions) {
    const candidate = path.join(rootDir, `doctor-contract-api${extension}`);
    if (fs.existsSync(candidate)) {
      return candidate;
    }
  }
  for (const extension of orderedExtensions) {
    const candidate = path.join(rootDir, `contract-api${extension}`);
    if (fs.existsSync(candidate)) {
      return candidate;
    }
  }
  return null;
}

function coerceLegacyConfigRules(value: unknown): LegacyConfigRule[] {
  if (!Array.isArray(value)) {
    return [];
  }
  return value.filter((entry) => {
    if (!entry || typeof entry !== "object") {
      return false;
    }
    const candidate = entry as { path?: unknown; message?: unknown };
    return Array.isArray(candidate.path) && typeof candidate.message === "string";
  }) as LegacyConfigRule[];
}

function coerceNormalizeCompatibilityConfig(
  value: unknown,
): PluginDoctorCompatibilityNormalizer | undefined {
  return typeof value === "function" ? (value as PluginDoctorCompatibilityNormalizer) : undefined;
}

function hasLegacyElevenLabsTalkFields(raw: unknown): boolean {
  const talk = asNullableRecord(asNullableRecord(raw)?.talk);
  if (!talk) {
    return false;
  }
  return ["voiceId", "voiceAliases", "modelId", "outputFormat", "apiKey"].some((key) =>
    Object.prototype.hasOwnProperty.call(talk, key),
  );
}

export function collectRelevantDoctorPluginIds(raw: unknown): string[] {
  const ids = new Set<string>();
  const root = asNullableRecord(raw);
  if (!root) {
    return [];
  }

  const channels = asNullableRecord(root.channels);
  if (channels) {
    for (const channelId of Object.keys(channels)) {
      if (channelId !== "defaults") {
        ids.add(channelId);
      }
    }
  }

  const pluginsEntries = asNullableRecord(asNullableRecord(root.plugins)?.entries);
  if (pluginsEntries) {
    for (const pluginId of Object.keys(pluginsEntries)) {
      ids.add(pluginId);
    }
  }

  if (hasLegacyElevenLabsTalkFields(root)) {
    ids.add("elevenlabs");
  }

  return [...ids].toSorted();
}

export function collectRelevantDoctorPluginIdsForTouchedPaths(params: {
  raw: unknown;
  touchedPaths: ReadonlyArray<ReadonlyArray<string>>;
}): string[] {
  const root = asNullableRecord(params.raw);
  if (!root) {
    return [];
  }

  const ids = new Set<string>();
  for (const touchedPath of params.touchedPaths) {
    const [first, second, third] = touchedPath;
    if (first === "channels") {
      if (!second) {
        return collectRelevantDoctorPluginIds(params.raw);
      }
      if (second !== "defaults") {
        ids.add(second);
      }
      continue;
    }
    if (first === "plugins") {
      if (second !== "entries" || !third) {
        return collectRelevantDoctorPluginIds(params.raw);
      }
      ids.add(third);
      continue;
    }
    if (first === "talk" && hasLegacyElevenLabsTalkFields(root)) {
      ids.add("elevenlabs");
    }
  }

  return [...ids].toSorted();
}

function getDoctorContractRecordCache(
  baseCacheKey: string,
): Map<string, PluginDoctorContractEntry | null> {
  let cache = doctorContractRecordCache.get(baseCacheKey);
  if (!cache) {
    cache = new Map();
    doctorContractRecordCache.set(baseCacheKey, cache);
  }
  return cache;
}

function loadPluginDoctorContractEntry(
  record: PluginManifestRegistryRecord,
  baseCacheKey: string,
): PluginDoctorContractEntry | null {
  const cache = getDoctorContractRecordCache(baseCacheKey);
  const cached = cache.get(record.id);
  if (cached !== undefined) {
    return cached;
  }

  const contractSource = resolveContractApiPath(record.rootDir);
  if (!contractSource) {
    cache.set(record.id, null);
    return null;
  }
  let mod: PluginDoctorContractModule;
  try {
    mod = getJiti(contractSource)(contractSource) as PluginDoctorContractModule;
  } catch {
    cache.set(record.id, null);
    return null;
  }
  const rules = coerceLegacyConfigRules(
    (mod as { default?: PluginDoctorContractModule }).default?.legacyConfigRules ??
      mod.legacyConfigRules,
  );
  const normalizeCompatibilityConfig = coerceNormalizeCompatibilityConfig(
    mod.normalizeCompatibilityConfig ??
      (mod as { default?: PluginDoctorContractModule }).default?.normalizeCompatibilityConfig,
  );
  if (rules.length === 0 && !normalizeCompatibilityConfig) {
    cache.set(record.id, null);
    return null;
  }
  const entry = {
    pluginId: record.id,
    rules,
    normalizeCompatibilityConfig,
  };
  cache.set(record.id, entry);
  return entry;
}

function resolvePluginDoctorContracts(params?: {
  workspaceDir?: string;
  env?: NodeJS.ProcessEnv;
  pluginIds?: readonly string[];
}): PluginDoctorContractEntry[] {
  const env = params?.env ?? process.env;
  const baseCacheKey = buildDoctorContractBaseCacheKey({
    workspaceDir: params?.workspaceDir,
    env,
  });
  const cacheKey = buildDoctorContractCacheKey({
    workspaceDir: params?.workspaceDir,
    env,
    pluginIds: params?.pluginIds,
  });
  const cached = doctorContractCache.get(cacheKey);
  if (cached) {
    return cached;
  }

  if (params?.pluginIds && params.pluginIds.length === 0) {
    doctorContractCache.set(cacheKey, []);
    return [];
  }

  const discovery = discoverOpenClawPlugins({
    workspaceDir: params?.workspaceDir,
    env,
    cache: true,
  });
  const manifestRegistry = loadPluginManifestRegistry({
    workspaceDir: params?.workspaceDir,
    env,
    cache: true,
    candidates: discovery.candidates,
    diagnostics: discovery.diagnostics,
  });

  const entries: PluginDoctorContractEntry[] = [];
  const selectedPluginIds = params?.pluginIds ? new Set(params.pluginIds) : null;
  for (const record of manifestRegistry.plugins) {
    if (
      selectedPluginIds &&
      !selectedPluginIds.has(record.id) &&
      !record.channels.some((channelId) => selectedPluginIds.has(channelId)) &&
      !record.providers.some((providerId) => selectedPluginIds.has(providerId))
    ) {
      continue;
    }
    const entry = loadPluginDoctorContractEntry(record, baseCacheKey);
    if (entry) {
      entries.push(entry);
    }
  }

  doctorContractCache.set(cacheKey, entries);
  return entries;
}

export function clearPluginDoctorContractRegistryCache(): void {
  doctorContractCache.clear();
  doctorContractRecordCache.clear();
  jitiLoaders.clear();
}

export function listPluginDoctorLegacyConfigRules(params?: {
  workspaceDir?: string;
  env?: NodeJS.ProcessEnv;
  pluginIds?: readonly string[];
}): LegacyConfigRule[] {
  return resolvePluginDoctorContracts(params).flatMap((entry) => entry.rules);
}

export function applyPluginDoctorCompatibilityMigrations(
  cfg: OpenClawConfig,
  params?: {
    workspaceDir?: string;
    env?: NodeJS.ProcessEnv;
    pluginIds?: readonly string[];
  },
): {
  config: OpenClawConfig;
  changes: string[];
} {
  let nextCfg = cfg;
  const changes: string[] = [];
  for (const entry of resolvePluginDoctorContracts(params)) {
    const mutation = entry.normalizeCompatibilityConfig?.({ cfg: nextCfg });
    if (!mutation || mutation.changes.length === 0) {
      continue;
    }
    nextCfg = mutation.config;
    changes.push(...mutation.changes);
  }
  return { config: nextCfg, changes };
}
