// Ollama plugin module implements discovery shared behavior.
import { getCachedLiveCatalogValue } from "openclaw/plugin-sdk/provider-catalog-shared";
import type {
  ModelProviderConfig,
  ModelDefinitionConfig,
} from "openclaw/plugin-sdk/provider-model-shared";

/**
 * Provider config input type — partial config without required `models`.
 * Replaces the deprecated `openclaw/plugin-sdk/config-types` import.
 */
type OllamaProviderConfigInput = Omit<Partial<ModelProviderConfig>, "models"> & {
  models?: ModelDefinitionConfig[];
};
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { OLLAMA_DEFAULT_BASE_URL } from "./defaults.js";
import { readProviderBaseUrl } from "./provider-base-url.js";
import { resolveOllamaApiBase } from "./provider-models.js";

export const OLLAMA_PROVIDER_ID = "ollama";
export const OLLAMA_DEFAULT_API_KEY = "ollama-local";

export type OllamaPluginConfig = {
  discovery?: {
    enabled?: boolean;
  };
};

type OllamaDiscoveryContext = {
  config: {
    models?: {
      providers?: Record<string, OllamaProviderConfigInput | undefined>;
    };
  };
  env: NodeJS.ProcessEnv;
  resolveProviderApiKey: (providerId: string) => {
    apiKey?: unknown;
    discoveryApiKey?: unknown;
  };
};

function readStringValue(value: unknown): string | undefined {
  if (typeof value === "string") {
    return normalizeOptionalString(value);
  }
  if (value && typeof value === "object" && "value" in value) {
    return normalizeOptionalString((value as { value?: unknown }).value);
  }
  return undefined;
}

function isOllamaApiKeyMarker(value: string): boolean {
  return value === "OLLAMA_API_KEY" || value === OLLAMA_DEFAULT_API_KEY;
}

export function resolveOllamaRuntimeBaseUrl(params: {
  api?: ModelProviderConfig["api"];
  configuredBaseUrl?: string;
  discoveredBaseUrl: string;
}): string {
  if (params.configuredBaseUrl && params.api && params.api !== "ollama") {
    return params.configuredBaseUrl;
  }
  return params.discoveredBaseUrl;
}

function resolveOllamaDiscoveryApiKey(params: {
  env: NodeJS.ProcessEnv;
  baseUrl?: string;
  explicitApiKey?: string;
  resolvedApiKey?: unknown;
  resolvedDiscoveryApiKey?: unknown;
}): string | undefined {
  const envValue = normalizeOptionalString(params.env.OLLAMA_API_KEY);
  const resolvedApiKey = normalizeOptionalString(params.resolvedApiKey);
  const resolvedDiscoveryApiKey = normalizeOptionalString(params.resolvedDiscoveryApiKey);
  const explicitApiKey = normalizeOptionalString(params.explicitApiKey);
  if (explicitApiKey && !isOllamaApiKeyMarker(explicitApiKey)) {
    return explicitApiKey;
  }
  if (!isLocalOllamaBaseUrl(params.baseUrl)) {
    if (resolvedDiscoveryApiKey) {
      return resolvedDiscoveryApiKey;
    }
    if (resolvedApiKey && !isOllamaApiKeyMarker(resolvedApiKey)) {
      return resolvedApiKey;
    }
    return envValue && envValue !== OLLAMA_DEFAULT_API_KEY ? envValue : undefined;
  }
  if (resolvedApiKey && resolvedApiKey !== envValue && !isOllamaApiKeyMarker(resolvedApiKey)) {
    return resolvedApiKey;
  }
  return OLLAMA_DEFAULT_API_KEY;
}

function shouldSkipAmbientOllamaDiscovery(env: NodeJS.ProcessEnv): boolean {
  return Boolean(env.VITEST) || env.NODE_ENV === "test";
}

const LOCAL_OLLAMA_HOSTNAMES = new Set([
  "localhost",
  "127.0.0.1",
  "0.0.0.0",
  "::1",
  "::",
  "docker.orb.internal",
  "host.docker.internal",
  "host.orb.internal",
]);
const LOOPBACK_OLLAMA_HOSTNAMES = new Set(["localhost", "127.0.0.1", "0.0.0.0", "::1", "::"]);

function isIpv4Loopback(host: string): boolean {
  if (!/^\d+\.\d+\.\d+\.\d+$/.test(host)) {
    return false;
  }
  const octets = host.split(".").map((part) => Number.parseInt(part, 10));
  if (octets.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
    return false;
  }
  return octets[0] === 127;
}

function isIpv4PrivateRange(host: string): boolean {
  if (!/^\d+\.\d+\.\d+\.\d+$/.test(host)) {
    return false;
  }
  const octets = host.split(".").map((part) => Number.parseInt(part, 10));
  if (octets.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
    return false;
  }
  const [a, b] = octets;
  return a === 10 || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168);
}

function isIpv6LocalRange(host: string): boolean {
  const lower = host.toLowerCase();
  return /^fe[89ab][0-9a-f]:/.test(lower) || /^f[cd][0-9a-f]{2}:/.test(lower);
}

export function isLocalOllamaBaseUrl(baseUrl: string | undefined | null): boolean {
  if (!baseUrl) {
    return true;
  }
  let parsed: URL;
  try {
    parsed = new URL(baseUrl);
  } catch {
    return false;
  }
  let host = parsed.hostname.toLowerCase();
  if (host.startsWith("[") && host.endsWith("]")) {
    host = host.slice(1, -1);
  }
  return (
    LOCAL_OLLAMA_HOSTNAMES.has(host) ||
    host.endsWith(".local") ||
    isIpv4PrivateRange(host) ||
    isIpv6LocalRange(host) ||
    (!host.includes(".") && !host.includes(":"))
  );
}

const HOSTED_OLLAMA_CLOUD_HOSTNAMES = new Set(["ollama.com", "api.ollama.com"]);

export function isHostedOllamaCloud(baseUrl: string | undefined | null): boolean {
  if (!baseUrl) {
    return false;
  }
  let parsed: URL;
  try {
    parsed = new URL(baseUrl);
  } catch {
    return false;
  }
  const host = parsed.hostname.toLowerCase();
  return HOSTED_OLLAMA_CLOUD_HOSTNAMES.has(host) || host.endsWith(".ollama.com");
}

function isLoopbackOllamaBaseUrl(baseUrl: string | undefined | null): boolean {
  if (!baseUrl) {
    return true;
  }
  let parsed: URL;
  try {
    parsed = new URL(baseUrl);
  } catch {
    return false;
  }
  let host = parsed.hostname.toLowerCase();
  if (host.startsWith("[") && host.endsWith("]")) {
    host = host.slice(1, -1);
  }
  return LOOPBACK_OLLAMA_HOSTNAMES.has(host) || isIpv4Loopback(host);
}

function hasExplicitRemoteOllamaApiProvider(
  providers: Record<string, OllamaProviderConfigInput | undefined> | undefined,
): boolean {
  if (!providers) {
    return false;
  }
  for (const [providerId, provider] of Object.entries(providers)) {
    if (providerId === OLLAMA_PROVIDER_ID || !provider) {
      continue;
    }
    if (normalizeOptionalString(provider.api)?.toLowerCase() !== "ollama") {
      continue;
    }
    const baseUrl = readProviderBaseUrl(provider);
    if (baseUrl && !isLoopbackOllamaBaseUrl(baseUrl)) {
      return true;
    }
  }
  return false;
}

export function shouldUseSyntheticOllamaAuth(
  providerConfig: OllamaProviderConfigInput | undefined,
): boolean {
  if (!hasMeaningfulExplicitOllamaConfig(providerConfig)) {
    return false;
  }
  return isLocalOllamaBaseUrl(readProviderBaseUrl(providerConfig));
}

function hasMeaningfulExplicitOllamaConfig(
  providerConfig: OllamaProviderConfigInput | undefined,
): boolean {
  if (!providerConfig) {
    return false;
  }
  if (Array.isArray(providerConfig.models) && providerConfig.models.length > 0) {
    return true;
  }
  const baseUrl = readProviderBaseUrl(providerConfig);
  if (baseUrl) {
    return resolveOllamaApiBase(baseUrl) !== OLLAMA_DEFAULT_BASE_URL;
  }
  if (readStringValue(providerConfig.apiKey)) {
    return true;
  }
  if (providerConfig.auth) {
    return true;
  }
  if (typeof providerConfig.authHeader === "boolean") {
    return true;
  }
  if (
    providerConfig.headers &&
    typeof providerConfig.headers === "object" &&
    Object.keys(providerConfig.headers).length > 0
  ) {
    return true;
  }
  if (providerConfig.request) {
    return true;
  }
  if (typeof providerConfig.injectNumCtxForOpenAICompat === "boolean") {
    return true;
  }
  return false;
}

export async function resolveOllamaDiscoveryResult(params: {
  ctx: OllamaDiscoveryContext;
  pluginConfig: OllamaPluginConfig;
  buildProvider: (
    configuredBaseUrl?: string,
    opts?: { quiet?: boolean },
  ) => Promise<ModelProviderConfig>;
}): Promise<{ provider: ModelProviderConfig } | null> {
  const explicit = params.ctx.config.models?.providers?.ollama;
  const hasExplicitModels = Array.isArray(explicit?.models) && explicit.models.length > 0;
  const hasMeaningfulExplicitConfig = hasMeaningfulExplicitOllamaConfig(explicit);
  const hasRemoteOllamaApiProvider = hasExplicitRemoteOllamaApiProvider(
    params.ctx.config.models?.providers,
  );
  const discoveryEnabled = params.pluginConfig.discovery?.enabled;
  if (!hasExplicitModels && discoveryEnabled === false) {
    return null;
  }
  // When the base URL points to hosted Ollama Cloud, skip auto-discovery.
  // Cloud instances are shared tenants where available models are managed
  // by the provider; only use explicitly configured models.
  // Remote self-hosted Ollama endpoints still auto-discover as before.
  const configuredBaseUrl = readProviderBaseUrl(explicit);
  if (!hasExplicitModels && configuredBaseUrl && isHostedOllamaCloud(configuredBaseUrl)) {
    return null;
  }
  const resolvedOllamaAuth = params.ctx.resolveProviderApiKey(OLLAMA_PROVIDER_ID);
  const ollamaKey = resolvedOllamaAuth.apiKey;
  const ollamaDiscoveryKey = resolvedOllamaAuth.discoveryApiKey;
  const hasOllamaDiscoveryOptIn = typeof ollamaKey === "string" && ollamaKey.trim().length > 0;
  const hasRealOllamaKey =
    typeof ollamaKey === "string" &&
    ollamaKey.trim().length > 0 &&
    ollamaKey.trim() !== OLLAMA_DEFAULT_API_KEY;
  const explicitApiKey = readStringValue(explicit?.apiKey);
  if (hasExplicitModels && explicit) {
    const discoveredBaseUrl = resolveOllamaApiBase(configuredBaseUrl);
    const api = explicit.api ?? "ollama";
    const apiKey = resolveOllamaDiscoveryApiKey({
      env: params.ctx.env,
      baseUrl: discoveredBaseUrl,
      explicitApiKey,
      resolvedApiKey: ollamaKey,
      resolvedDiscoveryApiKey: ollamaDiscoveryKey,
    });
    return {
      provider: {
        ...explicit,
        models: explicit.models ?? [],
        baseUrl: resolveOllamaRuntimeBaseUrl({ api, configuredBaseUrl, discoveredBaseUrl }),
        api,
        ...(apiKey ? { apiKey } : {}),
      },
    };
  }
  if (!hasMeaningfulExplicitConfig && hasRemoteOllamaApiProvider) {
    return null;
  }
  if (!hasOllamaDiscoveryOptIn && !hasMeaningfulExplicitConfig) {
    return null;
  }
  if (
    !hasRealOllamaKey &&
    !hasMeaningfulExplicitConfig &&
    shouldSkipAmbientOllamaDiscovery(params.ctx.env)
  ) {
    return null;
  }

  const quiet = !hasRealOllamaKey && !hasMeaningfulExplicitConfig;
  const provider = await getCachedLiveCatalogValue({
    keyParts: [
      OLLAMA_PROVIDER_ID,
      "models",
      configuredBaseUrl ?? OLLAMA_DEFAULT_BASE_URL,
      ollamaKey,
      quiet,
    ],
    load: async () =>
      await params.buildProvider(configuredBaseUrl, {
        quiet,
      }),
  });
  if (provider.models?.length === 0 && !ollamaKey && !explicit?.apiKey) {
    return null;
  }
  const apiKey = resolveOllamaDiscoveryApiKey({
    env: params.ctx.env,
    baseUrl: provider.baseUrl ?? configuredBaseUrl,
    explicitApiKey,
    resolvedApiKey: ollamaKey,
    resolvedDiscoveryApiKey: ollamaDiscoveryKey,
  });
  const api = explicit?.api ?? provider.api;
  return {
    provider: {
      ...provider,
      baseUrl: resolveOllamaRuntimeBaseUrl({
        api,
        configuredBaseUrl,
        discoveredBaseUrl: provider.baseUrl,
      }),
      api,
      ...(apiKey ? { apiKey } : {}),
    },
  };
}
