import type { GoatChainContext } from '#cli/core/model'
import type { McpServerConfig } from '#cli/compat/mcp'
import type { AgentRuntime, CreateAgentRuntimeOptions, RuntimeAdapter, RuntimeConfig } from '#cli/compat/runtime'
import { inferBuiltinModelContextWindow } from '#cli/compat/providers'
import {
  inspectProviderEnv,
  readProviderApiKeyForAdapter,
  readProviderBaseUrlForAdapter,
  resolveRuntimeAdapter,
} from '#cli/core/provider-env'
import { createAgentRuntime } from '#cli/compat/runtime'

type DynamicToolValue<T> = T | (() => T | undefined)

type CliRuntimeTools = Readonly<{
  websearch?: Readonly<{
    apiKey?: DynamicToolValue<string>
    apiEndpoint?: DynamicToolValue<string>
    numResults?: DynamicToolValue<number>
  }>
}>

type CliWebSearchInput = Readonly<{
  domains?: string[]
  limit: number
  query: string
  recencyDays?: number
}>

type CliWebSearchResult = Readonly<{
  link: string
  snippet?: string
  title?: string
}>

const DEFAULT_WEB_SEARCH_API_ENDPOINT = 'https://google.serper.dev/search?format=json'
const DEFAULT_WEB_SEARCH_NUM_RESULTS = 10

function normalizeText(value: unknown): string {
  return String(value ?? '').trim()
}

function normalizeOpt(value: unknown): string | undefined {
  const next = normalizeText(value)
  return next || undefined
}

function readDynamicToolValue<T>(value: DynamicToolValue<T> | undefined): T | undefined {
  return typeof value === 'function'
    ? (value as (() => T | undefined))()
    : value
}

function parseBool(value: unknown): boolean | undefined {
  const normalized = normalizeText(value).toLowerCase()
  if (!normalized)
    return undefined
  if (normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on')
    return true
  if (normalized === '0' || normalized === 'false' || normalized === 'no' || normalized === 'off')
    return false
  return undefined
}

function parsePositiveInt(value: unknown): number | undefined {
  const normalized = normalizeText(value)
  if (!normalized)
    return undefined
  const parsed = Number.parseInt(normalized, 10)
  if (!Number.isFinite(parsed) || parsed <= 0)
    return undefined
  return parsed
}

function cloneEnvRecord(
  value: Record<string, string | undefined> | null | undefined,
): Record<string, string | undefined> {
  return Object.entries(value ?? {}).reduce<Record<string, string | undefined>>(
    (next, [key, entry]) => {
      next[key] = entry
      return next
    },
    {},
  )
}

function resolveCliWebSearchSettings(
  tools: CliRuntimeTools | undefined,
): Readonly<{
  apiEndpoint: string
  apiKey?: string
  numResults: number
}> {
  return {
    apiEndpoint:
      normalizeOpt(readDynamicToolValue(tools?.websearch?.apiEndpoint))
      ?? DEFAULT_WEB_SEARCH_API_ENDPOINT,
    apiKey: normalizeOpt(readDynamicToolValue(tools?.websearch?.apiKey)),
    numResults:
      parsePositiveInt(readDynamicToolValue(tools?.websearch?.numResults))
      ?? DEFAULT_WEB_SEARCH_NUM_RESULTS,
  }
}

function buildCliWebSearchQuery(input: CliWebSearchInput): string {
  const query = normalizeText(input.query)
  const domains = (input.domains ?? [])
    .map(domain => normalizeOpt(domain))
    .filter((domain): domain is string => Boolean(domain))
  if (domains.length === 0)
    return query
  if (domains.length === 1)
    return `${query} site:${domains[0]}`
  return `${query} (${domains.map(domain => `site:${domain}`).join(' OR ')})`
}

function selectCliWebSearchResults(payload: unknown): CliWebSearchResult[] {
  if (!payload || typeof payload !== 'object' || Array.isArray(payload))
    return []
  const organic = (payload as { organic?: unknown }).organic
  if (!Array.isArray(organic))
    return []
  return organic.flatMap((entry) => {
    if (!entry || typeof entry !== 'object' || Array.isArray(entry))
      return []
    const record = entry as Record<string, unknown>
    const link = normalizeOpt(record.link)
    if (!link)
      return []
    return [{
      link,
      snippet: normalizeOpt(record.snippet),
      title: normalizeOpt(record.title),
    }]
  })
}

function formatCliWebSearchResults(input: Readonly<{
  query: string
  results: readonly CliWebSearchResult[]
}>): string {
  const lines = [`Search results for "${input.query}" (${input.results.length} found):`]
  if (input.results.length === 0) {
    lines.push('No results found.')
    return lines.join('\n')
  }
  input.results.forEach((result, index) => {
    lines.push(`${index + 1}. ${result.title ?? result.link}`)
    lines.push(`   URL: ${result.link}`)
    if (result.snippet)
      lines.push(`   Snippet: ${result.snippet}`)
  })
  return lines.join('\n')
}

function createCliWebSearchAdapter(
  tools: CliRuntimeTools | undefined,
):
  | Readonly<{
    webSearch: (input: CliWebSearchInput) => Promise<string>
    webSearchAvailability: () => boolean
  }>
  | undefined {
  if (!tools?.websearch)
    return undefined
  return {
    webSearchAvailability: () => Boolean(resolveCliWebSearchSettings(tools).apiKey),
    webSearch: async (input) => {
      const settings = resolveCliWebSearchSettings(tools)
      if (!settings.apiKey) {
        return 'Web search is not available in this runtime. Use fetch-url with a known URL or ask the user for a source.'
      }
      const limit = Math.min(
        parsePositiveInt(input.limit) ?? settings.numResults,
        DEFAULT_WEB_SEARCH_NUM_RESULTS,
      )
      const response = await globalThis.fetch(settings.apiEndpoint, {
        body: JSON.stringify({
          num: limit,
          q: buildCliWebSearchQuery(input),
        }),
        headers: {
          'Content-Type': 'application/json',
          'X-API-KEY': settings.apiKey,
        },
        method: 'POST',
      })
      if (!response.ok) {
        throw new Error(
          `Web search request failed with ${response.status} ${response.statusText}: ${await response.text()}`,
        )
      }
      const payload = await response.json()
      return formatCliWebSearchResults({
        query: normalizeText(input.query),
        results: selectCliWebSearchResults(payload).slice(0, limit),
      })
    },
  }
}

function mergeRuntimeConfig(
  base: RuntimeConfig,
  source: RuntimeConfig | undefined,
  opts: Readonly<{ preferSourceMaxInputTokens?: boolean }> = {},
): RuntimeConfig {
  const modelId = normalizeOpt(source?.modelId) ?? base.modelId
  const adapter
    = (normalizeOpt(source?.adapter) as RuntimeConfig['adapter'] | undefined)
      ?? base.adapter
  const provider = normalizeOpt(source?.provider) ?? base.provider ?? adapter
  const baseUrl = normalizeOpt(source?.baseUrl) ?? base.baseUrl
  const apiKey = normalizeOpt(source?.apiKey) ?? base.apiKey
  const sourceMaxInputTokens = parsePositiveInt(source?.maxInputTokens)
  const maxInputTokens = opts.preferSourceMaxInputTokens === false
    ? base.maxInputTokens ?? sourceMaxInputTokens
    : sourceMaxInputTokens ?? base.maxInputTokens
  const maxOutputTokens
    = parsePositiveInt(source?.maxOutputTokens) ?? base.maxOutputTokens
  const env = source?.env && typeof source.env === 'object'
    ? cloneEnvRecord(source.env)
    : base.env
      ? cloneEnvRecord(base.env)
      : undefined

  return {
    modelId,
    ...(adapter ? { adapter } : {}),
    ...(provider ? { provider } : {}),
    ...(baseUrl ? { baseUrl } : {}),
    ...(apiKey ? { apiKey } : {}),
    ...(source?.nextApiOAuth === true || base.nextApiOAuth === true
      ? { nextApiOAuth: true }
      : {}),
    ...(typeof maxInputTokens === 'number' ? { maxInputTokens } : {}),
    ...(typeof maxOutputTokens === 'number' ? { maxOutputTokens } : {}),
    ...(env ? { env } : {}),
  }
}

export function readCliRuntimeConfig(env: Record<string, string | undefined>): RuntimeConfig {
  const adapter = resolveRuntimeAdapter(inspectProviderEnv(env)) as RuntimeAdapter
  const providerId = normalizeOpt(env.DIMCODE_PROVIDER_ID)
  const modelId = normalizeOpt(env.DIMCODE_MODEL) ?? 'deepseek-v3.1'
  const baseUrl = normalizeOpt(readProviderBaseUrlForAdapter(env, adapter))
  const apiKey = normalizeOpt(readProviderApiKeyForAdapter(env, adapter))
  const nextApiOAuth = providerId === 'next-api-oauth'
  const maxInputTokens = parsePositiveInt(env.DIMCODE_CONTEXT_WINDOW)
    ?? inferBuiltinModelContextWindow(modelId, adapter)
  const maxOutputTokens = parsePositiveInt(env.DIMCODE_MAX_TOKENS)

  return {
    modelId,
    adapter,
    provider: providerId ?? adapter,
    ...(baseUrl ? { baseUrl } : {}),
    ...(apiKey ? { apiKey } : {}),
    ...(nextApiOAuth ? { nextApiOAuth: true } : {}),
    ...(maxInputTokens ? { maxInputTokens } : {}),
    ...(maxOutputTokens ? { maxOutputTokens } : {}),
    env: { ...env },
  }
}

export function buildCliCreateAgentRuntimeOptions(opts: Readonly<{
  context: GoatChainContext
  stateDir: string
  processEnv?: Record<string, string | undefined>
  tools?: CliRuntimeTools
  mcpServers?: readonly McpServerConfig[] | (() => readonly McpServerConfig[])
  forcedSkills?: readonly string[] | (() => readonly string[])
}>): CreateAgentRuntimeOptions {
  const getRuntimeEnv = (): Record<string, string | undefined> => ({
    ...cloneEnvRecord(
      (opts.context.env ?? {}) as Record<string, string | undefined>,
    ),
    ...cloneEnvRecord(opts.processEnv),
    ...cloneEnvRecord(
      (typeof process !== 'undefined' ? process.env : undefined) as
      | Record<string, string | undefined>
      | undefined,
    ),
  })
  const runtimeDebug = parseBool(getRuntimeEnv().DIMCODE_DEBUG) === true
  const runtimeConfig = readCliRuntimeConfig(getRuntimeEnv())
  const webSearchAdapter = createCliWebSearchAdapter(opts.tools)

  const runtimeOptions = {
    systemPrompt: opts.context.system,
    modelId: runtimeConfig.modelId,
    adapter: runtimeConfig.adapter,
    provider: runtimeConfig.provider,
    baseUrl: runtimeConfig.baseUrl,
    apiKey: runtimeConfig.apiKey,
    nextApiOAuth: runtimeConfig.nextApiOAuth,
    maxInputTokens: runtimeConfig.maxInputTokens,
    maxOutputTokens: runtimeConfig.maxOutputTokens,
    ...(typeof runtimeConfig.maxInputTokens === 'number'
      ? {
          memory: {
            contextTokenBudget: runtimeConfig.maxInputTokens,
          },
        }
      : {}),
    debug: runtimeDebug,
    stateDir: opts.stateDir,
    cliTools: true,
    delegateTasks: true,
    ...(opts.tools ? { tools: opts.tools as CreateAgentRuntimeOptions['tools'] } : {}),
    ...(opts.mcpServers ? { mcpServers: opts.mcpServers } : {}),
    ...(opts.forcedSkills ? { forcedSkills: opts.forcedSkills } : {}),
    ...(webSearchAdapter
      ? {
          webSearch: webSearchAdapter.webSearch,
          webSearchAvailability: webSearchAdapter.webSearchAvailability,
        }
      : {}),
    resolveRuntimeConfig: (input) => {
      const current = readCliRuntimeConfig(getRuntimeEnv())
      return input.requested
        ? mergeRuntimeConfig(current, input.requested, {
            preferSourceMaxInputTokens: true,
          })
        : mergeRuntimeConfig(current, input.existing, {
            preferSourceMaxInputTokens: false,
          })
    },
  } as CreateAgentRuntimeOptions & {
    webSearch?: (input: CliWebSearchInput) => Promise<string>
    webSearchAvailability?: () => boolean
  }

  return runtimeOptions
}

export async function createCliRuntime(opts: Readonly<{
  context: GoatChainContext
  stateDir: string
  processEnv?: Record<string, string | undefined>
  tools?: CliRuntimeTools
  mcpServers?: readonly McpServerConfig[] | (() => readonly McpServerConfig[])
  forcedSkills?: readonly string[] | (() => readonly string[])
}>): Promise<AgentRuntime> {
  return await createAgentRuntime(buildCliCreateAgentRuntimeOptions(opts))
}
