const NEXT_API_OAUTH_PROVIDER_ID = 'next-api-oauth'

export type ProviderSelectionCandidate = Readonly<{
  id: string
  modelIds: readonly string[]
  selectable: boolean
}>

type ModelLike = Readonly<{ id?: string | null }> | null | undefined

type ConnectionLike
  = | Readonly<{
    apiKey?: string | null
    models?: readonly ModelLike[] | null
  }>
  | null
  | undefined

type CustomProviderLike
  = | Readonly<{
    id?: string | null
    apiKey?: string | null
    models?: readonly ModelLike[] | null
  }>
  | null
  | undefined

type LegacyCustomProviderLike
  = | Readonly<{
    apiKey?: string | null
    baseUrl?: string | null
    models?: readonly ModelLike[] | null
  }>
  | null
  | undefined

type WorkspaceSelectionLike
  = | Readonly<{
    providerId?: string | null
    modelId?: string | null
  }>
  | null
  | undefined

type BuiltinProviderLike = Readonly<{
  id?: string | null
  customModels?: boolean | null
  defaultModelId?: string | null
  models?: readonly ModelLike[] | null
}>

function normalizeNonEmpty(value: unknown): string | undefined {
  const normalized = String(value ?? '').trim()
  return normalized || undefined
}

function dedupe(values: readonly string[]): string[] {
  const next: string[] = []
  const seen = new Set<string>()
  for (const value of values) {
    const normalized = normalizeNonEmpty(value)
    if (!normalized || seen.has(normalized))
      continue
    seen.add(normalized)
    next.push(normalized)
  }
  return next
}

function modelIdsFromList(
  models: readonly ModelLike[] | null | undefined,
): string[] {
  if (!Array.isArray(models))
    return []
  return dedupe(
    models
      .map(model => normalizeNonEmpty(model?.id))
      .filter(Boolean) as string[],
  )
}

function includeRememberedModelIds(
  modelIds: readonly string[],
  rememberedModelId: string | undefined,
): string[] {
  return rememberedModelId
    ? dedupe([...modelIds, rememberedModelId])
    : [...modelIds]
}

function getDefaultModelId(
  provider: BuiltinProviderLike | undefined,
): string | undefined {
  if (!provider)
    return undefined
  const explicit = normalizeNonEmpty(provider.defaultModelId)
  if (explicit)
    return explicit
  const firstReasoningDefault = Array.isArray(provider.models)
    ? provider.models.find((model: any) => Boolean(model?.reasoning?.default))
    : undefined
  return normalizeNonEmpty(firstReasoningDefault?.id)
    ?? normalizeNonEmpty(Array.isArray(provider.models) ? provider.models[0]?.id : undefined)
}

export function buildProviderSelectionCandidates(
  input: Readonly<{
    builtinProviders?: readonly BuiltinProviderLike[] | null
    connections?: Readonly<Record<string, ConnectionLike>> | null
    customProviders?: readonly CustomProviderLike[] | null
    legacyCustom?: LegacyCustomProviderLike | null
    rememberedModels?: Readonly<Record<string, string>> | null
  }>,
): Readonly<{
  candidates: readonly ProviderSelectionCandidate[]
  existingProviderIds: ReadonlySet<string>
}> {
  const connections = input.connections ?? {}
  const candidates: ProviderSelectionCandidate[] = []
  const existingProviderIds = new Set<string>()

  for (const builtin of input.builtinProviders ?? []) {
    const providerId = String(builtin.id ?? '').trim()
    if (!providerId)
      continue
    existingProviderIds.add(providerId)

    const connection = connections[providerId]
    const connectionModels = modelIdsFromList(connection?.models)
    const builtinModels = builtin.customModels
      ? connectionModels
      : modelIdsFromList(builtin.models as readonly ModelLike[])
    const defaultModelId = getDefaultModelId(builtin)
    const rememberedModelId = normalizeNonEmpty(
      input.rememberedModels?.[providerId],
    )
    const modelIds = builtin.customModels
      ? connectionModels
      : dedupe([
          ...(builtinModels ?? []),
          ...(defaultModelId ? [defaultModelId] : []),
        ])
    const resolvedModelIds = includeRememberedModelIds(
      modelIds,
      rememberedModelId,
    )
    const hasApiKey = Boolean(normalizeNonEmpty(connection?.apiKey))
    let selectable = false
    if (providerId === NEXT_API_OAUTH_PROVIDER_ID) {
      // ACP needs the provider to stay selectable after logout so the
      // session can surface `login` again using the remembered model list.
      selectable = resolvedModelIds.length > 0
    }
    else if (builtin.customModels) {
      selectable = resolvedModelIds.length > 0
    }
    else {
      selectable = hasApiKey && resolvedModelIds.length > 0
    }

    candidates.push({
      id: providerId,
      modelIds: resolvedModelIds,
      selectable,
    })
  }

  for (const provider of input.customProviders ?? []) {
    const providerId = normalizeNonEmpty(provider?.id)
    if (!providerId)
      continue
    existingProviderIds.add(providerId)
    const modelIds = modelIdsFromList(provider?.models)
    const rememberedModelId = normalizeNonEmpty(
      input.rememberedModels?.[providerId],
    )
    const resolvedModelIds = includeRememberedModelIds(
      modelIds,
      rememberedModelId,
    )
    candidates.push({
      id: providerId,
      modelIds: resolvedModelIds,
      selectable:
        Boolean(normalizeNonEmpty(provider?.apiKey))
        && resolvedModelIds.length > 0,
    })
  }

  // Skip legacy customProvider when customProviders array already has entries
  // to avoid duplicate provider entries from the same underlying config.
  const hasNewCustomProviders = (input.customProviders ?? []).length > 0
  const legacyModelIds = modelIdsFromList(input.legacyCustom?.models)
  const rememberedLegacyModelId = normalizeNonEmpty(
    input.rememberedModels?.custom,
  )
  const resolvedLegacyModelIds = includeRememberedModelIds(
    legacyModelIds,
    rememberedLegacyModelId,
  )
  const legacyExists = !hasNewCustomProviders && Boolean(
    resolvedLegacyModelIds.length
    || normalizeNonEmpty(input.legacyCustom?.apiKey)
    || normalizeNonEmpty(input.legacyCustom?.baseUrl),
  )
  if (legacyExists) {
    existingProviderIds.add('custom')
    candidates.push({
      id: 'custom',
      modelIds: resolvedLegacyModelIds,
      selectable:
        Boolean(normalizeNonEmpty(input.legacyCustom?.apiKey))
        && resolvedLegacyModelIds.length > 0,
    })
  }

  return { candidates, existingProviderIds }
}

export function stripUnknownActiveModelSelections(
  activeModelByProvider: Readonly<Record<string, string>> | null | undefined,
  existingProviderIds: ReadonlySet<string>,
): Record<string, string> {
  const next: Record<string, string> = {}
  for (const [providerId, modelId] of Object.entries(
    activeModelByProvider ?? {},
  )) {
    if (!existingProviderIds.has(providerId))
      continue
    const normalizedModelId = normalizeNonEmpty(modelId)
    if (!normalizedModelId)
      continue
    next[providerId] = normalizedModelId
  }
  return next
}

function chooseCandidateModel(
  candidate: ProviderSelectionCandidate,
  preferredModelId: string | undefined,
  rememberedModels: Readonly<Record<string, string>> | null | undefined,
): string | undefined {
  const normalizedPreferred = normalizeNonEmpty(preferredModelId)
  if (
    normalizedPreferred
    && (candidate.modelIds.length === 0
      || candidate.modelIds.includes(normalizedPreferred))
  ) {
    return normalizedPreferred
  }

  const remembered = normalizeNonEmpty(rememberedModels?.[candidate.id])
  if (
    remembered
    && (candidate.modelIds.length === 0 || candidate.modelIds.includes(remembered))
  ) {
    return remembered
  }

  return normalizeNonEmpty(candidate.modelIds[0])
}

export function resolveProviderSelection(
  input: Readonly<{
    candidates: readonly ProviderSelectionCandidate[]
    preferredProviderId?: string | null
    preferredModelId?: string | null
    rememberedModels?: Readonly<Record<string, string>> | null
  }>,
): Readonly<{ providerId?: string, modelId?: string }> {
  const selectable = input.candidates.filter(
    candidate => candidate.selectable && candidate.modelIds.length > 0,
  )
  const preferredProviderId = normalizeNonEmpty(input.preferredProviderId)

  if (preferredProviderId) {
    // Keep the user's explicit provider pinned even if it is temporarily
    // unavailable (for example after OAuth logout or a missing API key),
    // instead of silently hopping to an unrelated fallback provider.
    const current = input.candidates.find(
      candidate => candidate.id === preferredProviderId,
    )
    if (current) {
      const modelId = chooseCandidateModel(
        current,
        normalizeNonEmpty(input.preferredModelId),
        input.rememberedModels,
      )
      if (modelId)
        return { providerId: current.id, modelId }
    }
  }

  const fallback = selectable[0]
  if (!fallback)
    return {}

  const modelId = chooseCandidateModel(
    fallback,
    undefined,
    input.rememberedModels,
  )
  if (!modelId)
    return {}

  return { providerId: fallback.id, modelId }
}

export function sanitizeWorkspaceProviderSelections(
  input: Readonly<{
    workspaceSelections?: Readonly<
      Record<string, WorkspaceSelectionLike>
    > | null
    candidates: readonly ProviderSelectionCandidate[]
    rememberedModels?: Readonly<Record<string, string>> | null
  }>,
): Record<string, { providerId: string, modelId?: string }> {
  const selectableById = new Map(
    input.candidates.map(candidate => [candidate.id, candidate] as const),
  )

  const next: Record<string, { providerId: string, modelId?: string }> = {}
  for (const [cwd, selection] of Object.entries(
    input.workspaceSelections ?? {},
  )) {
    const providerId = normalizeNonEmpty(selection?.providerId)
    if (!providerId)
      continue
    const candidate = selectableById.get(providerId)
    if (!candidate)
      continue
    const modelId = chooseCandidateModel(
      candidate,
      normalizeNonEmpty(selection?.modelId),
      input.rememberedModels,
    )
    next[cwd] = {
      providerId,
      ...(modelId ? { modelId } : {}),
    }
  }
  return next
}
