import type { ProviderCustomModel } from '#cli/compat/providers'
import type { DimSdkStore } from '#cli/core/store'
import type { Ref } from 'vue'
import {
  getOAuthTokens,
  loadUserInfo,
  OAUTH_DUMMY_KEY,
  removeOAuthTokens,
  removeUserInfo,
  saveUserInfo,
  setOAuthTokens,
} from '#cli/core/auth'
import {
  buildNextApiOAuthTokenRecord,
  describeNextApiOAuthConfigError,
  fetchNextApiOAuthModels,
  fetchNextApiOAuthUserInfo,
  NEXT_API_OAUTH_ISSUER,
  NEXT_API_OAUTH_PROVIDER_ID,
  resolveNextApiOAuthBrowserUrl,
  resolveNextApiOAuthConfig,
  revokeNextApiOAuthToken,
  startNextApiOAuthServer,
  stopNextApiOAuthServer,
  waitForNextApiOAuthCallback,
} from '#cli/core/next-api-oauth'
import {
  generatePKCE,
  generateState,
} from '#cli/core/oauth'
import { syncOAuthRuntimeState } from '#cli/core/oauth-runtime-sync'
import { t } from '#cli/i18n'
import { ref, watch } from 'vue'
import { loadDimcodeUserConfig } from '../../cli/user-cache'
import {
  applyOAuthModelQuirks,
  getRuntimeEnv,
  normalizeUrl,
  openOAuthBrowserUrl,
  resolveActiveOpenAiBaseUrl,
} from './oauth-dialog-runtime'

import type { NextApiUserInfo } from '#cli/core/auth'

type DialogModel = Readonly<{ id: string; name: string; description: string }>
type PendingNextApiOAuth = Readonly<{
  pkce: Awaited<ReturnType<typeof generatePKCE>>
  state: string
  config: Awaited<ReturnType<typeof resolveNextApiOAuthConfig>>
}>
type OAuthDialogStore = Pick<DimSdkStore, 'state' | 'setEnvVar'>
type RuntimeGlobals = typeof globalThis & {
  __dimsdk_next_api_oauth_pending?: PendingNextApiOAuth
}

function runtimeGlobals(): RuntimeGlobals {
  return globalThis as RuntimeGlobals
}

function normalizeErrorMessage(
  error: unknown,
  fallback = t('common.unknownError'),
): string {
  if (error instanceof Error) {
    const message = error.message.trim()
    if (message)
      return message
  }

  const message = String(error ?? '').trim()
  return message || fallback
}

export function useNextApiOAuthDialog(
  opts: Readonly<{
    showToast: (text: string) => void
    store?: OAuthDialogStore
    nodeLike?: boolean
  }>,
): Readonly<{
  showNextApiOAuth: Ref<boolean>
  nextApiOAuthStatus: Ref<'loading' | 'authenticated' | 'unauthenticated'>
  nextApiOAuthEmail: Ref<string>
  nextApiOAuthUrl: Ref<string>
  nextApiOAuthError: Ref<string>
  selectedNextApiModel: Ref<string>
  nextApiModels: Ref<DialogModel[]>
  nextApiModelIndex: Ref<number>
  nextApiOAuthUserInfo: Ref<NextApiUserInfo | undefined>
  openNextApiOAuth: () => void
  closeNextApiOAuth: () => void
  loginWithNextApiOAuth: () => Promise<void>
  logoutNextApiOAuth: () => Promise<void>
  selectNextApiModel: (modelId: string) => Promise<void>
  viewNextApiPlans: () => Promise<void>
}> {
  const showNextApiOAuth = ref(false)
  const nextApiOAuthStatus = ref<
    'loading' | 'authenticated' | 'unauthenticated'
  >('unauthenticated')
  const nextApiOAuthEmail = ref('')
  const nextApiOAuthUrl = ref('')
  const nextApiOAuthError = ref('')
  const selectedNextApiModel = ref('')
  const nextApiModels = ref<DialogModel[]>([])
  const nextApiModelIndex = ref(0)
  const nextApiOAuthUserInfo = ref<NextApiUserInfo | undefined>(undefined)
  const suppressNextLoginError = ref(false)

  function getPendingState(): PendingNextApiOAuth | undefined {
    return runtimeGlobals().__dimsdk_next_api_oauth_pending
  }

  function setPendingState(pending: PendingNextApiOAuth | undefined): void {
    if (pending) {
      runtimeGlobals().__dimsdk_next_api_oauth_pending = pending
      return
    }
    delete runtimeGlobals().__dimsdk_next_api_oauth_pending
  }

  function toDialogModels(
    models: readonly ProviderCustomModel[],
  ): DialogModel[] {
    return models
      .map((model) => {
        const id = String(model?.id ?? '').trim()
        if (!id) return null
        const name = String(model?.name ?? '').trim() || id
        return {
          id,
          name,
          description: name !== id ? id : '',
        } satisfies DialogModel
      })
      .filter((model): model is DialogModel => Boolean(model))
  }

  function syncModelIndex(modelId: string): void {
    const index = nextApiModels.value.findIndex((model) => model.id === modelId)
    nextApiModelIndex.value = index >= 0 ? index : 0
  }

  function resolvePreferredModelId(
    models: readonly ProviderCustomModel[],
    preferred?: string,
  ): string | undefined {
    const normalized = String(preferred ?? '').trim()
    if (normalized) {
      if (
        !models.length ||
        models.some((model) => String(model.id ?? '').trim() === normalized)
      )
        return normalized
    }

    const first = String(models[0]?.id ?? '').trim()
    return first || undefined
  }

  async function loadConfiguredState(
    env: Record<string, string | undefined>,
  ): Promise<{
    models: ProviderCustomModel[]
    baseUrl: string
    modelId: string
  }> {
    const { config } = await loadDimcodeUserConfig(env)
    const settings = config.settings
    const connection
      = settings?.providerConnections?.[NEXT_API_OAUTH_PROVIDER_ID]
    const models = (connection?.models ?? [])
      .map((model) => {
        const id = String(model.id ?? '').trim()
        if (!id) return null
        return {
          id,
          ...(String(model.name ?? '').trim()
            ? { name: String(model.name).trim() }
            : {}),
          ...(typeof model.vision === 'boolean'
            ? { vision: model.vision }
            : {}),
          ...(model.reasoning ? { reasoning: model.reasoning } : {}),
          ...(model.limit ? { limit: model.limit } : {}),
        } satisfies ProviderCustomModel
      })
      .filter(
        (model): model is ProviderCustomModel =>
          Boolean(model),
      )

    const preferred = String(
      settings?.activeModelByProvider?.[NEXT_API_OAUTH_PROVIDER_ID] ?? '',
    ).trim()
    const modelId = preferred || String(models[0]?.id ?? '').trim()
    const baseUrl = normalizeUrl(connection.baseUrl)
    return { models, baseUrl, modelId }
  }

  async function shouldClearNextApiPrimaryState(
    env: Record<string, string | undefined>,
    tokens?: { relayBaseUrl?: string },
  ): Promise<boolean> {
    const activeProviderId = String(
      opts.store?.state.provider?.activeId ??
        env.DIMCODE_PROVIDER_ID ??
        '',
    ).trim()
    if (activeProviderId === NEXT_API_OAUTH_PROVIDER_ID) return true

    const activeBaseUrl = resolveActiveOpenAiBaseUrl(opts.store, env)
    if (!activeBaseUrl) return false

    const knownBaseUrls = new Set<string>()
    const tokenBaseUrl = normalizeUrl(tokens?.relayBaseUrl)
    if (tokenBaseUrl) knownBaseUrls.add(tokenBaseUrl)

    try {
      const configured = await loadConfiguredState(env)
      if (configured.baseUrl) knownBaseUrls.add(configured.baseUrl)
    } catch {
      // Ignore config lookup failures during logout heuristics.
    }

    return knownBaseUrls.has(activeBaseUrl)
  }

  async function loadOAuthStatus(): Promise<void> {
    try {
      const env = getRuntimeEnv()
      const tokens = await getOAuthTokens('next-api-oauth')
      if (!tokens) {
        nextApiOAuthStatus.value = 'unauthenticated'
        nextApiOAuthEmail.value = ''
        nextApiOAuthError.value = describeNextApiOAuthConfigError(env) || ''
        selectedNextApiModel.value = ''
        nextApiModels.value = []
        nextApiModelIndex.value = 0
        nextApiOAuthUserInfo.value = undefined
        return
      }

      const configured = await loadConfiguredState(env)
      nextApiOAuthStatus.value = 'authenticated'
      nextApiOAuthEmail.value =
        tokens.email || t('nextApiOAuthDialog.defaultAccount')
      nextApiOAuthError.value = ''
      nextApiModels.value = toDialogModels(configured.models)
      selectedNextApiModel.value = configured.modelId
      syncModelIndex(configured.modelId)

      // Refresh user info from server and update local cache
      try {
        const userInfo = await fetchNextApiOAuthUserInfo(
          tokens.access,
          tokens.issuer || NEXT_API_OAUTH_ISSUER,
        )
        await saveUserInfo(userInfo)
        nextApiOAuthUserInfo.value = userInfo
        // Update email display if server returned a more complete value
        if (userInfo.email)
          nextApiOAuthEmail.value = userInfo.email
      } catch {
        // Fall back to cached user info
        const cached = await loadUserInfo()
        nextApiOAuthUserInfo.value = cached
      }
    } catch {
      nextApiOAuthStatus.value = 'unauthenticated'
      nextApiOAuthEmail.value = ''
      nextApiOAuthError.value =
        describeNextApiOAuthConfigError(getRuntimeEnv()) || ''
      selectedNextApiModel.value = ''
      nextApiModels.value = []
      nextApiModelIndex.value = 0
      nextApiOAuthUserInfo.value = undefined
    }
  }

  async function syncNextApiOAuthState(
    optsIn: Readonly<{
      modelId?: string
      models: readonly ProviderCustomModel[]
      baseUrl: string
      apiKey: string
      invalidateSession?: boolean
    }>,
  ): Promise<void> {
    const modelId = String(optsIn.modelId ?? '').trim()
    const baseUrl = normalizeUrl(optsIn.baseUrl)
    const apiKey = String(optsIn.apiKey ?? '').trim() || OAUTH_DUMMY_KEY

    await syncOAuthRuntimeState({
      env: getRuntimeEnv(),
      store: opts.store,
      providerId: NEXT_API_OAUTH_PROVIDER_ID,
      adapter: 'openai',
      ...(modelId ? { modelId } : { clearModel: true }),
      ...(baseUrl ? { baseUrl } : { clearBaseUrl: true }),
      apiKey,
      authMethod: null,
      providerConnection: {
        providerId: NEXT_API_OAUTH_PROVIDER_ID,
        baseUrl: baseUrl || null,
        apiKey,
        models: optsIn.models,
      },
      activeModel: modelId
        ? { providerId: NEXT_API_OAUTH_PROVIDER_ID, modelId }
        : { providerId: NEXT_API_OAUTH_PROVIDER_ID, clear: true },
      workspaceSelection: {
        providerId: NEXT_API_OAUTH_PROVIDER_ID,
        ...(modelId ? { modelId } : {}),
      },
      invalidateSession: optsIn.invalidateSession ?? true,
    })

    if (modelId) {
      applyOAuthModelQuirks({
        modelId,
        store: opts.store,
        nodeLike: opts.nodeLike,
      })
    }
  }

  async function generateOAuthUrl(): Promise<void> {
    if (nextApiOAuthUrl.value || nextApiOAuthStatus.value === 'authenticated')
      return

    try {
      const env = getRuntimeEnv()
      const configError = describeNextApiOAuthConfigError(env)
      if (configError) {
        nextApiOAuthError.value = configError
        return
      }

      opts.showToast(t('nextApiOAuthDialog.toast.generatingUrl'))
      const config = await resolveNextApiOAuthConfig(env)
      const { redirectUri } = await startNextApiOAuthServer(config.port)
      const pkce = await generatePKCE()
      const state = generateState()
      nextApiOAuthUrl.value = await resolveNextApiOAuthBrowserUrl(
        config,
        redirectUri,
        pkce,
        state,
      )
      nextApiOAuthError.value = ''
      setPendingState({ pkce, state, config })
      opts.showToast(t('nextApiOAuthDialog.toast.urlReady'))
    } catch (error: unknown) {
      nextApiOAuthError.value = normalizeErrorMessage(error, '')
      opts.showToast(
        t('nextApiOAuthDialog.toast.generateUrlFailed', {
          error: nextApiOAuthError.value,
        }),
      )
    }
  }

  async function cancelPendingOAuth(): Promise<void> {
    const pending = getPendingState()
    setPendingState(undefined)
    nextApiOAuthUrl.value = ''

    try {
      if (pending?.config?.port) {
        await fetch(`http://localhost:${pending.config.port}/auth/cancel`, {
          method: 'GET',
        })
      }
    } catch {
      // Ignore local cancel failures.
    }

    try {
      stopNextApiOAuthServer()
    } catch {
      // Ignore local server stop failures.
    }
  }

  async function loginWithNextApiOAuth(): Promise<void> {
    if (nextApiOAuthStatus.value === 'loading') return

    try {
      nextApiOAuthStatus.value = 'loading'
      nextApiOAuthError.value = ''

      if (!nextApiOAuthUrl.value) await generateOAuthUrl()
      if (!nextApiOAuthUrl.value)
        throw new Error(
          nextApiOAuthError.value || 'Failed to generate OAuth URL',
        )

      const pending = getPendingState()
      if (!pending?.pkce || !pending?.state || !pending?.config)
        throw new Error('OAuth session not initialized')

      opts.showToast(t('nextApiOAuthDialog.toast.startingLogin'))
      const opened = await openOAuthBrowserUrl(
        nextApiOAuthUrl.value,
        opts.nodeLike,
      )
      if (opened) opts.showToast(t('nextApiOAuthDialog.toast.browserOpened'))
      else opts.showToast(t('nextApiOAuthDialog.toast.copyUrlToLogin'))

      const tokenResponse = await waitForNextApiOAuthCallback(
        pending.pkce,
        pending.state,
        pending.config,
      )
      if (!tokenResponse.access_token || !tokenResponse.refresh_token)
        throw new Error('OAuth token response is missing required fields')

      stopNextApiOAuthServer()
      nextApiOAuthUrl.value = ''
      setPendingState(undefined)

      const env = getRuntimeEnv()
      const storedTokens = buildNextApiOAuthTokenRecord(
        pending.config,
        tokenResponse,
      )
      await setOAuthTokens('next-api-oauth', storedTokens)

      let models: ProviderCustomModel[] = []
      try {
        models = await fetchNextApiOAuthModels(
          storedTokens.access,
          pending.config.relayBaseUrl,
        )
      } catch {
        // Model fetch is best effort; login should still succeed.
      }

      const configured = await loadConfiguredState(env)
      const modelId = resolvePreferredModelId(models, configured.modelId)
      await syncNextApiOAuthState({
        modelId,
        models,
        baseUrl: pending.config.relayBaseUrl,
        apiKey: storedTokens.access || OAUTH_DUMMY_KEY,
        invalidateSession: true,
      })

      nextApiOAuthStatus.value = 'authenticated'
      nextApiOAuthEmail.value =
        storedTokens.email || t('nextApiOAuthDialog.defaultAccount')
      nextApiOAuthError.value = ''
      nextApiModels.value = toDialogModels(models)
      selectedNextApiModel.value = modelId || ''
      syncModelIndex(selectedNextApiModel.value)

      // Fetch and store user info after successful login
      try {
        const userInfo = await fetchNextApiOAuthUserInfo(
          storedTokens.access,
          pending.config.issuer,
        )
        await saveUserInfo(userInfo)
        nextApiOAuthUserInfo.value = userInfo
        if (userInfo.email)
          nextApiOAuthEmail.value = userInfo.email
      } catch {
        // User info fetch is best effort; login should still succeed.
        nextApiOAuthUserInfo.value = undefined
      }

      opts.showToast(t('nextApiOAuthDialog.toast.authSuccess'))
      suppressNextLoginError.value = false
    } catch (error: unknown) {
      nextApiOAuthStatus.value = 'unauthenticated'
      nextApiOAuthEmail.value = ''
      nextApiOAuthError.value = normalizeErrorMessage(error)
      if (!suppressNextLoginError.value) {
        opts.showToast(
          t('nextApiOAuthDialog.toast.loginFailed', {
            error: normalizeErrorMessage(error),
          }),
        )
      }
      suppressNextLoginError.value = false

      try {
        await cancelPendingOAuth()
      } catch {
        // Ignore cleanup failures after login errors.
      }
    }
  }

  async function logoutNextApiOAuth(): Promise<void> {
    try {
      const env = getRuntimeEnv()
      const existingTokens = await getOAuthTokens('next-api-oauth')
      let remoteRevokeError = ''

      try {
        await revokeNextApiOAuthToken(
          existingTokens?.refresh || existingTokens?.access || '',
          { issuer: existingTokens?.issuer },
        )
      } catch (error: unknown) {
        remoteRevokeError = normalizeErrorMessage(error, '')
      }

      await removeOAuthTokens('next-api-oauth')
      await removeUserInfo()
      nextApiOAuthUserInfo.value = undefined
      const shouldClearPrimaryState = await shouldClearNextApiPrimaryState(
        env,
        existingTokens,
      )

      await syncOAuthRuntimeState({
        env,
        store: opts.store,
        providerConnection: {
          providerId: NEXT_API_OAUTH_PROVIDER_ID,
          clear: true,
        },
        activeModel: { providerId: NEXT_API_OAUTH_PROVIDER_ID, clear: true },
        authMethod: null,
        clearProviderId: shouldClearPrimaryState,
        clearModel: shouldClearPrimaryState,
        clearBaseUrl: shouldClearPrimaryState,
        clearApiKey: shouldClearPrimaryState,
        workspaceSelection: shouldClearPrimaryState
          ? { clear: true }
          : undefined,
        clearWorkspaceSelectionsByProvider: NEXT_API_OAUTH_PROVIDER_ID,
        invalidateSession: shouldClearPrimaryState,
      })

      nextApiOAuthStatus.value = 'unauthenticated'
      nextApiOAuthEmail.value = ''
      nextApiOAuthUrl.value = ''
      nextApiOAuthError.value = describeNextApiOAuthConfigError(env) || ''
      selectedNextApiModel.value = ''
      nextApiModels.value = []
      nextApiModelIndex.value = 0

      if (remoteRevokeError) {
        opts.showToast(
          t('nextApiOAuthDialog.toast.loggedOutWithRemoteWarning', {
            error: remoteRevokeError,
          }),
        )
      } else {
        opts.showToast(t('nextApiOAuthDialog.toast.loggedOut'))
      }

      closeNextApiOAuth()
    } catch (error: unknown) {
      opts.showToast(
        t('nextApiOAuthDialog.toast.logoutFailed', {
          error: normalizeErrorMessage(error),
        }),
      )
    }
  }

  function openNextApiOAuth(): void {
    showNextApiOAuth.value = true
  }

  function closeNextApiOAuth(): void {
    showNextApiOAuth.value = false
    if (nextApiOAuthStatus.value === 'authenticated' || !nextApiOAuthUrl.value)
      return
    suppressNextLoginError.value = true
    void cancelPendingOAuth()
  }

  watch(showNextApiOAuth, async (isOpen) => {
    if (!isOpen) return

    try {
      await loadOAuthStatus()
      if (
        nextApiOAuthStatus.value === 'unauthenticated' &&
        !nextApiOAuthError.value
      )
        await generateOAuthUrl()
    } catch (error: unknown) {
      opts.showToast(
        t('nextApiOAuthDialog.toast.initFailed', {
          error: normalizeErrorMessage(error),
        }),
      )
    }
  })

  async function selectNextApiModel(modelId: string): Promise<void> {
    try {
      const nextModelId = String(modelId ?? '').trim()
      if (!nextModelId) return

      const env = getRuntimeEnv()
      const tokens = await getOAuthTokens('next-api-oauth')
      const configured = await loadConfiguredState(env)
      const apiKey =
        String(tokens?.access ?? OAUTH_DUMMY_KEY).trim() || OAUTH_DUMMY_KEY
      const baseUrl = configured.baseUrl || normalizeUrl(tokens?.relayBaseUrl)

      await syncNextApiOAuthState({
        modelId: nextModelId,
        models: configured.models,
        baseUrl,
        apiKey,
        invalidateSession: true,
      })

      selectedNextApiModel.value = nextModelId
      syncModelIndex(nextModelId)
      opts.showToast(
        t('nextApiOAuthDialog.toast.modelSwitched', { model: nextModelId }),
      )
    } catch (error: unknown) {
      opts.showToast(
        t('nextApiOAuthDialog.toast.selectModelFailed', {
          error: normalizeErrorMessage(error),
        }),
      )
    }
  }

  async function viewNextApiPlans(): Promise<void> {
    await openOAuthBrowserUrl(NEXT_API_OAUTH_ISSUER, opts.nodeLike)
  }

  return {
    showNextApiOAuth,
    nextApiOAuthStatus,
    nextApiOAuthEmail,
    nextApiOAuthUrl,
    nextApiOAuthError,
    selectedNextApiModel,
    nextApiModels,
    nextApiModelIndex,
    nextApiOAuthUserInfo,
    openNextApiOAuth,
    closeNextApiOAuth,
    loginWithNextApiOAuth,
    logoutNextApiOAuth,
    selectNextApiModel,
    viewNextApiPlans,
  }
}
