// Media-understanding runner resolves providers/models, local roots, auth, and
// per-capability execution decisions for message attachments.
import path from "node:path";
import { mergeInboundPathRoots } from "@openclaw/media-core/inbound-path-policy";
import { findNormalizedProviderValue } from "@openclaw/model-catalog-core/provider-id";
import {
  normalizeLowercaseStringOrEmpty,
  normalizeNullableString,
  normalizeOptionalString,
} from "@openclaw/normalization-core/string-coerce";
import { uniqueStrings } from "@openclaw/normalization-core/string-normalization";
import type { ActiveMediaModel } from "../../packages/media-understanding-common/src/active-model.js";
import { isMediaUnderstandingSkipError } from "../../packages/media-understanding-common/src/errors.js";
import { providerSupportsCapability } from "../../packages/media-understanding-common/src/provider-supports.js";
import { isMinimaxVlmModel, isMinimaxVlmProvider } from "../agents/minimax-vlm.js";
import {
  buildModelAliasIndex,
  inferUniqueProviderFromConfiguredModels,
  resolveDefaultModelForAgent,
  resolveModelRefFromString,
} from "../agents/model-selection.js";
import type { MsgContext } from "../auto-reply/templating.js";
import {
  resolveAgentModelFallbackValues,
  resolveAgentModelPrimaryValue,
} from "../config/model-input.js";
import type { OpenClawConfig } from "../config/types.js";
import type {
  MediaUnderstandingConfig,
  MediaUnderstandingModelConfig,
} from "../config/types.tools.js";
import { logVerbose, shouldLogVerbose } from "../globals.js";
import { logWarn } from "../logger.js";
import { resolveChannelInboundAttachmentRoots } from "../media/channel-inbound-roots.js";
import { getDefaultMediaLocalRoots } from "../media/local-roots.js";
import { normalizeMediaFacts } from "../media/media-facts.js";
import { createLazyRuntimeModule, createLazyRuntimeNamedExport } from "../shared/lazy-runtime.js";
import { MediaAttachmentCache, selectAttachments } from "./attachments.js";
import { matchesMediaEntryCapability } from "./entry-capabilities.js";
import {
  clearLocalAudioInspectionCacheForTests,
  inspectLocalAudioSelection,
} from "./local-audio.js";
import { resolveOpenAiAudioAuthModelApi } from "./openai-audio-api.js";
import { normalizeMediaExecutionProviderId, normalizeMediaProviderId } from "./provider-id.js";
import {
  buildMediaUnderstandingRegistry,
  getMediaUnderstandingProvider,
} from "./provider-registry.js";
import {
  resolveModelEntries,
  resolveScopeDecision,
  type ResolvedMediaModelEntry,
} from "./resolve.js";
import {
  buildModelDecision,
  formatDecisionSummary,
  runCliEntry,
  runProviderEntry,
} from "./runner.entries.js";
import type {
  MediaAttachment,
  MediaUnderstandingCapability,
  MediaUnderstandingDecision,
  MediaUnderstandingModelDecision,
  MediaUnderstandingOutput,
  MediaUnderstandingProvider,
} from "./types.js";

export { createMediaAttachmentCache, normalizeMediaAttachments } from "./runner.attachments.js";

type ProviderRegistry = Map<string, MediaUnderstandingProvider>;
type ModelCatalogApi = typeof import("../agents/model-catalog.js") &
  typeof import("../agents/prepared-model-catalog.js");
type ModelCatalog = Awaited<ReturnType<ModelCatalogApi["loadPreparedModelCatalog"]>>;

type RunCapabilityResult = {
  outputs: MediaUnderstandingOutput[];
  decision: MediaUnderstandingDecision;
};

const loadHasAvailableAuthForProvider = createLazyRuntimeNamedExport(
  () => import("../agents/model-auth.js"),
  "hasAvailableAuthForProvider",
);

const loadPreparedModelCatalogApi = createLazyRuntimeModule(async () => ({
  ...(await import("../agents/model-catalog.js")),
  ...(await import("../agents/prepared-model-catalog.js")),
}));

function resolveLiteralProviderApiKey(
  cfg: OpenClawConfig | undefined,
  providerId: string,
): string | null {
  return normalizeNullableString(
    findNormalizedProviderValue(cfg?.models?.providers, providerId)?.apiKey,
  );
}

async function hasProviderAuthAvailable(params: {
  capability: MediaUnderstandingCapability;
  provider: string;
  cfg?: OpenClawConfig;
  agentDir?: string;
  workspaceDir?: string;
}): Promise<boolean> {
  // Literal config keys are cheap to detect; defer loading model-auth until
  // profile/env discovery is actually needed.
  if (resolveLiteralProviderApiKey(params.cfg, params.provider)) {
    return true;
  }
  const hasAvailableAuthForProvider = await loadHasAvailableAuthForProvider();
  return await hasAvailableAuthForProvider({
    ...params,
    modelApi: resolveOpenAiAudioAuthModelApi({
      capability: params.capability,
      providerId: params.provider,
    }),
  });
}

function resolveConfiguredKeyProviderOrder(params: {
  cfg: OpenClawConfig;
  providerRegistry: ProviderRegistry;
  capability: MediaUnderstandingCapability;
  fallbackProviders: readonly string[];
}): string[] {
  const configuredProviders = Object.keys(params.cfg.models?.providers ?? {})
    .map((providerId) => normalizeMediaExecutionProviderId(providerId))
    .filter(Boolean);
  const supportedProviders = uniqueStrings(configuredProviders).filter((providerId) =>
    providerSupportsCapability(
      params.providerRegistry.get(normalizeMediaProviderId(providerId)),
      params.capability,
    ),
  );
  return uniqueStrings([...supportedProviders, ...params.fallbackProviders]);
}

function resolveConfiguredImageModelId(params: {
  cfg: OpenClawConfig;
  providerId: string;
}): string | undefined {
  if (isMinimaxVlmProvider(params.providerId)) {
    return undefined;
  }
  const configured = resolveConfiguredImageModel(params);
  const id = configured?.id?.trim();
  return id || undefined;
}

function resolveConfiguredImageModel(params: {
  cfg: OpenClawConfig;
  providerId: string;
}): { id?: string; input?: string[] } | undefined {
  const providerCfg = findNormalizedProviderValue(
    params.cfg.models?.providers,
    params.providerId,
  ) as
    | {
        models?: Array<{
          id?: string;
          input?: string[];
        }>;
      }
    | undefined;
  return providerCfg?.models?.find((entry) => {
    const id = entry?.id?.trim();
    return Boolean(id) && entry?.input?.includes("image");
  });
}

function resolveCatalogImageModelId(params: {
  providerId: string;
  catalog: ModelCatalog;
  modelSupportsVision: ModelCatalogApi["modelSupportsVision"];
}): string | undefined {
  const matches = params.catalog.filter(
    (entry) =>
      normalizeMediaProviderId(entry.provider) === normalizeMediaProviderId(params.providerId) &&
      params.modelSupportsVision(entry),
  );
  if (matches.length === 0) {
    return undefined;
  }
  const autoEntry = matches.find((entry) => normalizeLowercaseStringOrEmpty(entry.id) === "auto");
  return normalizeOptionalString((autoEntry ?? matches[0])?.id);
}

function resolveDefaultMediaModelFromRegistry(params: {
  providerId: string;
  capability: MediaUnderstandingCapability;
  providerRegistry: ProviderRegistry;
}): string | undefined {
  const provider = params.providerRegistry.get(normalizeMediaProviderId(params.providerId));
  return normalizeOptionalString(provider?.defaultModels?.[params.capability]);
}

function resolveAutoMediaKeyProvidersFromRegistry(params: {
  capability: MediaUnderstandingCapability;
  providerRegistry: ProviderRegistry;
}): string[] {
  type AutoProviderEntry = {
    provider: MediaUnderstandingProvider;
    priority: number;
  };
  return [...params.providerRegistry.values()]
    .filter(
      (provider) =>
        provider.capabilities?.includes(params.capability) ??
        providerSupportsCapability(provider, params.capability),
    )
    .map((provider): AutoProviderEntry | null => {
      const priority = provider.autoPriority?.[params.capability];
      return typeof priority === "number" && Number.isFinite(priority)
        ? { provider, priority }
        : null;
    })
    .filter((entry): entry is AutoProviderEntry => entry !== null)
    .toSorted((left, right) => {
      if (left.priority !== right.priority) {
        return left.priority - right.priority;
      }
      return left.provider.id.localeCompare(right.provider.id);
    })
    .map((entry) => normalizeMediaProviderId(entry.provider.id))
    .filter(Boolean);
}

async function explicitImageModelVisionStatus(params: {
  cfg: OpenClawConfig;
  agentId?: string;
  providerId: string;
  model: string;
  agentDir?: string;
  workspaceDir?: string;
}): Promise<"supported" | "unsupported" | "unknown"> {
  // Explicit model overrides should survive unknown catalog state, but known
  // text-only models must not be routed into image understanding.
  if (
    isMinimaxVlmProvider(params.providerId) &&
    !isMinimaxVlmModel(params.providerId, params.model)
  ) {
    return "unsupported";
  }
  const configured = resolveConfiguredImageModel(params);
  if (configured?.id?.trim() === params.model && configured.input?.includes("image")) {
    return "supported";
  }
  const { findModelInCatalog, loadPreparedModelCatalog, modelSupportsVision } =
    await loadPreparedModelCatalogApi();
  const catalog = await loadPreparedModelCatalog({
    config: params.cfg,
    ...(params.agentId ? { agentId: params.agentId } : {}),
    ...(params.agentDir ? { agentDir: params.agentDir } : {}),
    ...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
  });
  const entry = findModelInCatalog(catalog, params.providerId, params.model);
  if (!entry) {
    return "unknown";
  }
  return modelSupportsVision(entry) ? "supported" : "unsupported";
}

async function resolveAutoImageModelId(params: {
  cfg: OpenClawConfig;
  agentId?: string;
  providerId: string;
  providerRegistry: ProviderRegistry;
  explicitModel?: string;
  agentDir?: string;
  workspaceDir?: string;
}): Promise<string | undefined> {
  const explicit = normalizeOptionalString(params.explicitModel);
  if (explicit) {
    const explicitStatus = await explicitImageModelVisionStatus({
      cfg: params.cfg,
      agentId: params.agentId,
      providerId: params.providerId,
      model: explicit,
      agentDir: params.agentDir,
      workspaceDir: params.workspaceDir,
    });
    if (explicitStatus !== "unsupported") {
      return explicit;
    }
  }
  if (isMinimaxVlmProvider(params.providerId)) {
    return "MiniMax-VL-01";
  }
  const configuredModel = resolveConfiguredImageModelId(params);
  if (configuredModel) {
    return configuredModel;
  }
  const defaultModel = resolveDefaultMediaModelFromRegistry({
    providerId: params.providerId,
    capability: "image",
    providerRegistry: params.providerRegistry,
  });
  if (defaultModel) {
    return defaultModel;
  }
  const { resolveDefaultMediaModel } = await import("./defaults.js");
  const bundledDefaultModel = resolveDefaultMediaModel({
    cfg: params.cfg,
    providerId: params.providerId,
    capability: "image",
    workspaceDir: params.workspaceDir,
  });
  if (bundledDefaultModel) {
    return bundledDefaultModel;
  }
  const { loadPreparedModelCatalog, modelSupportsVision } = await loadPreparedModelCatalogApi();
  const catalog = await loadPreparedModelCatalog({
    config: params.cfg,
    ...(params.agentId ? { agentId: params.agentId } : {}),
    ...(params.agentDir ? { agentDir: params.agentDir } : {}),
    ...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
  });
  return resolveCatalogImageModelId({
    providerId: params.providerId,
    catalog,
    modelSupportsVision,
  });
}

export function buildProviderRegistry(
  overrides?: Record<string, MediaUnderstandingProvider>,
  cfg?: OpenClawConfig,
): ProviderRegistry {
  return buildMediaUnderstandingRegistry(overrides, cfg);
}

export function resolveMediaAttachmentLocalRoots(params: {
  cfg: OpenClawConfig;
  ctx: MsgContext;
  workspaceDir?: string;
}): readonly string[] {
  const workspaceDirs = normalizeMediaFacts(params.ctx.media).flatMap((fact) =>
    fact.workspaceDir ? [path.resolve(fact.workspaceDir)] : [],
  );
  return mergeInboundPathRoots(
    getDefaultMediaLocalRoots(),
    workspaceDirs,
    params.workspaceDir ? [path.resolve(params.workspaceDir)] : undefined,
    resolveChannelInboundAttachmentRoots(params),
  );
}

function clearMediaUnderstandingBinaryCacheForTests(): void {
  clearLocalAudioInspectionCacheForTests();
}

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

async function resolveKeyEntry(params: {
  cfg: OpenClawConfig;
  agentId?: string;
  agentDir?: string;
  workspaceDir?: string;
  providerRegistry: ProviderRegistry;
  capability: MediaUnderstandingCapability;
  activeModel?: ActiveMediaModel;
}): Promise<MediaUnderstandingModelConfig | null> {
  const { cfg, agentId, agentDir, workspaceDir, providerRegistry, capability } = params;
  const checkProvider = async (
    providerId: string,
    model?: string,
  ): Promise<MediaUnderstandingModelConfig | null> => {
    const provider = getMediaUnderstandingProvider(providerId, providerRegistry);
    if (!provider) {
      return null;
    }
    if (capability === "audio" && !provider.transcribeAudio) {
      return null;
    }
    if (capability === "image" && !provider.describeImage) {
      return null;
    }
    if (capability === "video" && !provider.describeVideo) {
      return null;
    }
    if (
      !(await hasProviderAuthAvailable({
        capability,
        provider: providerId,
        cfg,
        agentDir,
        workspaceDir,
      }))
    ) {
      return null;
    }
    // The supplied model can belong to the active chat route. Audio providers
    // use their own default or stay model-less; explicit media entries bypass this auto path.
    const resolvedModel =
      capability === "image"
        ? await resolveAutoImageModelId({
            cfg,
            agentId,
            providerId,
            providerRegistry,
            explicitModel: model,
            agentDir,
            workspaceDir,
          })
        : capability === "audio"
          ? resolveDefaultMediaModelFromRegistry({
              providerId,
              capability: "audio",
              providerRegistry,
            })
          : (model ??
            resolveDefaultMediaModelFromRegistry({
              providerId,
              capability: "video",
              providerRegistry,
            }));
    if (capability === "image" && !resolvedModel) {
      return null;
    }
    return { type: "provider" as const, provider: providerId, model: resolvedModel };
  };

  const activeProvider = params.activeModel?.provider?.trim();
  if (activeProvider) {
    const activeEntry = await checkProvider(activeProvider, params.activeModel?.model);
    if (activeEntry) {
      return activeEntry;
    }
  }
  for (const providerId of resolveConfiguredKeyProviderOrder({
    cfg,
    providerRegistry,
    capability,
    fallbackProviders: resolveAutoMediaKeyProvidersFromRegistry({
      capability,
      providerRegistry,
    }),
  })) {
    const entry = await checkProvider(providerId, undefined);
    if (entry) {
      return entry;
    }
  }
  return null;
}

function resolveImageModelFromAgentDefaults(params: {
  cfg: OpenClawConfig;
  agentId?: string;
}): MediaUnderstandingModelConfig[] {
  const refs: string[] = [];
  const primary = resolveAgentModelPrimaryValue(params.cfg.agents?.defaults?.imageModel);
  if (primary?.trim()) {
    refs.push(primary.trim());
  }
  for (const fb of resolveAgentModelFallbackValues(params.cfg.agents?.defaults?.imageModel)) {
    if (fb?.trim()) {
      refs.push(fb.trim());
    }
  }
  if (refs.length === 0) {
    return [];
  }
  const defaultProvider = resolveDefaultModelForAgent({
    cfg: params.cfg,
    agentId: params.agentId,
  }).provider;
  const entries: MediaUnderstandingModelConfig[] = [];
  for (const ref of refs) {
    const effectiveDefaultProvider = ref.includes("/")
      ? defaultProvider
      : (inferUniqueProviderFromConfiguredModels({
          cfg: params.cfg,
          model: ref,
        }) ?? defaultProvider);
    const aliasIndex = buildModelAliasIndex({
      cfg: params.cfg,
      defaultProvider: effectiveDefaultProvider,
    });
    const resolved = resolveModelRefFromString({
      cfg: params.cfg,
      raw: ref,
      defaultProvider: effectiveDefaultProvider,
      aliasIndex,
    });
    if (!resolved) {
      continue;
    }
    entries.push({
      type: "provider",
      provider: resolved.ref.provider,
      model: resolved.ref.model,
    });
  }
  return entries;
}

function hasExplicitImageUnderstandingConfig(params: {
  cfg: OpenClawConfig;
  providerRegistry: ProviderRegistry;
}): boolean {
  return (params.cfg.tools?.media?.models ?? []).some((entry) =>
    matchesMediaEntryCapability({
      entry,
      source: "shared",
      capability: "image",
      providerRegistry: params.providerRegistry,
    }),
  );
}

function isMinimaxNativeVisionModel(params: { provider: string; model?: string }): boolean {
  // MiniMax M2.x catalog rows may advertise image input but still need the
  // MiniMax-VL-01 media-understanding path; only M3/M3.x is native vision here.
  return (
    isMinimaxVlmProvider(params.provider) &&
    /^MiniMax-M3(\b|[-.])/i.test(params.model?.trim() ?? "")
  );
}

async function activeModelSupportsNativeVision(params: {
  cfg: OpenClawConfig;
  agentId?: string;
  activeModel?: ActiveMediaModel;
  agentDir?: string;
  workspaceDir?: string;
}): Promise<boolean> {
  const activeProvider = params.activeModel?.provider?.trim();
  if (!activeProvider) {
    return false;
  }
  if (
    isMinimaxVlmProvider(activeProvider) &&
    !isMinimaxNativeVisionModel({
      provider: activeProvider,
      model: params.activeModel?.model,
    })
  ) {
    return false;
  }
  const { findModelInCatalog, loadPreparedModelCatalog, modelSupportsVision } =
    await loadPreparedModelCatalogApi();
  const catalog = await loadPreparedModelCatalog({
    config: params.cfg,
    ...(params.agentId ? { agentId: params.agentId } : {}),
    ...(params.agentDir ? { agentDir: params.agentDir } : {}),
    ...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
  });
  const entry = findModelInCatalog(catalog, activeProvider, params.activeModel?.model ?? "");
  return modelSupportsVision(entry);
}

async function resolveAutoEntries(params: {
  cfg: OpenClawConfig;
  agentId?: string;
  agentDir?: string;
  workspaceDir?: string;
  providerRegistry: ProviderRegistry;
  capability: MediaUnderstandingCapability;
  activeModel?: ActiveMediaModel;
}): Promise<MediaUnderstandingModelConfig[]> {
  if (params.capability === "image") {
    const activeSupportsVision = await activeModelSupportsNativeVision({
      cfg: params.cfg,
      agentId: params.agentId,
      activeModel: params.activeModel,
      agentDir: params.agentDir,
      workspaceDir: params.workspaceDir,
    });
    if (!activeSupportsVision) {
      const imageModelEntries = resolveImageModelFromAgentDefaults({
        cfg: params.cfg,
        agentId: params.agentId,
      });
      if (imageModelEntries.length > 0) {
        return imageModelEntries;
      }
    }
  }
  const activeEntry = await resolveActiveModelEntry(params);
  if (activeEntry) {
    return [activeEntry];
  }
  if (params.capability === "audio") {
    const keyEntry = await resolveKeyEntry(params);
    if (keyEntry) {
      return [keyEntry];
    }
    const localAudio = await inspectLocalAudioSelection();
    if (localAudio.entries.length > 0) {
      return localAudio.entries;
    }
  }
  const keys = await resolveKeyEntry(params);
  if (keys) {
    return [keys];
  }
  return [];
}

export async function resolveAutoImageModel(params: {
  cfg: OpenClawConfig;
  agentId?: string;
  agentDir?: string;
  workspaceDir?: string;
  activeModel?: ActiveMediaModel;
}): Promise<ActiveMediaModel | null> {
  const providerRegistry = buildProviderRegistry(undefined, params.cfg);
  const toActive = (entry: MediaUnderstandingModelConfig | null): ActiveMediaModel | null => {
    if (!entry || entry.type === "cli") {
      return null;
    }
    const provider = entry.provider;
    const model = entry.model?.trim();
    if (!provider || !model) {
      return null;
    }
    return { provider, model };
  };
  const configuredImageModel = resolveImageModelFromAgentDefaults({
    cfg: params.cfg,
    agentId: params.agentId,
  })
    .map((entry) => toActive(entry))
    .find((entry): entry is ActiveMediaModel => entry !== null);
  if (configuredImageModel) {
    return configuredImageModel;
  }
  const activeEntry = await resolveActiveModelEntry({
    cfg: params.cfg,
    agentId: params.agentId,
    agentDir: params.agentDir,
    workspaceDir: params.workspaceDir,
    providerRegistry,
    capability: "image",
    activeModel: params.activeModel,
  });
  const resolvedActive = toActive(activeEntry);
  if (resolvedActive) {
    return resolvedActive;
  }
  const keyEntry = await resolveKeyEntry({
    cfg: params.cfg,
    agentId: params.agentId,
    agentDir: params.agentDir,
    workspaceDir: params.workspaceDir,
    providerRegistry,
    capability: "image",
    activeModel: params.activeModel,
  });
  return toActive(keyEntry);
}

async function resolveActiveModelEntry(params: {
  cfg: OpenClawConfig;
  agentId?: string;
  agentDir?: string;
  workspaceDir?: string;
  providerRegistry: ProviderRegistry;
  capability: MediaUnderstandingCapability;
  activeModel?: ActiveMediaModel;
}): Promise<MediaUnderstandingModelConfig | null> {
  const activeProviderRaw = params.activeModel?.provider?.trim();
  if (!activeProviderRaw) {
    return null;
  }
  const providerId = normalizeMediaExecutionProviderId(activeProviderRaw);
  if (!providerId) {
    return null;
  }
  const provider = getMediaUnderstandingProvider(providerId, params.providerRegistry);
  if (!provider) {
    return null;
  }
  if (params.capability === "audio" && !provider.transcribeAudio) {
    return null;
  }
  if (params.capability === "image" && !provider.describeImage) {
    return null;
  }
  if (params.capability === "video" && !provider.describeVideo) {
    return null;
  }
  const hasAuth = await hasProviderAuthAvailable({
    capability: params.capability,
    provider: providerId,
    cfg: params.cfg,
    agentDir: params.agentDir,
    workspaceDir: params.workspaceDir,
  });
  if (!hasAuth) {
    return null;
  }
  let model: string | undefined;
  if (params.capability === "image") {
    model = await resolveAutoImageModelId({
      cfg: params.cfg,
      agentId: params.agentId,
      providerId,
      providerRegistry: params.providerRegistry,
      explicitModel: params.activeModel?.model,
      agentDir: params.agentDir,
      workspaceDir: params.workspaceDir,
    });
  } else if (params.capability === "audio") {
    model = resolveDefaultMediaModelFromRegistry({
      providerId,
      capability: "audio",
      providerRegistry: params.providerRegistry,
    });
  } else {
    model =
      params.activeModel?.model ??
      resolveDefaultMediaModelFromRegistry({
        providerId,
        capability: "video",
        providerRegistry: params.providerRegistry,
      });
  }
  if (params.capability === "image" && !model) {
    return null;
  }
  return {
    type: "provider",
    provider: providerId,
    model,
  };
}

async function runAttachmentEntries(params: {
  capability: MediaUnderstandingCapability;
  cfg: OpenClawConfig;
  ctx: MsgContext;
  attachment: MediaAttachment;
  agentId?: string;
  agentDir?: string;
  workspaceDir?: string;
  providerRegistry: ProviderRegistry;
  cache: MediaAttachmentCache;
  entries: ResolvedMediaModelEntry[];
  config?: MediaUnderstandingConfig;
}): Promise<{
  output: MediaUnderstandingOutput | null;
  attempts: MediaUnderstandingModelDecision[];
}> {
  const { entries, capability } = params;
  const attachmentIndex = params.attachment.index;
  const attempts: MediaUnderstandingModelDecision[] = [];
  for (const candidate of entries) {
    const { entry } = candidate;
    const entryType = entry.type ?? (entry.command ? "cli" : "provider");
    try {
      const result =
        entryType === "cli"
          ? await runCliEntry({
              capability,
              entry,
              cfg: params.cfg,
              ctx: params.ctx,
              attachment: params.attachment,
              cache: params.cache,
              config: params.config,
            })
          : await runProviderEntry({
              capability,
              entry,
              cfg: params.cfg,
              ctx: params.ctx,
              attachmentIndex,
              cache: params.cache,
              agentId: params.agentId,
              agentDir: params.agentDir,
              workspaceDir: params.workspaceDir,
              providerRegistry: params.providerRegistry,
              config: params.config,
              secretOwnerId: candidate.secretOwnerId,
            });
      if (result?.text) {
        const decision = buildModelDecision({ entry, entryType, outcome: "success" });
        if (result.provider) {
          decision.provider = result.provider;
        }
        if (result.model) {
          decision.model = result.model;
        }
        if (result.requestedBackend) {
          decision.requestedBackend = result.requestedBackend;
        }
        if (result.observedBackend) {
          decision.observedBackend = result.observedBackend;
        }
        attempts.push(decision);
        return { output: result, attempts };
      }
      attempts.push(
        buildModelDecision({ entry, entryType, outcome: "skipped", reason: "empty output" }),
      );
    } catch (err) {
      if (isMediaUnderstandingSkipError(err)) {
        attempts.push(
          buildModelDecision({
            entry,
            entryType,
            outcome: "skipped",
            reason: `${err.reason}: ${err.message}`,
          }),
        );
        if (shouldLogVerbose()) {
          logVerbose(`Skipping ${capability} model due to ${err.reason}: ${err.message}`);
        }
        continue;
      }
      attempts.push(
        buildModelDecision({
          entry,
          entryType,
          outcome: "failed",
          reason: String(err),
        }),
      );
      if (shouldLogVerbose()) {
        logVerbose(`${capability} understanding failed: ${String(err)}`);
      }
    }
  }

  return { output: null, attempts };
}

function hasFailedMediaAttempt(attachments: MediaUnderstandingDecision["attachments"]): boolean {
  return attachments.some((attachment) =>
    attachment.attempts.some((attempt) => attempt.outcome === "failed"),
  );
}

export async function runCapability(params: {
  capability: MediaUnderstandingCapability;
  cfg: OpenClawConfig;
  ctx: MsgContext;
  attachments: MediaAttachmentCache;
  media: MediaAttachment[];
  agentId?: string;
  agentDir?: string;
  workspaceDir?: string;
  providerRegistry: ProviderRegistry;
  config?: MediaUnderstandingConfig;
  activeModel?: ActiveMediaModel;
}): Promise<RunCapabilityResult> {
  const { capability, cfg, ctx } = params;
  const config: MediaUnderstandingConfig = params.config ?? cfg.tools?.media?.[capability] ?? {};
  if (config?.enabled === false) {
    return {
      outputs: [],
      decision: { capability, outcome: "disabled", attachments: [] },
    };
  }

  const attachmentPolicy = config?.attachments;
  const selected = selectAttachments({
    capability,
    attachments: params.media,
    policy: attachmentPolicy,
  });
  if (selected.length === 0) {
    return {
      outputs: [],
      decision: { capability, outcome: "no-attachment", attachments: [] },
    };
  }

  const scopeDecision = resolveScopeDecision({ scope: config?.scope, ctx });
  if (scopeDecision === "deny") {
    if (shouldLogVerbose()) {
      logVerbose(`${capability} understanding disabled by scope policy.`);
    }
    return {
      outputs: [],
      decision: {
        capability,
        outcome: "scope-deny",
        attachments: selected.map((item) => ({ attachmentIndex: item.index, attempts: [] })),
      },
    };
  }

  // Skip image understanding when the primary model supports vision natively.
  // The image will be injected directly into the model context instead.
  const activeProvider = params.activeModel?.provider?.trim();
  if (
    capability === "image" &&
    activeProvider &&
    !hasExplicitImageUnderstandingConfig({
      cfg,
      providerRegistry: params.providerRegistry,
    })
  ) {
    if (
      await activeModelSupportsNativeVision({
        cfg,
        agentId: params.agentId,
        activeModel: params.activeModel,
        agentDir: params.agentDir,
        workspaceDir: params.workspaceDir,
      })
    ) {
      if (shouldLogVerbose()) {
        logVerbose("Skipping image understanding: primary model supports vision natively");
      }
      const model = params.activeModel?.model?.trim();
      const reason = "primary model supports vision natively";
      return {
        outputs: [],
        decision: {
          capability,
          outcome: "skipped",
          attachments: selected.map((item) => {
            const attempt = {
              type: "provider" as const,
              provider: activeProvider,
              model: model || undefined,
              outcome: "skipped" as const,
              reason,
            };
            return {
              attachmentIndex: item.index,
              attempts: [attempt],
              chosen: attempt,
            };
          }),
        },
      };
    }
  }

  const entries = resolveModelEntries({
    cfg,
    capability,
    config,
    providerRegistry: params.providerRegistry,
  });
  let resolvedEntries: ResolvedMediaModelEntry[] = entries;
  if (resolvedEntries.length === 0) {
    resolvedEntries = (
      await resolveAutoEntries({
        cfg,
        agentId: params.agentId,
        agentDir: params.agentDir,
        workspaceDir: params.workspaceDir,
        providerRegistry: params.providerRegistry,
        capability,
        activeModel: params.activeModel,
      })
    ).map((entry) => ({ entry }));
  }
  if (resolvedEntries.length === 0) {
    return {
      outputs: [],
      decision: {
        capability,
        outcome: "skipped",
        attachments: selected.map((item) => ({ attachmentIndex: item.index, attempts: [] })),
      },
    };
  }

  const outputs: MediaUnderstandingOutput[] = [];
  const attachmentDecisions: MediaUnderstandingDecision["attachments"] = [];
  for (const attachment of selected) {
    const { output, attempts } = await runAttachmentEntries({
      capability,
      cfg,
      ctx,
      attachment,
      agentId: params.agentId,
      agentDir: params.agentDir,
      workspaceDir: params.workspaceDir,
      providerRegistry: params.providerRegistry,
      cache: params.attachments,
      entries: resolvedEntries,
      config,
    });
    if (output) {
      outputs.push(output);
    }
    attachmentDecisions.push({
      attachmentIndex: attachment.index,
      attempts,
      chosen: attempts.find((attempt) => attempt.outcome === "success"),
    });
  }
  const decision: MediaUnderstandingDecision = {
    capability,
    outcome:
      outputs.length > 0
        ? "success"
        : hasFailedMediaAttempt(attachmentDecisions)
          ? "failed"
          : "skipped",
    attachments: attachmentDecisions,
  };
  if (decision.outcome === "failed") {
    logWarn(`media-understanding: ${formatDecisionSummary(decision)}`);
  } else if (shouldLogVerbose()) {
    logVerbose(`Media understanding ${formatDecisionSummary(decision)}`);
  }
  return {
    outputs,
    decision,
  };
}
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */
