// Resolves bundled plugin source metadata from package manifests.
import path from "node:path";
import { isRecord } from "@openclaw/normalization-core/record-coerce";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { discoverOpenClawPlugins, type PluginDiscoveryResult } from "./discovery.js";
import { loadPluginManifest } from "./manifest.js";

export type BundledPluginSource = {
  pluginId: string;
  localPath: string;
  npmSpec?: string;
  version?: string;
  configSchema?: Record<string, unknown>;
  requiresConfig?: boolean;
};

type BundledPluginLookup =
  | { kind: "localPath"; value: string }
  | { kind: "npmSpec"; value: string }
  | { kind: "pluginId"; value: string };

export function findBundledPluginSourceInMap(params: {
  bundled: ReadonlyMap<string, BundledPluginSource>;
  lookup: BundledPluginLookup;
}): BundledPluginSource | undefined {
  const targetValue = params.lookup.value.trim();
  if (!targetValue) {
    return undefined;
  }
  if (params.lookup.kind === "pluginId") {
    return params.bundled.get(targetValue);
  }
  for (const source of params.bundled.values()) {
    if (
      (params.lookup.kind === "npmSpec" && source.npmSpec === targetValue) ||
      (params.lookup.kind === "localPath" &&
        path.resolve(source.localPath) === path.resolve(targetValue))
    ) {
      return source;
    }
  }
  return undefined;
}

export function resolveBundledPluginSources(params: {
  workspaceDir?: string;
  /** Use an explicit env when bundled roots should resolve independently from process.env. */
  env?: NodeJS.ProcessEnv;
  discovery?: PluginDiscoveryResult;
}): Map<string, BundledPluginSource> {
  const discovery =
    params.discovery ??
    discoverOpenClawPlugins({ workspaceDir: params.workspaceDir, env: params.env });
  const bundled = new Map<string, BundledPluginSource>();

  for (const candidate of discovery.candidates) {
    if (candidate.origin !== "bundled") {
      continue;
    }
    const manifest = loadPluginManifest(candidate.rootDir, false);
    if (!manifest.ok) {
      continue;
    }
    const pluginId = manifest.manifest.id;
    if (bundled.has(pluginId)) {
      continue;
    }

    const npmSpec =
      normalizeOptionalString(candidate.packageManifest?.install?.npmSpec) ||
      normalizeOptionalString(candidate.packageName) ||
      undefined;

    const version =
      normalizeOptionalString(candidate.packageVersion) ||
      normalizeOptionalString(manifest.manifest.version) ||
      undefined;

    bundled.set(pluginId, {
      pluginId,
      localPath: candidate.rootDir,
      npmSpec,
      version,
      ...(isRecord(manifest.manifest.configSchema)
        ? { configSchema: manifest.manifest.configSchema }
        : {}),
      requiresConfig: pluginConfigSchemaHasRequiredFields(manifest.manifest.configSchema),
    });
  }

  return bundled;
}

let processBundledPluginSources: ReadonlyMap<string, BundledPluginSource> | undefined;

/** Bundled manifests are process-stable; installs and metadata changes require restart. */
export function getProcessBundledPluginSources(): ReadonlyMap<string, BundledPluginSource> {
  processBundledPluginSources ??= resolveBundledPluginSources({});
  return processBundledPluginSources;
}

function pluginConfigSchemaHasRequiredFields(schema: unknown): boolean {
  if (!isRecord(schema)) {
    return false;
  }
  const required = schema.required;
  return Array.isArray(required) && required.some((entry) => typeof entry === "string");
}

export function findBundledPluginSource(params: {
  lookup: BundledPluginLookup;
  workspaceDir?: string;
  /** Use an explicit env when bundled roots should resolve independently from process.env. */
  env?: NodeJS.ProcessEnv;
}): BundledPluginSource | undefined {
  const bundled = resolveBundledPluginSources({
    workspaceDir: params.workspaceDir,
    env: params.env,
  });
  return findBundledPluginSourceInMap({
    bundled,
    lookup: params.lookup,
  });
}

export function resolveBundledPluginInstallCommandHint(params: {
  pluginId: string;
  workspaceDir?: string;
  /** Use an explicit env when bundled roots should resolve independently from process.env. */
  env?: NodeJS.ProcessEnv;
}): string | null {
  const bundledSource = findBundledPluginSource({
    lookup: { kind: "pluginId", value: params.pluginId },
    workspaceDir: params.workspaceDir,
    env: params.env,
  });
  if (!bundledSource?.localPath) {
    return null;
  }
  return `openclaw plugins install ${bundledSource.localPath}`;
}
