import type { ProviderCustomModel } from '#cli/compat/providers'
import type { NextApiUserInfo, OAuthTokens } from './auth'
import type { PkceCodes } from './oauth'
import { extractEmail } from './oauth'

export const NEXT_API_OAUTH_PROVIDER_ID = 'next-api-oauth'
export const NEXT_API_OAUTH_PORT = 1456
export const NEXT_API_OAUTH_ISSUER = 'https://nextapi.fazhitech.cn'
export const NEXT_API_OAUTH_CLIENT_ID = 'b1f78e8953cc42b9aec0993c37e7811e'
export const NEXT_API_OAUTH_CLIENT_SECRET
  = 'rpwBhBEnHPA14WypoqsNECFzUjcTvs9PJh1t2L2CdOhjHqtB'
export const NEXT_API_OAUTH_DEFAULT_SCOPE = 'openid profile email'

export interface NextApiOAuthConfigInput {
  issuer?: string
  clientId?: string
  clientSecret?: string
  scope: string
  relayBaseUrl?: string
  port: number
}

export interface NextApiOAuthResolvedConfig {
  issuer: string
  clientId: string
  clientSecret?: string
  scope: string
  relayBaseUrl: string
  authorizationEndpoint: string
  tokenEndpoint: string
  port: number
}

export interface NextApiOAuthErrorPayload {
  error?: string
  error_description?: string
  login_hint?: string
}

export interface NextApiOAuthTokenResponse {
  access_token: string
  refresh_token: string
  expires_in?: number
  token_type?: string
  scope?: string
  id_token?: string
}

interface PendingNextApiOAuth {
  state: string
  pkce: PkceCodes
  config: NextApiOAuthResolvedConfig
  resolve: (tokens: NextApiOAuthTokenResponse) => void
  reject: (error: Error) => void
  timeout: ReturnType<typeof setTimeout>
}

const HTML_SUCCESS = `<!doctype html>
<html>
  <head>
    <title>DimCode - Next API Authorization Successful</title>
    <style>
      body {
        font-family:
          system-ui,
          -apple-system,
          sans-serif;
        display: flex;
        justify-content: center;
        align-items: center;
        height: 100vh;
        margin: 0;
        background: #131010;
        color: #f1ecec;
      }
      .container {
        text-align: center;
        padding: 2rem;
      }
      h1 {
        color: #f1ecec;
        margin-bottom: 1rem;
      }
      p {
        color: #b7b1b1;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <h1>Authorization Successful</h1>
      <p>You can close this window and return to DimCode.</p>
    </div>
    <script>
      setTimeout(() => window.close(), 2000)
    </script>
  </body>
</html>`

function htmlError(error: string) {
  return `<!doctype html>
<html>
  <head>
    <title>DimCode - Next API Authorization Failed</title>
    <style>
      body {
        font-family:
          system-ui,
          -apple-system,
          sans-serif;
        display: flex;
        justify-content: center;
        align-items: center;
        height: 100vh;
        margin: 0;
        background: #131010;
        color: #f1ecec;
      }
      .container {
        text-align: center;
        padding: 2rem;
        max-width: 42rem;
      }
      h1 {
        color: #f1ecec;
        margin-bottom: 1rem;
      }
      p {
        color: #b7b1b1;
        white-space: pre-wrap;
        word-break: break-word;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <h1>Authorization Failed</h1>
      <p>${escapeHtml(error)}</p>
    </div>
  </body>
</html>`
}

function escapeHtml(value: string): string {
  return value
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;')
}

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

function trimTrailingSlash(raw: string): string {
  return raw.replace(/\/+$/, '')
}

function normalizeHttpUrl(raw: unknown): string | undefined {
  const value = normalizeText(raw)
  if (!value)
    return undefined
  if (!/^https?:\/\//i.test(value))
    return undefined
  return trimTrailingSlash(value)
}

async function discoverOAuthMetadata(
  issuer: string,
): Promise<
  Partial<
    Pick<NextApiOAuthResolvedConfig, 'authorizationEndpoint' | 'tokenEndpoint'>
  >
> {
  const discoveryUrl = `${issuer}/.well-known/openid-configuration`
  try {
    const response = await fetch(discoveryUrl)
    if (!response.ok)
      return {}
    const payload = (await response.json()) as {
      authorization_endpoint?: unknown
      token_endpoint?: unknown
    }
    return {
      authorizationEndpoint: normalizeHttpUrl(payload?.authorization_endpoint),
      tokenEndpoint: normalizeHttpUrl(payload?.token_endpoint),
    }
  }
  catch {
    return {}
  }
}

export function readNextApiOAuthConfigInput(
  _env: Record<string, string | undefined>,
): NextApiOAuthConfigInput {
  const issuer = normalizeHttpUrl(NEXT_API_OAUTH_ISSUER)
  const clientId = normalizeText(NEXT_API_OAUTH_CLIENT_ID)
  const clientSecret = normalizeText(NEXT_API_OAUTH_CLIENT_SECRET)
  return {
    issuer,
    clientId,
    clientSecret,
    scope: NEXT_API_OAUTH_DEFAULT_SCOPE,
    relayBaseUrl: issuer ? `${issuer}/v1` : undefined,
    port: NEXT_API_OAUTH_PORT,
  }
}

export function describeNextApiOAuthConfigError(
  env: Record<string, string | undefined>,
): string | undefined {
  const config = readNextApiOAuthConfigInput(env)
  const missing: string[] = []
  if (!config.issuer)
    missing.push('NEXT_API_OAUTH_ISSUER')
  if (!config.clientId)
    missing.push('NEXT_API_OAUTH_CLIENT_ID')
  if (!missing.length)
    return undefined
  return `Missing built-in Next API OAuth configuration: ${missing.join(', ')}.`
}

export async function resolveNextApiOAuthConfig(
  env: Record<string, string | undefined>,
): Promise<NextApiOAuthResolvedConfig> {
  const input = readNextApiOAuthConfigInput(env)
  const configError = describeNextApiOAuthConfigError(env)
  if (configError)
    throw new Error(configError)

  const discovered = await discoverOAuthMetadata(input.issuer!)
  return {
    issuer: input.issuer!,
    clientId: input.clientId!,
    clientSecret: input.clientSecret,
    scope: input.scope,
    relayBaseUrl: input.relayBaseUrl || `${input.issuer!}/v1`,
    authorizationEndpoint:
      discovered.authorizationEndpoint || `${input.issuer!}/oauth/authorize`,
    tokenEndpoint: discovered.tokenEndpoint || `${input.issuer!}/oauth/token`,
    port: input.port,
  }
}

export function buildNextApiAuthorizeUrl(
  config: NextApiOAuthResolvedConfig,
  redirectUri: string,
  pkce: PkceCodes,
  state: string,
): string {
  const params = new URLSearchParams({
    response_type: 'code',
    client_id: config.clientId,
    redirect_uri: redirectUri,
    scope: config.scope,
    code_challenge: pkce.challenge,
    code_challenge_method: 'S256',
    state,
  })
  return `${config.authorizationEndpoint}?${params.toString()}`
}

export async function resolveNextApiOAuthBrowserUrl(
  config: NextApiOAuthResolvedConfig,
  redirectUri: string,
  pkce: PkceCodes,
  state: string,
): Promise<string> {
  return buildNextApiAuthorizeUrl(config, redirectUri, pkce, state)
}

async function exchangeNextApiCodeForTokens(
  code: string,
  redirectUri: string,
  pkce: PkceCodes,
  config: NextApiOAuthResolvedConfig,
): Promise<NextApiOAuthTokenResponse> {
  const body = new URLSearchParams({
    grant_type: 'authorization_code',
    code,
    redirect_uri: redirectUri,
    client_id: config.clientId,
    code_verifier: pkce.verifier,
  })
  if (config.clientSecret)
    body.set('client_secret', config.clientSecret)

  const response = await fetch(config.tokenEndpoint, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: body.toString(),
  })
  if (!response.ok) {
    let detail = ''
    try {
      const payload = (await response.json()) as NextApiOAuthErrorPayload
      const errorCode = normalizeText(payload?.error)
      const description = normalizeText(payload?.error_description)
      if (errorCode && description)
        detail = `${errorCode}: ${description}`
      else if (description)
        detail = description
      else if (errorCode)
        detail = errorCode
    }
    catch {
      try {
        const text = normalizeText(await response.text())
        if (text)
          detail = text
      }
      catch {
        detail = ''
      }
    }

    if (detail)
      throw new Error(`Token exchange failed: ${response.status} ${detail}`)
    throw new Error(`Token exchange failed: ${response.status}`)
  }
  return response.json()
}

export function buildNextApiOAuthTokenRecord(
  config: NextApiOAuthResolvedConfig,
  tokens: NextApiOAuthTokenResponse,
): OAuthTokens {
  return {
    type: 'oauth',
    refresh: tokens.refresh_token,
    access: tokens.access_token,
    expires: Date.now() + (tokens.expires_in ?? 3600) * 1000,
    email: extractEmail({
      access_token: tokens.access_token,
      refresh_token: tokens.refresh_token,
      expires_in: tokens.expires_in,
      id_token: tokens.id_token || '',
    }),
    issuer: config.issuer,
    authorizationEndpoint: config.authorizationEndpoint,
    tokenEndpoint: config.tokenEndpoint,
    clientId: config.clientId,
    clientSecret: config.clientSecret,
    scope: tokens.scope || config.scope,
    relayBaseUrl: config.relayBaseUrl,
  }
}

export async function revokeNextApiOAuthToken(
  token: string,
  opts: Readonly<{ issuer?: string }>,
): Promise<void> {
  const rawToken = normalizeText(token)
  const issuer = normalizeHttpUrl(opts.issuer)
  if (!rawToken || !issuer)
    return

  const response = await fetch(`${issuer}/oauth/revoke`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      token: rawToken,
    }).toString(),
    signal: AbortSignal.timeout(3_000),
  })
  if (!response.ok)
    throw new Error(`Token revoke failed: ${response.status}`)
}

export async function refreshNextApiAccessToken(
  tokens: OAuthTokens,
): Promise<OAuthTokens> {
  const tokenEndpoint = normalizeHttpUrl(tokens.tokenEndpoint)
  const clientId = normalizeText(tokens.clientId)
  const refreshToken = normalizeText(tokens.refresh)
  if (!tokenEndpoint || !clientId || !refreshToken) {
    throw new Error(
      'Next API OAuth tokens are missing refresh metadata. Please run `dim auth next-api-oauth` again.',
    )
  }

  const body = new URLSearchParams({
    grant_type: 'refresh_token',
    refresh_token: refreshToken,
    client_id: clientId,
  })
  if (tokens.clientSecret)
    body.set('client_secret', tokens.clientSecret)

  const response = await fetch(tokenEndpoint, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: body.toString(),
  })
  if (!response.ok)
    throw new Error(`Token refresh failed: ${response.status}`)

  const payload = (await response.json()) as NextApiOAuthTokenResponse
  if (!payload.access_token)
    throw new Error('Token refresh failed: missing access_token')

  return {
    ...tokens,
    access: payload.access_token,
    refresh: payload.refresh_token || tokens.refresh,
    expires: Date.now() + (payload.expires_in ?? 3600) * 1000,
    email:
      extractEmail({
        access_token: payload.access_token,
        refresh_token: payload.refresh_token || tokens.refresh,
        expires_in: payload.expires_in,
        id_token: payload.id_token || '',
      }) || tokens.email,
    scope: payload.scope || tokens.scope,
  }
}

export async function fetchNextApiOAuthUserInfo(
  accessToken: string,
  issuer: string,
): Promise<NextApiUserInfo> {
  const response = await fetch(`${issuer}/oauth/userinfo`, {
    headers: {
      Authorization: `Bearer ${accessToken}`,
    },
  })
  if (!response.ok)
    throw new Error(`UserInfo fetch failed: ${response.status}`)

  const payload = (await response.json()) as {
    sub?: string
    email?: string
    name?: string
    preferred_username?: string
  }
  if (!payload?.sub)
    throw new Error('UserInfo response missing sub field')

  return {
    sub: payload.sub,
    email: payload.email,
    name: payload.name,
    preferred_username: payload.preferred_username,
    fetched_at: Date.now(),
  }
}

export async function fetchNextApiOAuthModels(
  accessToken: string,
  relayBaseUrl: string,
): Promise<ProviderCustomModel[]> {
  const baseUrl = normalizeHttpUrl(relayBaseUrl)
  if (!baseUrl)
    return []

  const response = await fetch(`${baseUrl}/models`, {
    headers: {
      Authorization: `Bearer ${accessToken}`,
    },
  })
  if (!response.ok)
    throw new Error(`Model discovery failed: ${response.status}`)

  const payload = (await response.json()) as { data?: unknown }
  const items = Array.isArray(payload?.data) ? payload.data : []
  return items
    .map((item): ProviderCustomModel | null => {
      const raw = item as Record<string, any>
      const id = normalizeText(raw?.id)
      if (!id)
        return null
      const inputModalities = Array.isArray(raw?.modalities?.input)
        ? raw.modalities.input.map((value: unknown) =>
            String(value ?? '').toLowerCase(),
          )
        : []
      return {
        id,
        name: normalizeText(raw?.display_name ?? raw?.name ?? raw?.id),
        vision:
          typeof raw?.vision === 'boolean'
            ? raw.vision
            : inputModalities.includes('image'),
        reasoning:
          raw?.reasoning?.supported === true
            ? { supported: true, default: raw.reasoning.default === true }
            : undefined,
      }
    })
    .filter((item): item is ProviderCustomModel => Boolean(item))
}

let nextApiOAuthServer: any
let pendingNextApiOAuth: PendingNextApiOAuth | undefined
let activeNextApiOAuthPort = NEXT_API_OAUTH_PORT

export async function startNextApiOAuthServer(
  port = NEXT_API_OAUTH_PORT,
): Promise<{ port: number, redirectUri: string }> {
  if (nextApiOAuthServer) {
    return {
      port: activeNextApiOAuthPort,
      redirectUri: `http://localhost:${activeNextApiOAuthPort}/auth/callback`,
    }
  }

  activeNextApiOAuthPort = port

  if (typeof Bun !== 'undefined') {
    try {
      nextApiOAuthServer = Bun.serve({
        port: activeNextApiOAuthPort,
        fetch(request: Request) {
          return handleNextApiOAuthRequest(new URL(request.url))
        },
      })
    }
    catch (error: any) {
      if (String(error?.message || '').includes('address already in use')) {
        throw new Error(
          `Port ${activeNextApiOAuthPort} is already in use. Please stop the process using this port and try again.`,
        )
      }
      throw error
    }
    return {
      port: activeNextApiOAuthPort,
      redirectUri: `http://localhost:${activeNextApiOAuthPort}/auth/callback`,
    }
  }

  const http = await import('node:http')
  nextApiOAuthServer = http.createServer(async (req, res) => {
    try {
      const url = new URL(
        req.url || '/',
        `http://localhost:${activeNextApiOAuthPort}`,
      )
      const webResponse = handleNextApiOAuthRequest(url)
      const buffer = Buffer.from(await webResponse.arrayBuffer())
      res.statusCode = webResponse.status
      webResponse.headers.forEach((value, key) => res.setHeader(key, value))
      res.end(buffer)
    }
    catch (error: any) {
      res.statusCode = 500
      res.end(error?.message || 'Internal Server Error')
    }
  })

  await new Promise<void>((resolve, reject) => {
    nextApiOAuthServer.listen(activeNextApiOAuthPort, () => resolve())
    nextApiOAuthServer.on('error', (error: any) => {
      if (error?.code === 'EADDRINUSE') {
        reject(
          new Error(
            `Port ${activeNextApiOAuthPort} is already in use. Please stop the process using this port and try again.`,
          ),
        )
        return
      }
      reject(error)
    })
  })

  return {
    port: activeNextApiOAuthPort,
    redirectUri: `http://localhost:${activeNextApiOAuthPort}/auth/callback`,
  }
}

export function handleNextApiOAuthRequest(url: URL): Response {
  if (url.pathname === '/auth/callback') {
    const code = url.searchParams.get('code') || ''
    const state = url.searchParams.get('state') || ''
    const error = url.searchParams.get('error') || ''
    const errorDescription = url.searchParams.get('error_description') || ''

    if (error) {
      const errorMessage = errorDescription
        ? `${error}: ${errorDescription}`
        : error
      pendingNextApiOAuth?.reject(new Error(errorMessage))
      pendingNextApiOAuth = undefined
      return new Response(htmlError(errorMessage), {
        status: 400,
        headers: { 'Content-Type': 'text/html; charset=utf-8' },
      })
    }

    if (!code) {
      const errorMessage = 'Missing authorization code'
      pendingNextApiOAuth?.reject(new Error(errorMessage))
      pendingNextApiOAuth = undefined
      return new Response(htmlError(errorMessage), {
        status: 400,
        headers: { 'Content-Type': 'text/html; charset=utf-8' },
      })
    }

    if (!pendingNextApiOAuth || state !== pendingNextApiOAuth.state) {
      const errorMessage = 'Invalid OAuth state - possible CSRF attempt'
      pendingNextApiOAuth?.reject(new Error(errorMessage))
      pendingNextApiOAuth = undefined
      return new Response(htmlError(errorMessage), {
        status: 400,
        headers: { 'Content-Type': 'text/html; charset=utf-8' },
      })
    }

    const current = pendingNextApiOAuth
    pendingNextApiOAuth = undefined
    clearTimeout(current.timeout)

    exchangeNextApiCodeForTokens(
      code,
      `http://localhost:${activeNextApiOAuthPort}/auth/callback`,
      current.pkce,
      current.config,
    )
      .then(current.resolve)
      .catch(current.reject)

    return new Response(HTML_SUCCESS, {
      status: 200,
      headers: { 'Content-Type': 'text/html; charset=utf-8' },
    })
  }

  if (url.pathname === '/auth/cancel') {
    pendingNextApiOAuth?.reject(new Error('Login cancelled'))
    pendingNextApiOAuth = undefined
    return new Response('Cancelled', { status: 200 })
  }

  return new Response('Not Found', { status: 404 })
}

export function stopNextApiOAuthServer(): void {
  if (nextApiOAuthServer) {
    if (typeof nextApiOAuthServer.stop === 'function') {
      nextApiOAuthServer.stop()
    }
    else if (typeof nextApiOAuthServer.close === 'function') {
      nextApiOAuthServer.close()
    }
    nextApiOAuthServer = undefined
  }
}

export function waitForNextApiOAuthCallback(
  pkce: PkceCodes,
  state: string,
  config: NextApiOAuthResolvedConfig,
): Promise<NextApiOAuthTokenResponse> {
  return new Promise((resolve, reject) => {
    const timeout = setTimeout(
      () => {
        if (pendingNextApiOAuth) {
          pendingNextApiOAuth = undefined
          reject(
            new Error('OAuth callback timeout - authorization took too long'),
          )
        }
      },
      5 * 60 * 1000,
    )

    pendingNextApiOAuth = {
      state,
      pkce,
      config,
      resolve,
      reject,
      timeout,
    }
  })
}
