import type { GoatChainContext as DimSdkContext } from '#cli/core/model'
import type { DimcodeUserToolsV1 } from '#cli/compat/config'
import type { DimcodeUserCacheV1, DimcodeUserConfigV1 } from './user-cache'
import { loadDimcodeUserTools, loadMcpJson, migrateLegacyCustomProvider } from '#cli/compat/config'
import { DIMCODE_SYSTEM_PROMPT } from '#cli/compat/prompts/system'
import {
  buildProviderSelectionCandidates,
  getBuiltinProvider,
  getProviderAdapter,
  getProviderBaseUrl,
  getProviderDisplayName,
  isCustomProviderId,
  isLegacyCustomProviderId,
  LEGACY_DEFAULT_CONTEXT_WINDOW,
  listBuiltinProviders,
  normalizeProviderId,
  resolveBuiltinProviderBaseUrl,
  resolveProviderSelection,
  resolveProviderSelectionContextWindow,
  shouldIgnoreLegacyStoredContextWindow,
  stripUnknownActiveModelSelections,
  usesCcMock,
} from '#cli/compat/providers'
import {
  getDimcodeConfigDir,
  getDimcodeUserDir,
  loadDimcodeUserCache,
  loadDimcodeUserConfig,
  normalizeNum,
  normalizeStr,
} from './user-cache'

declare const __CLI_VERSION__: string | undefined

const RUNTIME_ENV_KEYS = new Set<string>([
  'OPENAI_API_KEY',
  'OPENAI_BASE_URL',
  'GOOGLE_API_KEY',
  'ANTHROPIC_API_KEY',
  'DIMCODE_MODEL',
  'DIMCODE_PROVIDER_ADAPTER',
  'DIMCODE_PROVIDER_ID',
  'DIMCODE_PROVIDER_NAME',
  'DIMCODE_OPENAI_BASE_URL',
  'DIMCODE_GEMINI_BASE_URL',
  'DIMCODE_ANTHROPIC_BASE_URL',
])

/**
 * Builds the runtime context passed to the app/store/agent.
 *
 * This function merges:
 * - explicit runtime env (only when compatible)
 * - last saved UI settings from `cache.json`
 * - provider settings from `config.json`
 */
export async function createDimcodeContext(
  opts: Readonly<{ cwd: string, env: Record<string, string | undefined> }>,
): Promise<
  Readonly<{
    context: DimSdkContext
    cacheDir: string
    cacheFile: string
    cache: DimcodeUserCacheV1
    configDir: string
    configFile: string
    config: DimcodeUserConfigV1
    toolsFile: string
    tools: DimcodeUserToolsV1
    mcpJsonFile: string
    mcpServers: import('#cli/compat/config').McpServerSettings[]
  }>
> {
  const fastStartRaw = String(opts.env.DIMCODE_FAST_START ?? '')
    .trim()
    .toLowerCase()
  const fastStart
    = fastStartRaw === '1'
      || fastStartRaw === 'true'
      || fastStartRaw === 'yes'
      || fastStartRaw === 'on'
  const version
    = normalizeStr(
      typeof __CLI_VERSION__ === 'string' ? __CLI_VERSION__ : undefined,
    ) ?? (fastStart ? null : await readRepoPackageVersion(opts.cwd))

  const resolveToolsPath = async () => {
    const [dir, path] = await Promise.all([
      getDimcodeConfigDir(opts.env),
      import('node:path'),
    ])
    return { dir, filePath: path.join(dir, 'tools.json') }
  }

  const [loadedCache, loadedConfig, loadedTools, loadedMcp] = fastStart
    ? await (async () => {
        const [cacheDir, configDir, toolsPath, path] = await Promise.all([
          getDimcodeUserDir(opts.env),
          getDimcodeConfigDir(opts.env),
          resolveToolsPath(),
          import('node:path'),
        ])
        const cacheFile = path.join(cacheDir, 'cache.json')
        const configFile = path.join(configDir, 'config.json')
        const mcpJsonFile = path.join(toolsPath.dir, 'mcp.json')
        return [
          {
            dir: cacheDir,
            filePath: cacheFile,
            cache: { version: 1, updatedAt: 0 } as DimcodeUserCacheV1,
          },
          {
            dir: configDir,
            filePath: configFile,
            config: { version: 1, updatedAt: 0 } as DimcodeUserConfigV1,
          },
          {
            dir: toolsPath.dir,
            filePath: toolsPath.filePath,
            tools: { version: 1, updatedAt: 0 } as DimcodeUserToolsV1,
          },
          {
            dir: toolsPath.dir,
            filePath: mcpJsonFile,
            servers: [] as import('#cli/compat/config').McpServerSettings[],
          },
        ] as const
      })()
    : await Promise.all([
        loadDimcodeUserCache(opts.env),
        loadDimcodeUserConfig(opts.env),
        loadDimcodeUserTools(opts.env),
        loadMcpJson(opts.env),
      ])
  const cache = loadedCache.cache
  const config = loadedConfig.config
  migrateLegacyCustomProvider(config)
  type SavedCacheSettings = NonNullable<DimcodeUserCacheV1['settings']>
  type SavedConfigSettings = NonNullable<DimcodeUserConfigV1['settings']>
  type MutableSavedConfigSettings = { providerId?: string }
  type SavedCustomProvider = NonNullable<
    SavedConfigSettings['customProviders']
  >[number]
  const saved: SavedCacheSettings = cache.settings ?? {}
  const savedConfig: SavedConfigSettings = config.settings ?? {}
  const mutableConfigSettings
    = config.settings as MutableSavedConfigSettings | undefined
  const savedCwd = normalizeStr(saved.cwd)
  const savedFocusFiles = Array.isArray(saved.focusFiles)
    ? saved.focusFiles.map((f: unknown) => String(f ?? '')).filter(Boolean)
    : undefined
  // Use current directory (opts.cwd) instead of savedCwd to ensure CLI runs in the actual working directory
  const cwd = opts.cwd || savedCwd || '.'

  const v = (key: string) => normalizeStr(opts.env[key])
  const explicitRuntimeEnv = Object.fromEntries(
    Object.entries(opts.env).flatMap(([key, value]) => {
      const canonical = String(key ?? '').trim()
      const normalized = normalizeStr(value)
      if (!canonical || !RUNTIME_ENV_KEYS.has(canonical) || !normalized)
        return []
      return [[canonical, normalized]]
    }),
  ) as Record<string, string>

  const applyRuntimeEnvIfMissing = (
    target: Record<string, string | undefined>,
    key: string,
    source: Record<string, string>,
  ): void => {
    const value = source[key]
    if (!value || target[key])
      return
    target[key] = value
  }

  const mergedEnv: Record<string, string | undefined> = {
    DIMCODE_VERSION: version ?? 'dev',
    ...(v('DIMCODE_STARTUP_TOAST')
      ? { DIMCODE_STARTUP_TOAST: v('DIMCODE_STARTUP_TOAST') }
      : {}),
    ...(v('DIMCODE_STARTUP_UPGRADE_TOAST')
      ? { DIMCODE_STARTUP_UPGRADE_TOAST: v('DIMCODE_STARTUP_UPGRADE_TOAST') }
      : {}),
  }

  const savedProviderId = normalizeProviderId(savedConfig.providerId)
  const savedConnections = (savedConfig.providerConnections ?? {}) as Record<
    string,
    { apiKey?: string, baseUrl?: string, models?: Array<{ id?: string }> }
  >
  const savedActiveModelByProvider = (savedConfig.activeModelByProvider
    ?? {}) as Record<string, string>
  const savedCustomProviders: ReadonlyArray<SavedCustomProvider>
    = Array.isArray(savedConfig.customProviders)
      ? (savedConfig.customProviders as ReadonlyArray<SavedCustomProvider>)
      : []
  const legacyCustom = savedConfig.customProvider
  const legacyBaseUrl = normalizeStr(legacyCustom?.baseUrl)
  const legacyApiKey = normalizeStr(legacyCustom?.apiKey)
  const legacyCustomModels = Array.isArray(legacyCustom?.models)
    ? legacyCustom.models
    : []
  const rawProviderId = (() => {
    if (
      isLegacyCustomProviderId(savedProviderId)
      && savedCustomProviders.length
    ) {
      const fallbackId = normalizeStr(savedCustomProviders[0]?.id)
      if (fallbackId) {
        if (mutableConfigSettings)
          mutableConfigSettings.providerId = fallbackId
        return fallbackId
      }
    }
    return savedProviderId
  })()

  const { candidates, existingProviderIds } = buildProviderSelectionCandidates({
    builtinProviders: listBuiltinProviders(),
    connections: savedConnections,
    customProviders: savedCustomProviders,
    legacyCustom,
    rememberedModels: {
      ...savedActiveModelByProvider,
    },
  })
  const rememberedModels = stripUnknownActiveModelSelections(
    {
      ...savedActiveModelByProvider,
    },
    existingProviderIds,
  )
  const selection = resolveProviderSelection({
    candidates,
    preferredProviderId: rawProviderId,
    rememberedModels,
  })
  const providerId = normalizeStr(selection.providerId)
  const providerModel = normalizeStr(selection.modelId)

  if (mutableConfigSettings) {
    if (providerId)
      mutableConfigSettings.providerId = providerId
    else delete mutableConfigSettings.providerId
  }

  const customProvider = isCustomProviderId(providerId)
    ? savedCustomProviders.find(
        p => String(p?.id ?? '').trim() === providerId,
      )
    : undefined

  const builtin
    = providerId
      && !isCustomProviderId(providerId)
      && !isLegacyCustomProviderId(providerId)
      ? getBuiltinProvider(providerId)
      : undefined
  const savedConnection = savedConnections[providerId ?? '']
  const savedConnectionBaseUrl = normalizeStr(savedConnection?.baseUrl)
  const builtinBaseUrl = normalizeStr(getProviderBaseUrl(builtin))
  const adapter = isCustomProviderId(providerId)
    ? customProvider?.adapter
    : isLegacyCustomProviderId(providerId)
      ? 'openai'
      : providerId === 'openai'
        && savedConnectionBaseUrl
        && savedConnectionBaseUrl !== builtinBaseUrl
        ? 'openai'
        : getProviderAdapter(builtin)
  const providerName = providerId
    ? isCustomProviderId(providerId)
      ? normalizeStr(customProvider?.name)
      : isLegacyCustomProviderId(providerId)
        ? 'Custom Provider'
        : getProviderDisplayName(builtin ?? { id: providerId })
    : undefined
  const providerBaseUrl = isCustomProviderId(providerId)
    ? normalizeStr(customProvider?.baseUrl)
    : isLegacyCustomProviderId(providerId)
      ? legacyBaseUrl
      : normalizeStr(
          resolveBuiltinProviderBaseUrl(builtin, savedConnection?.baseUrl),
        )
  const providerApiKey = isCustomProviderId(providerId)
    ? normalizeStr(customProvider?.apiKey)
    : isLegacyCustomProviderId(providerId)
      ? legacyApiKey
      : normalizeStr(savedConnection?.apiKey)
  const resolvedModelContextWindow = resolveProviderSelectionContextWindow({
    providerId,
    modelId: providerModel,
    connections: savedConnections,
    customProviders: savedCustomProviders,
    legacyCustomModels,
  })
  const savedContextWindow = normalizeNum(saved.contextWindow)
  const effectiveSavedContextWindow = shouldIgnoreLegacyStoredContextWindow(
    savedContextWindow,
    resolvedModelContextWindow,
  )
    ? undefined
    : savedContextWindow

  if (
    isLegacyCustomProviderId(providerId)
    && !providerBaseUrl
    && !providerApiKey
    && !providerModel
  ) {
    throw new Error(
      'Legacy custom provider config is missing. Please run /connect to add a custom provider and select a model.',
    )
  }

  if (!mergedEnv.DIMCODE_PROVIDER_ADAPTER && adapter)
    mergedEnv.DIMCODE_PROVIDER_ADAPTER = adapter
  if (!mergedEnv.DIMCODE_PROVIDER_ID && providerId)
    mergedEnv.DIMCODE_PROVIDER_ID = providerId
  if (!mergedEnv.DIMCODE_PROVIDER_NAME && providerName)
    mergedEnv.DIMCODE_PROVIDER_NAME = providerName

  if (!mergedEnv.DIMCODE_MODEL && providerModel)
    mergedEnv.DIMCODE_MODEL = providerModel
  if (adapter === 'gemini') {
    if (!mergedEnv.DIMCODE_GEMINI_BASE_URL && providerBaseUrl)
      mergedEnv.DIMCODE_GEMINI_BASE_URL = providerBaseUrl
    if (!mergedEnv.GOOGLE_API_KEY && providerApiKey)
      mergedEnv.GOOGLE_API_KEY = providerApiKey
  }
  else if (adapter === 'anthropic') {
    if (!mergedEnv.DIMCODE_ANTHROPIC_BASE_URL && providerBaseUrl)
      mergedEnv.DIMCODE_ANTHROPIC_BASE_URL = providerBaseUrl
    if (!mergedEnv.ANTHROPIC_API_KEY && providerApiKey)
      mergedEnv.ANTHROPIC_API_KEY = providerApiKey
  }
  else {
    if (!mergedEnv.DIMCODE_OPENAI_BASE_URL && providerBaseUrl)
      mergedEnv.DIMCODE_OPENAI_BASE_URL = providerBaseUrl
    if (!mergedEnv.OPENAI_API_KEY && providerApiKey)
      mergedEnv.OPENAI_API_KEY = providerApiKey
  }

  if (!mergedEnv.DIMCODE_CONTEXT_WINDOW) {
    const nextContextWindow
      = resolvedModelContextWindow ?? effectiveSavedContextWindow
    if (typeof nextContextWindow === 'number' && nextContextWindow > 0)
      mergedEnv.DIMCODE_CONTEXT_WINDOW = String(nextContextWindow)
    else
      mergedEnv.DIMCODE_CONTEXT_WINDOW = String(LEGACY_DEFAULT_CONTEXT_WINDOW)
  }
  if (!mergedEnv.DIMCODE_TEMPERATURE && normalizeNum(saved.temperature) != null)
    mergedEnv.DIMCODE_TEMPERATURE = String(saved.temperature)
  if (
    !mergedEnv.DIMCODE_INTERLEAVED_THINKING
    && typeof saved.interleavedThinking === 'boolean'
  ) {
    mergedEnv.DIMCODE_INTERLEAVED_THINKING = saved.interleavedThinking
      ? '1'
      : '0'
  }
  if (!mergedEnv.DIMCODE_TOOL_APPROVALS && saved.toolApprovals) {
    const raw = String(saved.toolApprovals)
    const mapped
      = (raw === 'auto' || raw === 'all')
        ? 'auto'
        : (raw === 'strict' || raw === 'manual')
            ? 'strict'
            : raw === 'normal'
              ? 'normal'
              : undefined
    if (mapped)
      mergedEnv.DIMCODE_TOOL_APPROVALS = mapped
  }
  if (!mergedEnv.DIMCODE_LANG && normalizeStr(saved.locale))
    mergedEnv.DIMCODE_LANG = normalizeStr(saved.locale)
  if (!mergedEnv.DIMCODE_THEME && normalizeStr(saved.themeMode))
    mergedEnv.DIMCODE_THEME = normalizeStr(saved.themeMode)

  const ccmockEnabled = usesCcMock(builtin)
  if (ccmockEnabled) {
    mergedEnv.DIMCODE_CCMOCK = '1'
  }
  else {
    delete mergedEnv.DIMCODE_CCMOCK
  }

  // Merge explicit runtime env overrides after UI-derived defaults so bare provider
  // keys from process/env cannot shadow persisted session configuration.
  applyRuntimeEnvIfMissing(
    mergedEnv,
    'DIMCODE_PROVIDER_ADAPTER',
    explicitRuntimeEnv,
  )
  applyRuntimeEnvIfMissing(
    mergedEnv,
    'DIMCODE_PROVIDER_ID',
    explicitRuntimeEnv,
  )
  applyRuntimeEnvIfMissing(
    mergedEnv,
    'DIMCODE_PROVIDER_NAME',
    explicitRuntimeEnv,
  )
  applyRuntimeEnvIfMissing(mergedEnv, 'DIMCODE_MODEL', explicitRuntimeEnv)
  applyRuntimeEnvIfMissing(
    mergedEnv,
    'DIMCODE_OPENAI_BASE_URL',
    explicitRuntimeEnv,
  )
  applyRuntimeEnvIfMissing(
    mergedEnv,
    'DIMCODE_GEMINI_BASE_URL',
    explicitRuntimeEnv,
  )
  applyRuntimeEnvIfMissing(
    mergedEnv,
    'DIMCODE_ANTHROPIC_BASE_URL',
    explicitRuntimeEnv,
  )
  if (!mergedEnv.DIMCODE_PROVIDER_ID && !mergedEnv.DIMCODE_PROVIDER_ADAPTER) {
    // For legacy env-only setups, keep bare runtime keys only when no session UI
    // provider context is resolved.
    applyRuntimeEnvIfMissing(mergedEnv, 'OPENAI_API_KEY', explicitRuntimeEnv)
    applyRuntimeEnvIfMissing(mergedEnv, 'OPENAI_BASE_URL', explicitRuntimeEnv)
    applyRuntimeEnvIfMissing(mergedEnv, 'GOOGLE_API_KEY', explicitRuntimeEnv)
    applyRuntimeEnvIfMissing(mergedEnv, 'ANTHROPIC_API_KEY', explicitRuntimeEnv)
  }

  return {
    context: {
      system: DIMCODE_SYSTEM_PROMPT,
      cwd,
      env: mergedEnv,
      ...(savedFocusFiles?.length ? { focusFiles: savedFocusFiles } : {}),
    },
    cacheDir: loadedCache.dir,
    cacheFile: loadedCache.filePath,
    cache,
    configDir: loadedConfig.dir,
    configFile: loadedConfig.filePath,
    config,
    toolsFile: loadedTools.filePath,
    tools: loadedTools.tools,
    mcpJsonFile: loadedMcp.filePath,
    mcpServers: loadedMcp.servers,
  } as const
}

export async function readRepoPackageVersion(
  cwd: string,
): Promise<string | null> {
  try {
    const [{ readFile }, path] = await Promise.all([
      import('node:fs/promises'),
      import('node:path'),
    ])
    const raw = await readFile(path.join(cwd, 'package.json'), 'utf8')
    const parsed = JSON.parse(raw)
    const v = typeof parsed?.version === 'string' ? parsed.version : null
    return v && v.trim() ? v.trim() : null
  }
  catch {
    return null
  }
}
