/**
 * Resolves CLI runtime backends registered by plugins or setup metadata.
 */
import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { ContextEngineHostCapability } from "../context-engine/types.js";
import type {
  CliBackendConfig,
  CliBackendLiveSessionRequirement,
  CliBackendRuntimeArtifactPolicy,
} from "../plugins/cli-backend.types.js";
import { resolveRuntimeCliBackends } from "../plugins/cli-backends.runtime.js";
import {
  resolvePluginSetupCliBackend,
  resolvePluginSetupRegistry,
} from "../plugins/setup-registry.js";
import { resolveRuntimeTextTransforms } from "../plugins/text-transforms.runtime.js";
import type {
  CliBackendAuthEpochMode,
  CliBackendNormalizeConfigContext,
  CliBundleMcpMode,
  CliBackendPlugin,
  CliBackendNativeToolMode,
  CliBackendSideQuestionToolMode,
  CliBackendToolAvailabilityEnforcement,
  PluginTextTransforms,
} from "../plugins/types.js";
import { mergePluginTextTransforms } from "./plugin-text-transforms.js";

type CliBackendsDeps = {
  resolvePluginSetupCliBackend: typeof resolvePluginSetupCliBackend;
  resolvePluginSetupRegistry: typeof resolvePluginSetupRegistry;
  resolveRuntimeCliBackends: typeof resolveRuntimeCliBackends;
};

const defaultCliBackendsDeps: CliBackendsDeps = {
  resolvePluginSetupCliBackend,
  resolvePluginSetupRegistry,
  resolveRuntimeCliBackends,
};

let cliBackendsDeps: CliBackendsDeps = defaultCliBackendsDeps;

/** Fully merged CLI backend definition used by agent runner execution. */
export type ResolvedCliBackend = {
  id: string;
  modelProvider?: string;
  config: CliBackendConfig;
  bundleMcp: boolean;
  bundleMcpMode?: CliBundleMcpMode;
  pluginId?: string;
  transformSystemPrompt?: CliBackendPlugin["transformSystemPrompt"];
  textTransforms?: PluginTextTransforms;
  defaultAuthProfileId?: string;
  authEpochMode?: CliBackendAuthEpochMode;
  autoSelectAuthProfile?: boolean;
  contextEngineHostCapabilities?: readonly ContextEngineHostCapability[];
  ownsNativeCompaction?: boolean;
  prepareExecution?: CliBackendPlugin["prepareExecution"];
  resolveExecutionArgs?: CliBackendPlugin["resolveExecutionArgs"];
  parseJsonlEvent?: CliBackendPlugin["parseJsonlEvent"];
  toolAvailabilityEnforcement?: CliBackendToolAvailabilityEnforcement;
  nativeToolMode?: CliBackendNativeToolMode;
  sideQuestionToolMode?: CliBackendSideQuestionToolMode;
  runtimeArtifact?: CliBackendRuntimeArtifactPolicy;
  liveSessionRequirement?: CliBackendLiveSessionRequirement;
};

type ResolvedCliBackendLiveTest = {
  defaultModelRef?: string;
  defaultImageProbe: boolean;
  defaultMcpProbe: boolean;
  dockerNpmPackage?: string;
  dockerBinaryName?: string;
};

/** Binding between a model provider and the CLI runtime that serves it. */
type CliRuntimeModelBackendBinding = {
  provider: string;
  runtime: string;
  pluginId?: string;
};

type FallbackCliBackendPolicy = {
  modelProvider?: string;
  bundleMcp: boolean;
  bundleMcpMode?: CliBundleMcpMode;
  baseConfig?: CliBackendConfig;
  normalizeConfig?: (
    config: CliBackendConfig,
    context?: CliBackendNormalizeConfigContext,
  ) => CliBackendConfig;
  transformSystemPrompt?: CliBackendPlugin["transformSystemPrompt"];
  textTransforms?: PluginTextTransforms;
  defaultAuthProfileId?: string;
  authEpochMode?: CliBackendAuthEpochMode;
  autoSelectAuthProfile?: boolean;
  contextEngineHostCapabilities?: readonly ContextEngineHostCapability[];
  ownsNativeCompaction?: boolean;
  prepareExecution?: CliBackendPlugin["prepareExecution"];
  resolveExecutionArgs?: CliBackendPlugin["resolveExecutionArgs"];
  parseJsonlEvent?: CliBackendPlugin["parseJsonlEvent"];
  toolAvailabilityEnforcement?: CliBackendToolAvailabilityEnforcement;
  nativeToolMode?: CliBackendNativeToolMode;
  sideQuestionToolMode?: CliBackendSideQuestionToolMode;
  runtimeArtifact?: CliBackendRuntimeArtifactPolicy;
  liveSessionRequirement?: CliBackendLiveSessionRequirement;
};

const FALLBACK_CLI_BACKEND_POLICIES: Record<string, FallbackCliBackendPolicy> = {};

function normalizeBundleMcpMode(
  mode: CliBundleMcpMode | undefined,
  enabled: boolean,
): CliBundleMcpMode | undefined {
  if (!enabled) {
    return undefined;
  }
  return mode ?? "claude-config-file";
}

function resolveToolAvailabilityEnforcement(
  backend: Pick<
    CliBackendPlugin,
    "nativeToolMode" | "resolveExecutionArgs" | "toolAvailabilityEnforcement"
  > & { builtWithOpenClawVersion?: string },
): CliBackendToolAvailabilityEnforcement | undefined {
  if (backend.toolAvailabilityEnforcement) {
    return backend.toolAvailabilityEnforcement;
  }
  // v2026.7.2-beta.1 through .3 made selectable + resolveExecutionArgs the
  // public enforcement contract. Require matching package build provenance so
  // a new no-op hook cannot be mistaken for that shipped SDK path.
  const builtWith = backend.builtWithOpenClawVersion?.replace(/^v/u, "");
  const isShippedBetaContract = /^2026\.7\.2-beta\.[123]$/u.test(builtWith ?? "");
  return isShippedBetaContract &&
    backend.nativeToolMode === "selectable" &&
    backend.resolveExecutionArgs
    ? "execution-args"
    : undefined;
}

function resolveSetupCliBackendPolicy(provider: string): FallbackCliBackendPolicy | undefined {
  const entry = cliBackendsDeps.resolvePluginSetupCliBackend({
    backend: provider,
  });
  if (!entry) {
    return undefined;
  }
  return {
    // Setup-registered backends keep narrow CLI paths generic even when the
    // runtime plugin registry has not booted yet.
    bundleMcp: entry.backend.bundleMcp === true,
    modelProvider: resolveCliBackendModelProvider(entry.backend),
    bundleMcpMode: normalizeBundleMcpMode(
      entry.backend.bundleMcpMode,
      entry.backend.bundleMcp === true,
    ),
    baseConfig: entry.backend.config,
    normalizeConfig: entry.backend.normalizeConfig,
    transformSystemPrompt: entry.backend.transformSystemPrompt,
    textTransforms: entry.backend.textTransforms,
    defaultAuthProfileId: entry.backend.defaultAuthProfileId,
    authEpochMode: entry.backend.authEpochMode,
    autoSelectAuthProfile: entry.backend.autoSelectAuthProfile,
    contextEngineHostCapabilities: entry.backend.contextEngineHostCapabilities,
    ownsNativeCompaction: entry.backend.ownsNativeCompaction,
    prepareExecution: entry.backend.prepareExecution,
    resolveExecutionArgs: entry.backend.resolveExecutionArgs,
    parseJsonlEvent: entry.backend.parseJsonlEvent,
    toolAvailabilityEnforcement: entry.backend.toolAvailabilityEnforcement,
    nativeToolMode: entry.backend.nativeToolMode,
    sideQuestionToolMode: entry.backend.sideQuestionToolMode,
    runtimeArtifact: entry.backend.runtimeArtifact,
    liveSessionRequirement: entry.backend.liveSessionRequirement,
  };
}

function resolveFallbackCliBackendPolicy(provider: string): FallbackCliBackendPolicy | undefined {
  return FALLBACK_CLI_BACKEND_POLICIES[provider] ?? resolveSetupCliBackendPolicy(provider);
}

function normalizeBackendKey(key: string): string {
  return normalizeProviderId(key);
}

function resolveRegisteredBackend(provider: string) {
  const normalized = normalizeBackendKey(provider);
  return cliBackendsDeps
    .resolveRuntimeCliBackends()
    .find((entry) => normalizeBackendKey(entry.id) === normalized);
}

function resolveCliBackendModelProvider(
  backend: Pick<CliBackendPlugin, "modelProvider">,
): string | undefined {
  const provider = backend.modelProvider?.trim();
  return provider ? normalizeProviderId(provider) : undefined;
}

function addCliRuntimeModelBinding(
  bindings: Map<string, CliRuntimeModelBackendBinding>,
  params: { backend: CliBackendPlugin; pluginId?: string },
): void {
  const provider = resolveCliBackendModelProvider(params.backend);
  const runtime = normalizeBackendKey(params.backend.id);
  if (!provider || !runtime) {
    return;
  }
  bindings.set(`${provider}:${runtime}`, {
    provider,
    runtime,
    ...(params.pluginId ? { pluginId: params.pluginId } : {}),
  });
}

/** Lists model-provider to CLI-runtime bindings from runtime and optional setup registries. */
export function listCliRuntimeModelBackendBindings(
  params: {
    config?: OpenClawConfig;
    env?: NodeJS.ProcessEnv;
    includeSetupRegistry?: boolean;
  } = {},
): CliRuntimeModelBackendBinding[] {
  const bindings = new Map<string, CliRuntimeModelBackendBinding>();
  for (const backend of cliBackendsDeps.resolveRuntimeCliBackends()) {
    addCliRuntimeModelBinding(bindings, {
      backend,
      ...(backend.pluginId ? { pluginId: backend.pluginId } : {}),
    });
  }
  if (params.includeSetupRegistry === true) {
    for (const entry of cliBackendsDeps.resolvePluginSetupRegistry({
      config: params.config,
      env: params.env,
    }).cliBackends) {
      addCliRuntimeModelBinding(bindings, {
        backend: entry.backend,
        pluginId: entry.pluginId,
      });
    }
  }
  return [...bindings.values()].toSorted((left, right) =>
    left.provider === right.provider
      ? left.runtime.localeCompare(right.runtime)
      : left.provider.localeCompare(right.provider),
  );
}

/** Lists CLI runtime ids that alias canonical model providers. */
export function listCliRuntimeProviderIds(
  params: {
    config?: OpenClawConfig;
    env?: NodeJS.ProcessEnv;
    includeSetupRegistry?: boolean;
  } = {},
): string[] {
  // Only CLI backends with a canonical modelProvider are runtime aliases that
  // should be hidden from model-provider pickers. Standalone CLI backends own
  // direct refs such as acme-cli/model and must remain selectable.
  return [
    ...new Set(
      listCliRuntimeModelBackendBindings(params)
        .map((binding) => normalizeBackendKey(binding.runtime))
        .filter(Boolean),
    ),
  ].toSorted();
}

/** Resolves the canonical model provider served by a CLI runtime id. */
export function resolveCliRuntimeCanonicalProvider(params: {
  runtime: string | undefined;
  config?: OpenClawConfig;
  env?: NodeJS.ProcessEnv;
  includeSetupRegistry?: boolean;
}): string | undefined {
  const runtime = normalizeBackendKey(params.runtime ?? "");
  if (!runtime) {
    return undefined;
  }
  const runtimeBinding = listCliRuntimeModelBackendBindings().find(
    (binding) => binding.runtime === runtime,
  );
  if (runtimeBinding) {
    return runtimeBinding.provider;
  }
  if (params.includeSetupRegistry !== true) {
    return undefined;
  }
  const setupBackend = cliBackendsDeps.resolvePluginSetupCliBackend({
    backend: runtime,
    config: params.config,
    env: params.env,
  });
  return setupBackend ? resolveCliBackendModelProvider(setupBackend.backend) : undefined;
}

/** Resolves the binding for one provider/runtime pair when registered. */
export function resolveCliRuntimeModelBackendBinding(params: {
  provider: string | undefined;
  runtime: string | undefined;
  config?: OpenClawConfig;
  env?: NodeJS.ProcessEnv;
}): CliRuntimeModelBackendBinding | undefined {
  const provider = normalizeProviderId(params.provider ?? "");
  const runtime = normalizeBackendKey(params.runtime ?? "");
  if (!provider || !runtime) {
    return undefined;
  }
  const runtimeBinding = listCliRuntimeModelBackendBindings().find(
    (binding) => binding.provider === provider && binding.runtime === runtime,
  );
  if (runtimeBinding) {
    return runtimeBinding;
  }
  const includeSetupRegistry = params.config !== undefined || params.env !== undefined;
  if (!includeSetupRegistry) {
    return undefined;
  }
  const setupBackend = cliBackendsDeps.resolvePluginSetupCliBackend({
    backend: runtime,
    config: params.config,
    env: params.env,
  });
  if (!setupBackend) {
    return undefined;
  }
  const setupProvider = resolveCliBackendModelProvider(setupBackend.backend);
  return setupProvider === provider
    ? {
        provider,
        runtime,
        ...(setupBackend.pluginId ? { pluginId: setupBackend.pluginId } : {}),
      }
    : undefined;
}

/** Checks whether a runtime is registered to serve a model provider. */
export function isCliRuntimeModelBackendForProvider(params: {
  provider: string | undefined;
  runtime: string | undefined;
  config?: OpenClawConfig;
  env?: NodeJS.ProcessEnv;
}): boolean {
  return resolveCliRuntimeModelBackendBinding(params) !== undefined;
}

/** Resolves live-test defaults advertised by a CLI backend plugin. */
export function resolveCliBackendLiveTest(provider: string): ResolvedCliBackendLiveTest | null {
  const normalized = normalizeBackendKey(provider);
  const entry =
    cliBackendsDeps.resolvePluginSetupCliBackend({ backend: normalized }) ??
    cliBackendsDeps
      .resolveRuntimeCliBackends()
      .find((backend) => normalizeBackendKey(backend.id) === normalized);
  if (!entry) {
    return null;
  }
  const backend = "backend" in entry ? entry.backend : entry;
  return {
    defaultModelRef: backend.liveTest?.defaultModelRef,
    defaultImageProbe: backend.liveTest?.defaultImageProbe === true,
    defaultMcpProbe: backend.liveTest?.defaultMcpProbe === true,
    dockerNpmPackage: backend.liveTest?.docker?.npmPackage,
    dockerBinaryName: backend.liveTest?.docker?.binaryName,
  };
}

/** Resolves setup-safe live-session protocol metadata without normalizing runtime config. */
export function resolveCliBackendLiveSessionRequirement(
  provider: string,
): CliBackendLiveSessionRequirement | null {
  const normalized = normalizeBackendKey(provider);
  const entry =
    cliBackendsDeps.resolvePluginSetupCliBackend({ backend: normalized }) ??
    cliBackendsDeps
      .resolveRuntimeCliBackends()
      .find((backend) => normalizeBackendKey(backend.id) === normalized);
  if (!entry) {
    return null;
  }
  const backend = "backend" in entry ? entry.backend : entry;
  return backend.liveSessionRequirement ?? null;
}

/** Resolves the executable CLI backend registered by its owning plugin. */
export function resolveCliBackendConfig(
  provider: string,
  cfg?: OpenClawConfig,
  options: { agentId?: string } = {},
): ResolvedCliBackend | null {
  const normalized = normalizeBackendKey(provider);
  const normalizeContext: CliBackendNormalizeConfigContext = {
    backendId: normalized,
    ...(options.agentId ? { agentId: options.agentId } : {}),
    ...(cfg ? { config: cfg } : {}),
  };
  const runtimeTextTransforms = resolveRuntimeTextTransforms();
  const registered = resolveRegisteredBackend(normalized);
  if (registered) {
    const registeredConfig = { ...registered.config };
    const config = registered.normalizeConfig
      ? registered.normalizeConfig(registeredConfig, normalizeContext)
      : registeredConfig;
    const command = config.command?.trim();
    if (!command) {
      return null;
    }
    return {
      id: normalized,
      ...(registered.modelProvider
        ? { modelProvider: normalizeProviderId(registered.modelProvider) }
        : {}),
      config: { ...config, command },
      bundleMcp: registered.bundleMcp === true,
      bundleMcpMode: normalizeBundleMcpMode(
        registered.bundleMcpMode,
        registered.bundleMcp === true,
      ),
      pluginId: registered.pluginId,
      transformSystemPrompt: registered.transformSystemPrompt,
      textTransforms: mergePluginTextTransforms(runtimeTextTransforms, registered.textTransforms),
      defaultAuthProfileId: registered.defaultAuthProfileId,
      authEpochMode: registered.authEpochMode,
      autoSelectAuthProfile: registered.autoSelectAuthProfile,
      contextEngineHostCapabilities: registered.contextEngineHostCapabilities,
      ownsNativeCompaction: registered.ownsNativeCompaction,
      prepareExecution: registered.prepareExecution,
      resolveExecutionArgs: registered.resolveExecutionArgs,
      parseJsonlEvent: registered.parseJsonlEvent,
      toolAvailabilityEnforcement: resolveToolAvailabilityEnforcement(registered),
      nativeToolMode: registered.nativeToolMode,
      sideQuestionToolMode: registered.sideQuestionToolMode,
      runtimeArtifact: registered.runtimeArtifact,
      liveSessionRequirement: registered.liveSessionRequirement,
    };
  }

  const fallbackPolicy = resolveFallbackCliBackendPolicy(normalized);
  if (!fallbackPolicy?.baseConfig) {
    return null;
  }
  const config = fallbackPolicy.normalizeConfig
    ? fallbackPolicy.normalizeConfig(fallbackPolicy.baseConfig, normalizeContext)
    : fallbackPolicy.baseConfig;
  const command = config.command?.trim();
  if (!command) {
    return null;
  }
  return {
    id: normalized,
    ...(fallbackPolicy.modelProvider ? { modelProvider: fallbackPolicy.modelProvider } : {}),
    config: { ...config, command },
    bundleMcp: fallbackPolicy.bundleMcp,
    bundleMcpMode: fallbackPolicy.bundleMcpMode,
    transformSystemPrompt: fallbackPolicy.transformSystemPrompt,
    textTransforms: mergePluginTextTransforms(runtimeTextTransforms, fallbackPolicy.textTransforms),
    defaultAuthProfileId: fallbackPolicy.defaultAuthProfileId,
    authEpochMode: fallbackPolicy.authEpochMode,
    autoSelectAuthProfile: fallbackPolicy.autoSelectAuthProfile,
    contextEngineHostCapabilities: fallbackPolicy.contextEngineHostCapabilities,
    ownsNativeCompaction: fallbackPolicy.ownsNativeCompaction,
    prepareExecution: fallbackPolicy.prepareExecution,
    resolveExecutionArgs: fallbackPolicy.resolveExecutionArgs,
    parseJsonlEvent: fallbackPolicy.parseJsonlEvent,
    toolAvailabilityEnforcement: fallbackPolicy.toolAvailabilityEnforcement,
    nativeToolMode: fallbackPolicy.nativeToolMode,
    sideQuestionToolMode: fallbackPolicy.sideQuestionToolMode,
    runtimeArtifact: fallbackPolicy.runtimeArtifact,
    liveSessionRequirement: fallbackPolicy.liveSessionRequirement,
  };
}

/** Test-only dependency controls for CLI backend registry resolution. */
const testing = {
  resetDepsForTest(): void {
    cliBackendsDeps = defaultCliBackendsDeps;
  },
  setDepsForTest(deps: Partial<CliBackendsDeps>): void {
    cliBackendsDeps = {
      ...defaultCliBackendsDeps,
      ...deps,
    };
  },
} as const;

if (process.env.VITEST || process.env.NODE_ENV === "test") {
  (globalThis as Record<PropertyKey, unknown>)[Symbol.for("openclaw.cliBackendsTestApi")] = testing;
}
