import type {
  BuiltinProviderId,
  CustomProviderId,
  LegacyCustomProviderId,
  ProviderAdapter,
  ProviderCustomModel,
  ProviderDefinition,
  ProviderId,
  ProviderModel,
} from './types'
import _aihubmix from './aihubmix.json'

import _anthropic from './anthropic.json'
import _arkCode from './ark-code.json'
import _bailianCodingPlan from './bailian-coding-plan.json'
import _deepseek from './deepseek.json'
import _google from './google.json'
import _kimiForCoding from './kimi-for-coding.json'
import _lmStudio from './lm-studio.json'
import _minimaxCnCodingPlan from './minimax-cn-coding-plan.json'
import _minimaxCn from './minimax-cn.json'
import _minimaxCodingPlan from './minimax-coding-plan.json'
import _minimax from './minimax.json'
import _modelsAihubmix from './models/aihubmix.json'
import _modelsOpenrouter from './models/openrouter.json'
import _modelsSiliconflowCom from './models/siliconflow-com.json'
import _modelsSiliconflow from './models/siliconflow.json'
import _modelsZenmux from './models/zenmux.json'
import _moonshotAi from './moonshot-ai.json'
import _moonshot from './moonshot.json'
import _nextApiOAuth from './next-api-oauth.json'
import _ollama from './ollama.json'
import _openai from './openai.json'
import _openrouter from './openrouter.json'
import _ppinfra from './ppinfra.json'
import _siliconflowCom from './siliconflow-com.json'
import _siliconflow from './siliconflow.json'
import { BUILTIN_PROVIDER_IDS } from './types'
import _xai from './xai.json'

import _zaiCodingPlan from './zai-coding-plan.json'
import _zai from './zai.json'
import _zenmux from './zenmux.json'
import _zhipuaiCodingPlan from './zhipuai-coding-plan.json'
import _zhipuai from './zhipuai.json'

export {
  fetchModelsForProviderAdapter,
  testProviderConnection,
} from './provider-api'
export {
  buildProviderSelectionCandidates,
  resolveProviderSelection,
  sanitizeWorkspaceProviderSelections,
  stripUnknownActiveModelSelections,
} from './provider-selection'
export type {
  BuiltinProviderId,
  CustomProvider,
  CustomProviderId,
  LegacyCustomProviderId,
  ProviderAdapter,
  ProviderConnection,
  ProviderCustomConfig,
  ProviderCustomModel,
  ProviderDefinition,
  ProviderId,
  ProviderModel,
  ProviderState,
} from './types'

type ProviderJson = Omit<ProviderDefinition, 'models'> & {
  models?: ProviderModel[]
  modelsFile?: string
}

const providerModelCache = new Map<string, ProviderModel[]>()
const builtinProviderCache = new Map<BuiltinProviderId, ProviderDefinition>()
let builtinProvidersCache: ProviderDefinition[] | null = null
const BUILTIN_PROVIDER_BASE_URL_EDITABLE_IDS = new Set<BuiltinProviderId>([
  'anthropic',
  'google',
  'lm-studio',
  'ollama',
  'openai',
])

export const LEGACY_DEFAULT_CONTEXT_WINDOW = 128000
type ContextWindowModelLike = Readonly<{
  id?: string
  limit?: { context?: number, output?: number }
}>

const providerJsonLoaders: Record<BuiltinProviderId, () => ProviderJson> = {
  'openai': () => _openai as unknown as ProviderJson,
  'next-api-oauth': () => _nextApiOAuth as unknown as ProviderJson,
  'openrouter': () => _openrouter as unknown as ProviderJson,
  'aihubmix': () => _aihubmix as unknown as ProviderJson,
  'bailian-coding-plan': () => _bailianCodingPlan as unknown as ProviderJson,
  'zhipuai': () => _zhipuai as unknown as ProviderJson,
  'zhipuai-coding-plan': () => _zhipuaiCodingPlan as unknown as ProviderJson,
  'zai': () => _zai as unknown as ProviderJson,
  'zai-coding-plan': () => _zaiCodingPlan as unknown as ProviderJson,
  'deepseek': () => _deepseek as unknown as ProviderJson,
  'zenmux': () => _zenmux as unknown as ProviderJson,
  'moonshot': () => _moonshot as unknown as ProviderJson,
  'moonshot-ai': () => _moonshotAi as unknown as ProviderJson,
  'kimi-for-coding': () => _kimiForCoding as unknown as ProviderJson,
  'google': () => _google as unknown as ProviderJson,
  'anthropic': () => _anthropic as unknown as ProviderJson,
  'siliconflow': () => _siliconflow as unknown as ProviderJson,
  'siliconflow-com': () => _siliconflowCom as unknown as ProviderJson,
  'ppinfra': () => _ppinfra as unknown as ProviderJson,
  'ark-code': () => _arkCode as unknown as ProviderJson,
  'ollama': () => _ollama as unknown as ProviderJson,
  'lm-studio': () => _lmStudio as unknown as ProviderJson,
  'xai': () => _xai as unknown as ProviderJson,
  'minimax-cn': () => _minimaxCn as unknown as ProviderJson,
  'minimax-cn-coding-plan': () =>
    _minimaxCnCodingPlan as unknown as ProviderJson,
  'minimax': () => _minimax as unknown as ProviderJson,
  'minimax-coding-plan': () => _minimaxCodingPlan as unknown as ProviderJson,
}

const embeddedModelFiles: Record<string, unknown> = {
  'models/aihubmix.json': _modelsAihubmix,
  'models/openrouter.json': _modelsOpenrouter,
  'models/siliconflow.json': _modelsSiliconflow,
  'models/siliconflow-com.json': _modelsSiliconflowCom,
  'models/zenmux.json': _modelsZenmux,
}

function loadModelsFromFile(
  modelsFile: string,
  providerId: string,
): ProviderModel[] {
  const cached = providerModelCache.get(providerId)
  if (cached)
    return cached

  const embedded = embeddedModelFiles[modelsFile]
  if (embedded) {
    const models = Array.isArray(embedded) ? (embedded as ProviderModel[]) : []
    providerModelCache.set(providerId, models)
    return models
  }

  console.warn(
    `[providers] No embedded models for ${providerId} (${modelsFile})`,
  )
  const empty: ProviderModel[] = []
  providerModelCache.set(providerId, empty)
  return empty
}

function resolveProviderDefinition(raw: ProviderJson): ProviderDefinition {
  if (Array.isArray(raw.models))
    return raw as ProviderDefinition

  const modelsFile = String(raw.modelsFile ?? '').trim()
  if (modelsFile) {
    const meta = { ...raw } as ProviderJson
    delete (meta as ProviderJson).models
    delete (meta as ProviderJson).modelsFile
    const provider = {
      ...meta,
      models: [] as ProviderModel[],
    } as ProviderDefinition
    let cached: ProviderModel[] | null = null
    Object.defineProperty(provider, 'models', {
      enumerable: true,
      get() {
        if (!cached)
          cached = loadModelsFromFile(modelsFile, provider.id)
        return cached
      },
    })
    return provider
  }

  const meta = { ...raw } as ProviderJson
  delete (meta as ProviderJson).models
  delete (meta as ProviderJson).modelsFile
  return {
    ...meta,
    models: [],
  } as ProviderDefinition
}

function resolveBuiltinProvider(id: BuiltinProviderId): ProviderDefinition {
  const cached = builtinProviderCache.get(id)
  if (cached)
    return cached
  const loader = providerJsonLoaders[id]
  if (!loader)
    throw new Error(`[providers] Missing provider loader for ${id}`)
  const raw = loader()
  const resolved = resolveProviderDefinition(raw)
  builtinProviderCache.set(id, resolved)
  return resolved
}

function listAllBuiltinProviders(): readonly ProviderDefinition[] {
  if (!builtinProvidersCache) {
    builtinProvidersCache = BUILTIN_PROVIDER_IDS.map(id =>
      resolveBuiltinProvider(id),
    )
  }
  return builtinProvidersCache
}

export function listBuiltinProviders(): readonly ProviderDefinition[] {
  return listAllBuiltinProviders()
}

export function isBuiltinProviderId(id: string): id is BuiltinProviderId {
  return (BUILTIN_PROVIDER_IDS as readonly string[]).includes(id)
}

export function isLegacyCustomProviderId(
  id: string | undefined | null,
): id is LegacyCustomProviderId {
  return String(id ?? '').trim() === 'custom'
}

export function isCustomProviderId(
  id: string | undefined | null,
): id is CustomProviderId {
  const s = String(id ?? '').trim()
  return s.startsWith('cp_') && s.length > 3
}

export function normalizeProviderId(
  id: string | undefined | null,
): ProviderId | undefined {
  const s = String(id ?? '').trim()
  if (!s)
    return undefined
  const aliasMap: Record<string, ProviderId> = {
    zhipu: 'zhipuai',
    kimi: 'moonshot-ai',
  }
  if (aliasMap[s])
    return aliasMap[s]
  if (isLegacyCustomProviderId(s))
    return 'custom'
  if (isCustomProviderId(s))
    return s
  return isBuiltinProviderId(s) ? s : undefined
}

export function getBuiltinProvider(
  id: string | undefined | null,
): ProviderDefinition | undefined {
  const normalized = normalizeProviderId(id)
  if (!normalized || normalized === 'custom' || isCustomProviderId(normalized))
    return undefined
  return resolveBuiltinProvider(normalized as BuiltinProviderId)
}

export function getProviderDisplayName(
  provider: Readonly<{ id: string, name?: string }>,
): string {
  const name = String(provider?.name ?? '').trim()
  return name || String(provider?.id ?? '').trim()
}

export function getModelDisplayName(
  model: ProviderModel | ProviderCustomModel,
): string {
  const display = String((model as any)?.display_name ?? '').trim()
  if (display)
    return display
  const name = String((model as any)?.name ?? '').trim()
  return name || String(model?.id ?? '').trim()
}

export function getDefaultModelId(
  provider: ProviderDefinition | undefined,
): string | undefined {
  if (!provider)
    return undefined
  const explicit = String((provider as any)?.defaultModelId ?? '').trim()
  if (explicit)
    return explicit
  const byReasoning = provider.models.find(m =>
    Boolean(m?.reasoning?.default),
  )
  return (
    String(byReasoning?.id ?? provider.models[0]?.id ?? '').trim() || undefined
  )
}

export function getProviderBaseUrl(
  provider: ProviderDefinition | undefined,
): string | undefined {
  if (!provider)
    return undefined
  const api = String(provider.api ?? '').trim()
  return api || undefined
}

export function isBuiltinProviderBaseUrlEditable(
  id: string | undefined | null,
): boolean {
  const normalized = normalizeProviderId(id)
  if (!normalized || normalized === 'custom' || isCustomProviderId(normalized))
    return false
  return BUILTIN_PROVIDER_BASE_URL_EDITABLE_IDS.has(
    normalized as BuiltinProviderId,
  )
}

export function resolveBuiltinProviderBaseUrl(
  provider: ProviderDefinition | undefined,
  candidateBaseUrl?: string | null,
): string | undefined {
  if (!provider)
    return undefined
  const builtinBaseUrl = getProviderBaseUrl(provider)
  if (!isBuiltinProviderBaseUrlEditable(provider.id))
    return builtinBaseUrl
  const candidate = String(candidateBaseUrl ?? '').trim()
  return candidate || builtinBaseUrl
}

export function getProviderAdapter(
  provider: ProviderDefinition | undefined,
): ProviderAdapter {
  if (
    provider?.adapter === 'gemini'
    || provider?.adapter === 'anthropic'
    || provider?.adapter === 'openai-responses'
  ) {
    return provider.adapter
  }
  return 'openai'
}

export function usesCcMock(provider: ProviderDefinition | undefined): boolean {
  return Boolean(provider?.ccmock)
}

function normalizeModelId(raw: string | undefined | null): string {
  return String(raw ?? '').trim()
}

function matchCatalogModel<T extends Readonly<{ id?: string }>>(
  models: readonly T[],
  rawId: string,
): T | undefined {
  const normalized = normalizeModelId(rawId)
  if (!normalized)
    return undefined
  const exact = models.find(m => normalizeModelId(m?.id) === normalized)
  if (exact)
    return exact
  const parts = normalized
    .split('/')
    .map(s => s.trim())
    .filter(Boolean)
  if (!parts.length)
    return undefined
  const tail = parts[parts.length - 1]
  if (!tail)
    return undefined
  return models.find(m => normalizeModelId(m?.id) === tail)
}

function normalizePositiveLimit(value: unknown): number | undefined {
  if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0)
    return undefined
  return Math.floor(value)
}

function isAdapterCompatible(
  candidate: ProviderAdapter,
  target: ProviderAdapter | undefined,
): boolean {
  if (!target)
    return true
  if (candidate === target)
    return true
  return (
    (candidate === 'openai' && target === 'openai-responses')
    || (candidate === 'openai-responses' && target === 'openai')
  )
}

function normalizeModelLimit(
  limit: { context?: unknown, output?: unknown } | undefined,
): { context?: number, output?: number } | undefined {
  if (!limit)
    return undefined
  const context = normalizePositiveLimit(limit.context)
  const output = normalizePositiveLimit(limit.output)
  if (context == null && output == null)
    return undefined
  return {
    ...(context != null ? { context } : {}),
    ...(output != null ? { output } : {}),
  }
}

export function getModelLimitForModels(
  models: readonly ContextWindowModelLike[] | undefined,
  modelId: string,
): { context?: number, output?: number } | undefined {
  if (!Array.isArray(models) || !models.length)
    return undefined
  const match = matchCatalogModel(models, modelId)
  return normalizeModelLimit(
    match?.limit as { context?: unknown, output?: unknown } | undefined,
  )
}

export function getModelContextWindowForModels(
  models: readonly ContextWindowModelLike[] | undefined,
  modelId: string,
): number | undefined {
  return getModelLimitForModels(models, modelId)?.context
}

export function inferBuiltinModelContextWindow(
  modelId: string,
  adapter?: ProviderAdapter,
): number | undefined {
  const normalized = normalizeModelId(modelId)
  if (!normalized)
    return undefined

  let bestMatch: number | undefined
  for (const provider of listBuiltinProviders()) {
    if (!isAdapterCompatible(getProviderAdapter(provider), adapter))
      continue
    const contextWindow = getModelContextWindowForModels(
      provider.models,
      normalized,
    )
    if (typeof contextWindow !== 'number' || contextWindow <= 0)
      continue
    if (bestMatch == null || contextWindow > bestMatch)
      bestMatch = contextWindow
  }

  return bestMatch
}

export function resolveProviderSelectionContextWindow(
  opts: Readonly<{
    providerId?: string | null
    modelId?: string | null
    connections?: Readonly<
      Record<
        string,
        Readonly<{ models?: readonly ContextWindowModelLike[] }> | undefined
      >
    > | null
    customProviders?:
      | readonly Readonly<{
        id?: string
        models?: readonly ContextWindowModelLike[]
      }>[]
      | null
    legacyCustomModels?: readonly ContextWindowModelLike[] | null
  }>,
): number | undefined {
  const providerId = normalizeProviderId(opts.providerId)
  if (!providerId)
    return undefined

  if (providerId === 'custom') {
    const legacyModels = Array.isArray(opts.legacyCustomModels)
      ? opts.legacyCustomModels
      : []
    const selectedModelId
      = normalizeModelId(opts.modelId) || normalizeModelId(legacyModels[0]?.id)
    return selectedModelId
      ? getModelContextWindowForModels(legacyModels, selectedModelId)
      : undefined
  }

  if (isCustomProviderId(providerId)) {
    const customProvider = (opts.customProviders ?? []).find(
      provider => String(provider?.id ?? '').trim() === providerId,
    )
    const customModels = Array.isArray(customProvider?.models)
      ? customProvider.models
      : []
    const selectedModelId
      = normalizeModelId(opts.modelId) || normalizeModelId(customModels[0]?.id)
    return selectedModelId
      ? getModelContextWindowForModels(customModels, selectedModelId)
      : undefined
  }

  const builtin = getBuiltinProvider(providerId)
  if (!builtin)
    return undefined
  const connectionModels = opts.connections?.[providerId]?.models
  const models
    = builtin.customModels
      && Array.isArray(connectionModels)
      && connectionModels.length
      ? connectionModels
      : builtin.models
  const selectedModelId
    = normalizeModelId(opts.modelId)
      || normalizeModelId(connectionModels?.[0]?.id)
      || normalizeModelId(getDefaultModelId(builtin))
  return selectedModelId
    ? getModelContextWindowForModels(models, selectedModelId)
    : undefined
}

export function shouldIgnoreLegacyStoredContextWindow(
  storedContextWindow: number | undefined,
  resolvedModelContextWindow: number | undefined,
): boolean {
  return (
    storedContextWindow === LEGACY_DEFAULT_CONTEXT_WINDOW
    && typeof resolvedModelContextWindow === 'number'
    && resolvedModelContextWindow > 0
    && resolvedModelContextWindow !== LEGACY_DEFAULT_CONTEXT_WINDOW
  )
}

export function getAihubmixLimitForModelId(
  modelId: string,
): { context?: number, output?: number } | undefined {
  const provider = getBuiltinProvider('aihubmix')
  if (!provider || !Array.isArray(provider.models) || !provider.models.length)
    return undefined
  return getModelLimitForModels(provider.models, modelId)
}

export function getAihubmixCapabilitiesForModelId(
  modelId: string,
):
  | { vision?: boolean, reasoning?: { supported: boolean, default?: boolean } }
  | undefined {
  const provider = getBuiltinProvider('aihubmix')
  if (!provider || !Array.isArray(provider.models) || !provider.models.length)
    return undefined
  const match = matchCatalogModel(provider.models, modelId)
  if (!match)
    return undefined
  const vision = typeof match.vision === 'boolean' ? match.vision : undefined
  const reasoning = match.reasoning?.supported
    ? { supported: true, default: match.reasoning.default }
    : undefined
  if (vision == null && reasoning == null)
    return undefined
  return {
    ...(vision != null ? { vision } : {}),
    ...(reasoning != null ? { reasoning } : {}),
  }
}

export function modelSupportsThinkingContinuation(modelId: string): boolean {
  const normalized = normalizeModelId(modelId)
  if (!normalized)
    return false
  for (const provider of listBuiltinProviders()) {
    const model = matchCatalogModel(provider.models, normalized)
    if (!model)
      continue
    const continuation = (model as any)?.extra_capabilities?.reasoning?.continuation
    if (Array.isArray(continuation) && continuation.includes('thinking_blocks'))
      return true
  }
  return false
}

export function modelPrefersReasoningSummary(modelId: string): boolean {
  const normalized = normalizeModelId(modelId)
  if (!normalized)
    return false
  for (const provider of listBuiltinProviders()) {
    const model = matchCatalogModel(provider.models, normalized)
    if (!model)
      continue
    return String(
      (model as any)?.extra_capabilities?.reasoning?.visibility ?? '',
    ).trim().toLowerCase() === 'summary'
  }
  return false
}
