import { getProviderConfig } from '../provider-registry'
import type { OAuthToken } from '../types'
import { getOAuthCredentials } from './credentials'

/**
 * In-flight refresh deduplication map.
 * Keyed by `provider:refreshToken` — concurrent callers with the same expired token
 * share a single refresh request instead of racing against each other.
 */
const inflightRefreshes = new Map<string, Promise<OAuthToken>>()

/** @internal Exposed for testing — do not use in production code. */
export function _getInflightRefreshCount(): number {
  return inflightRefreshes.size
}

/**
 * Refresh OAuth token if expired (pure function)
 *
 * Checks if token expires within 5 minutes and refreshes if needed.
 * Automatically determines provider config from token.provider field.
 * Respects provider-specific token exchange methods (basic-auth, form-encoded, json-body).
 * Handles both RFC 6749-compliant (snake_case) and non-compliant (camelCase) providers.
 */
export async function refreshOAuthTokenIfExpired(token: OAuthToken): Promise<OAuthToken> {
  // Validate token has provider field
  if (!token.provider) {
    throw new Error('Token missing provider field - cannot refresh')
  }

  // Validate refresh token exists
  if (!token.refreshToken) {
    throw new Error('Token cannot be refreshed (no refresh_token). User must re-authorize.')
  }

  // Check if token is still valid
  const expiresAt = new Date(token.expiresAt).getTime()
  const now = Date.now()
  const bufferMs = 5 * 60 * 1000 // 5 minutes

  if (expiresAt > now + bufferMs) {
    console.log('[OAuth] Token still valid, no refresh needed', {
      provider: token.provider,
      expiresAt: token.expiresAt,
      remainingMinutes: Math.floor((expiresAt - now) / 60000)
    })
    return token // Still valid
  }

  // Token expired, delegate to force refresh
  return forceRefreshOAuthToken(token)
}

/**
 * Force refresh OAuth token (bypasses expiry check)
 *
 * Use this when you receive a 401 from the API even though the token
 * appeared valid based on expiresAt. This handles cases where:
 * - The stored expiresAt is stale/incorrect
 * - The token was revoked before expiry
 * - Clock skew between systems
 */
export async function forceRefreshOAuthToken(token: OAuthToken): Promise<OAuthToken> {
  // Validate token has provider field
  if (!token.provider) {
    throw new Error('Token missing provider field - cannot refresh')
  }

  // Validate refresh token exists
  if (!token.refreshToken) {
    throw new Error('Token cannot be refreshed (no refresh_token). User must re-authorize.')
  }

  // Deduplicate concurrent refreshes for the same token
  const dedupeKey = `${token.provider}:${token.refreshToken}`
  const inflight = inflightRefreshes.get(dedupeKey)
  if (inflight) {
    console.log('[OAuth] Reusing in-flight refresh', { provider: token.provider })
    return inflight
  }

  const refreshPromise = performTokenRefresh(token)
  inflightRefreshes.set(dedupeKey, refreshPromise)

  try {
    return await refreshPromise
  } finally {
    inflightRefreshes.delete(dedupeKey)
  }
}

async function performTokenRefresh(token: OAuthToken): Promise<OAuthToken> {
  console.log('[OAuth] Force refreshing token', {
    provider: token.provider,
    expiresAt: token.expiresAt
  })

  // Get provider configuration
  const config = getProviderConfig(token.provider)

  // Get client credentials from environment
  const { clientId, clientSecret } = getOAuthCredentials(config.id)

  // Perform token refresh using provider-specific exchange method
  let response: Response

  switch (config.tokenExchange) {
    case 'basic-auth': {
      // Notion pattern: Authorization: Basic {base64(clientId:clientSecret)}
      const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString('base64')
      response = await fetch(config.tokenUrl, {
        method: 'POST',
        headers: {
          Authorization: `Basic ${credentials}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          grant_type: 'refresh_token',
          refresh_token: token.refreshToken
        })
      })
      break
    }

    case 'form-encoded': {
      // Google pattern: application/x-www-form-urlencoded body
      const params = new URLSearchParams({
        grant_type: 'refresh_token',
        refresh_token: token.refreshToken,
        client_id: clientId,
        client_secret: clientSecret
      })
      response = await fetch(config.tokenUrl, {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        body: params.toString()
      })
      break
    }

    case 'json-body': {
      // Most providers: application/json body
      response = await fetch(config.tokenUrl, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          grant_type: 'refresh_token',
          refresh_token: token.refreshToken,
          client_id: clientId,
          client_secret: clientSecret
        })
      })
      break
    }

    default: {
      throw new Error(`Unsupported token exchange method: ${config.tokenExchange}`)
    }
  }

  // Handle error response
  if (!response.ok) {
    let errorMessage = `Failed to refresh OAuth token for ${config.id}: ${response.status}`

    try {
      const errorData = (await response.json()) as Record<string, unknown>
      // Parse OAuth error response (RFC 6749 Section 5.2)
      if (errorData.error) {
        errorMessage += ` - ${errorData.error}`
        if (errorData.error_description) {
          errorMessage += `: ${errorData.error_description}`
        }
        // Identify invalid_grant errors (user must re-authorize)
        if (errorData.error === 'invalid_grant') {
          errorMessage += ' (User must re-authorize)'
        }
      }
    } catch {
      // If error response is not JSON, use status text
      errorMessage += ` - ${response.statusText}`
    }

    throw new Error(errorMessage)
  }

  const data = (await response.json()) as Record<string, unknown>

  // Defensive normalization: handle both snake_case (RFC 6749) and camelCase
  const accessToken = (data.access_token ?? data.accessToken) as string | undefined
  const refreshToken = (data.refresh_token ?? data.refreshToken) as string | undefined
  const expiresIn = (data.expires_in ?? data.expiresIn ?? 3600) as number

  // Validate required fields
  if (!accessToken) {
    throw new Error(
      `Invalid token refresh response: missing access_token. Received keys: ${Object.keys(data).join(', ')}`
    )
  }

  console.log('[OAuth] Token refreshed successfully', {
    provider: token.provider,
    hasNewRefreshToken: !!refreshToken,
    expiresInSeconds: expiresIn,
    validForHours: Math.floor(expiresIn / 3600)
  })

  // Return refreshed token with all original fields preserved
  return {
    ...token,
    accessToken,
    refreshToken: refreshToken || token.refreshToken, // Some providers don't return new refresh token
    expiresAt: new Date(Date.now() + expiresIn * 1000).toISOString()
  }
}
