import type { Api, Model } from "@mariozechner/pi-ai";
import type { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent";
import type { OpenClawConfig } from "../../config/config.js";
import type { ModelDefinitionConfig } from "../../config/types.js";
import { resolveOpenClawAgentDir } from "../agent-paths.js";
import { DEFAULT_CONTEXT_TOKENS } from "../defaults.js";
import { buildModelAliasLines } from "../model-alias-lines.js";
import { normalizeModelCompat } from "../model-compat.js";
import { resolveForwardCompatModel } from "../model-forward-compat.js";
import { findNormalizedProviderValue, normalizeProviderId } from "../model-selection.js";
import { discoverAuthStorage, discoverModels } from "../pi-model-discovery.js";

type InlineModelEntry = ModelDefinitionConfig & {
  provider: string;
  baseUrl?: string;
  headers?: Record<string, string>;
};
type InlineProviderConfig = {
  baseUrl?: string;
  api?: ModelDefinitionConfig["api"];
  models?: ModelDefinitionConfig[];
  headers?: Record<string, string>;
};

export { buildModelAliasLines };

function resolveConfiguredProviderConfig(
  cfg: OpenClawConfig | undefined,
  provider: string,
): InlineProviderConfig | undefined {
  const configuredProviders = cfg?.models?.providers;
  if (!configuredProviders) {
    return undefined;
  }
  const exactProviderConfig = configuredProviders[provider];
  if (exactProviderConfig) {
    return exactProviderConfig;
  }
  return findNormalizedProviderValue(configuredProviders, provider);
}

function applyConfiguredProviderOverrides(params: {
  discoveredModel: Model<Api>;
  providerConfig?: InlineProviderConfig;
  modelId: string;
}): Model<Api> {
  const { discoveredModel, providerConfig, modelId } = params;
  if (!providerConfig) {
    return discoveredModel;
  }
  const configuredModel = providerConfig.models?.find((candidate) => candidate.id === modelId);
  if (
    !configuredModel &&
    !providerConfig.baseUrl &&
    !providerConfig.api &&
    !providerConfig.headers
  ) {
    return discoveredModel;
  }
  return {
    ...discoveredModel,
    api: configuredModel?.api ?? providerConfig.api ?? discoveredModel.api,
    baseUrl: providerConfig.baseUrl ?? discoveredModel.baseUrl,
    reasoning: configuredModel?.reasoning ?? discoveredModel.reasoning,
    input: configuredModel?.input ?? discoveredModel.input,
    cost: configuredModel?.cost ?? discoveredModel.cost,
    contextWindow: configuredModel?.contextWindow ?? discoveredModel.contextWindow,
    maxTokens: configuredModel?.maxTokens ?? discoveredModel.maxTokens,
    headers:
      providerConfig.headers || configuredModel?.headers
        ? {
            ...discoveredModel.headers,
            ...providerConfig.headers,
            ...configuredModel?.headers,
          }
        : discoveredModel.headers,
    compat: configuredModel?.compat ?? discoveredModel.compat,
  };
}

export function buildInlineProviderModels(
  providers: Record<string, InlineProviderConfig>,
): InlineModelEntry[] {
  return Object.entries(providers).flatMap(([providerId, entry]) => {
    const trimmed = providerId.trim();
    if (!trimmed) {
      return [];
    }
    return (entry?.models ?? []).map((model) => ({
      ...model,
      provider: trimmed,
      baseUrl: entry?.baseUrl,
      api: model.api ?? entry?.api,
      headers:
        entry?.headers || (model as InlineModelEntry).headers
          ? { ...entry?.headers, ...(model as InlineModelEntry).headers }
          : undefined,
    }));
  });
}

export function resolveModelWithRegistry(params: {
  provider: string;
  modelId: string;
  modelRegistry: ModelRegistry;
  cfg?: OpenClawConfig;
}): Model<Api> | undefined {
  const { provider, modelId, modelRegistry, cfg } = params;
  const providerConfig = resolveConfiguredProviderConfig(cfg, provider);
  const model = modelRegistry.find(provider, modelId) as Model<Api> | null;

  if (model) {
    return normalizeModelCompat(
      applyConfiguredProviderOverrides({
        discoveredModel: model,
        providerConfig,
        modelId,
      }),
    );
  }

  const providers = cfg?.models?.providers ?? {};
  const inlineModels = buildInlineProviderModels(providers);
  const normalizedProvider = normalizeProviderId(provider);
  const inlineMatch = inlineModels.find(
    (entry) => normalizeProviderId(entry.provider) === normalizedProvider && entry.id === modelId,
  );
  if (inlineMatch) {
    return normalizeModelCompat(inlineMatch as Model<Api>);
  }

  // Forward-compat fallbacks must be checked BEFORE the generic providerCfg fallback.
  // Otherwise, configured providers can default to a generic API and break specific transports.
  const forwardCompat = resolveForwardCompatModel(provider, modelId, modelRegistry);
  if (forwardCompat) {
    return normalizeModelCompat(
      applyConfiguredProviderOverrides({
        discoveredModel: forwardCompat,
        providerConfig,
        modelId,
      }),
    );
  }

  // OpenRouter is a pass-through proxy - any model ID available on OpenRouter
  // should work without being pre-registered in the local catalog.
  if (normalizedProvider === "openrouter") {
    return normalizeModelCompat({
      id: modelId,
      name: modelId,
      api: "openai-completions",
      provider,
      baseUrl: "https://openrouter.ai/api/v1",
      reasoning: false,
      input: ["text"],
      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
      contextWindow: DEFAULT_CONTEXT_TOKENS,
      // Align with OPENROUTER_DEFAULT_MAX_TOKENS in models-config.providers.ts
      maxTokens: 8192,
    } as Model<Api>);
  }

  const configuredModel = providerConfig?.models?.find((candidate) => candidate.id === modelId);
  if (providerConfig || modelId.startsWith("mock-")) {
    return normalizeModelCompat({
      id: modelId,
      name: modelId,
      api: providerConfig?.api ?? "openai-responses",
      provider,
      baseUrl: providerConfig?.baseUrl,
      reasoning: configuredModel?.reasoning ?? false,
      input: ["text"],
      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
      contextWindow:
        configuredModel?.contextWindow ??
        providerConfig?.models?.[0]?.contextWindow ??
        DEFAULT_CONTEXT_TOKENS,
      maxTokens:
        configuredModel?.maxTokens ??
        providerConfig?.models?.[0]?.maxTokens ??
        DEFAULT_CONTEXT_TOKENS,
      headers:
        providerConfig?.headers || configuredModel?.headers
          ? { ...providerConfig?.headers, ...configuredModel?.headers }
          : undefined,
    } as Model<Api>);
  }

  return undefined;
}

export function resolveModel(
  provider: string,
  modelId: string,
  agentDir?: string,
  cfg?: OpenClawConfig,
): {
  model?: Model<Api>;
  error?: string;
  authStorage: AuthStorage;
  modelRegistry: ModelRegistry;
} {
  const resolvedAgentDir = agentDir ?? resolveOpenClawAgentDir();
  const authStorage = discoverAuthStorage(resolvedAgentDir);
  const modelRegistry = discoverModels(authStorage, resolvedAgentDir);
  const model = resolveModelWithRegistry({ provider, modelId, modelRegistry, cfg });
  if (model) {
    return { model, authStorage, modelRegistry };
  }

  return {
    error: buildUnknownModelError(provider, modelId),
    authStorage,
    modelRegistry,
  };
}

/**
 * Build a more helpful error when the model is not found.
 *
 * Local providers (ollama, vllm) need a dummy API key to be registered.
 * Users often configure `agents.defaults.model.primary: "ollama/…"` but
 * forget to set `OLLAMA_API_KEY`, resulting in a confusing "Unknown model"
 * error.  This detects known providers that require opt-in auth and adds
 * a hint.
 *
 * See: https://github.com/openclaw/openclaw/issues/17328
 */
const LOCAL_PROVIDER_HINTS: Record<string, string> = {
  ollama:
    "Ollama requires authentication to be registered as a provider. " +
    'Set OLLAMA_API_KEY="ollama-local" (any value works) or run "openclaw configure". ' +
    "See: https://docs.openclaw.ai/providers/ollama",
  vllm:
    "vLLM requires authentication to be registered as a provider. " +
    'Set VLLM_API_KEY (any value works) or run "openclaw configure". ' +
    "See: https://docs.openclaw.ai/providers/vllm",
};

function buildUnknownModelError(provider: string, modelId: string): string {
  const base = `Unknown model: ${provider}/${modelId}`;
  const hint = LOCAL_PROVIDER_HINTS[provider.toLowerCase()];
  return hint ? `${base}. ${hint}` : base;
}
