import { logEvent } from 'src/services/analytics/index.js'
import { useCloudOAuthCallbackOnly } from '../../constants/oauth.js'
import { openBrowser } from '../../utils/browser.js'
import { AuthCodeListener } from './auth-code-listener.js'
import * as client from './client.js'
import * as crypto from './crypto.js'
import type {
  OAuthProfileResponse,
  OAuthTokenExchangeResponse,
  OAuthTokens,
  RateLimitTier,
  SubscriptionType,
} from './types.js'

const DEFAULT_OAUTH_CALLBACK_TIMEOUT_MS = 300_000

function oauthCallbackTimeoutMs(): number {
  const n = parseInt(process.env.RIOS_OAUTH_CALLBACK_TIMEOUT_MS || '', 10)
  return n > 0 ? n : DEFAULT_OAUTH_CALLBACK_TIMEOUT_MS
}

function loopbackTimeoutMessage(ms: number): string {
  const sec = Math.round(ms / 1000)
  return (
    `OAuth timed out after ${sec}s waiting for the browser to reach this machine ` +
    `(http://127.0.0.1 or http://localhost with the port from your login link). ` +
    `If the website already showed “success” but RiOS Code did not continue, ` +
    `RiOS Cloud did not redirect to that loopback URL — ask admins to allow ` +
    `dynamic loopback redirects for this OAuth client (RFC 8252). ` +
    `You can also try: export RIOS_OAUTH_LOOPBACK_HOST=localhost, ` +
    `paste the full callback URL in the RiOS Code prompt (if visible), ` +
    `or set RIOS_OAUTH_USE_CLOUD_CALLBACK=1 for paste-only mode.`
  )
}

function cloudPasteTimeoutMessage(ms: number): string {
  const sec = Math.round(ms / 1000)
  return (
    `OAuth timed out after ${sec}s waiting for a pasted callback URL. ` +
    `After signing in at RiOS Cloud, copy the **full URL** from the browser ` +
    `address bar (including ?code=...&state=...) and paste it into the terminal.`
  )
}

/**
 * OAuth service that handles the OAuth 2.0 authorization code flow with PKCE.
 *
 * Supports:
 * 1. Automatic: browser redirects to loopback; local server captures the code
 * 2. Manual paste: paste the callback URL in the terminal (loopback or cloud-only mode), or set
 *    RIOS_OAUTH_USE_CLOUD_CALLBACK=1 when you want RiOS Cloud opened without a local listener.
 */
export class OAuthService {
  private codeVerifier: string
  private authCodeListener: AuthCodeListener | null = null
  private port: number | null = null
  private manualAuthCodeResolver: ((authorizationCode: string) => void) | null =
    null
  private cloudCallbackWaitTimeout: ReturnType<typeof setTimeout> | undefined
  private loopbackWaitTimeoutId: ReturnType<typeof setTimeout> | undefined
  /** Set when the user pastes a callback URL so token exchange matches that redirect_uri. */
  private manualTokenExchangeUseManualRedirect: boolean | undefined = undefined

  constructor() {
    this.codeVerifier = crypto.generateCodeVerifier()
  }

  async startOAuthFlow(
    authURLHandler: (
      automaticUrl: string,
      manualUrl?: string,
    ) => Promise<void>,
    options?: {
      loginWithClaudeAi?: boolean
      inferenceOnly?: boolean
      expiresIn?: number
      orgUUID?: string
      loginHint?: string
      loginMethod?: string
      /**
       * Don't call openBrowser(). Caller takes both URLs via authURLHandler
       * and decides how/where to open them. Used by the SDK control protocol
       * (claude_authenticate) where the SDK client owns the user's display,
       * not this process.
       */
      skipBrowserOpen?: boolean
    },
  ): Promise<OAuthTokens> {
    const cloudOnly = useCloudOAuthCallbackOnly()
    this.manualTokenExchangeUseManualRedirect = undefined

    if (!cloudOnly) {
      this.authCodeListener = new AuthCodeListener()
      this.port = await this.authCodeListener.start()
    } else {
      this.authCodeListener = null
      this.port = 0
    }

    const codeChallenge = crypto.generateCodeChallenge(this.codeVerifier)
    const state = crypto.generateState()

    const opts = {
      codeChallenge,
      state,
      port: this.port ?? 0,
      loginWithClaudeAi: options?.loginWithClaudeAi,
      inferenceOnly: options?.inferenceOnly,
      orgUUID: options?.orgUUID,
      loginHint: options?.loginHint,
      loginMethod: options?.loginMethod,
    }
    const manualFlowUrl = client.buildAuthUrl({ ...opts, isManual: true })
    const automaticFlowUrl = cloudOnly
      ? manualFlowUrl
      : client.buildAuthUrl({ ...opts, isManual: false })

    const onReady = async () => {
      if (options?.skipBrowserOpen) {
        await authURLHandler(automaticFlowUrl, manualFlowUrl)
      } else {
        await authURLHandler(automaticFlowUrl, manualFlowUrl)
        await openBrowser(automaticFlowUrl)
      }
    }

    const authorizationCode = cloudOnly
      ? await this.waitForCloudAuthorizationPaste(onReady)
      : await this.waitForAuthorizationCode(state, onReady)

    const isAutomaticFlow =
      !cloudOnly && (this.authCodeListener?.hasPendingResponse() ?? false)
    const useManualRedirect =
      cloudOnly ||
      (this.manualTokenExchangeUseManualRedirect !== undefined
        ? this.manualTokenExchangeUseManualRedirect
        : !isAutomaticFlow)
    logEvent('tengu_oauth_auth_code_received', {
      automatic: isAutomaticFlow,
      cloud_callback_only: cloudOnly,
    })

    try {
      const tokenResponse = await client.exchangeCodeForTokens(
        authorizationCode,
        state,
        this.codeVerifier,
        this.port!,
        useManualRedirect,
        options?.expiresIn,
      )

      const profileInfo = await client.fetchProfileInfo(
        tokenResponse.access_token,
      )

      if (isAutomaticFlow) {
        const scopes = client.parseScopes(tokenResponse.scope)
        this.authCodeListener?.handleSuccessRedirect(scopes)
      }

      return this.formatTokens(
        tokenResponse,
        profileInfo.subscriptionType,
        profileInfo.rateLimitTier,
        profileInfo.rawProfile,
      )
    } catch (error) {
      if (isAutomaticFlow) {
        this.authCodeListener?.handleErrorRedirect()
      }
      throw error
    } finally {
      this.manualTokenExchangeUseManualRedirect = undefined
      this.authCodeListener?.close()
    }
  }

  private clearCloudCallbackTimeout(): void {
    if (this.cloudCallbackWaitTimeout !== undefined) {
      clearTimeout(this.cloudCallbackWaitTimeout)
      this.cloudCallbackWaitTimeout = undefined
    }
  }

  private clearLoopbackWaitTimeout(): void {
    if (this.loopbackWaitTimeoutId !== undefined) {
      clearTimeout(this.loopbackWaitTimeoutId)
      this.loopbackWaitTimeoutId = undefined
    }
  }

  /** RiOS Cloud redirect only — user pastes callback URL into the terminal. */
  private async waitForCloudAuthorizationPaste(
    onReady: () => Promise<void>,
  ): Promise<string> {
    const timeoutMs = oauthCallbackTimeoutMs()
    const timeoutMessage = cloudPasteTimeoutMessage(timeoutMs)

    return new Promise<string>((resolve, reject) => {
      this.manualAuthCodeResolver = resolve

      this.cloudCallbackWaitTimeout = setTimeout(() => {
        this.manualAuthCodeResolver = null
        this.clearCloudCallbackTimeout()
        reject(new Error(timeoutMessage))
      }, timeoutMs)

      void onReady().catch((err: unknown) => {
        this.clearCloudCallbackTimeout()
        this.manualAuthCodeResolver = null
        reject(err instanceof Error ? err : new Error(String(err)))
      })
    })
  }

  private async waitForAuthorizationCode(
    state: string,
    onReady: () => Promise<void>,
  ): Promise<string> {
    const timeoutMs = oauthCallbackTimeoutMs()
    const timeoutMessage = loopbackTimeoutMessage(timeoutMs)

    return new Promise<string>((resolve, reject) => {
      this.manualAuthCodeResolver = resolve

      this.loopbackWaitTimeoutId = setTimeout(() => {
        this.loopbackWaitTimeoutId = undefined
        this.manualAuthCodeResolver = null
        this.authCodeListener?.abortWait(new Error(timeoutMessage))
        this.authCodeListener?.close()
        reject(new Error(timeoutMessage))
      }, timeoutMs)

      this.authCodeListener
        ?.waitForAuthorization(state, onReady)
        .then(authorizationCode => {
          this.clearLoopbackWaitTimeout()
          this.manualAuthCodeResolver = null
          resolve(authorizationCode)
        })
        .catch(error => {
          this.clearLoopbackWaitTimeout()
          this.manualAuthCodeResolver = null
          reject(error)
        })
    })
  }

  handleManualAuthCodeInput(params: {
    authorizationCode: string
    state: string
    useManualRedirectForTokenExchange?: boolean
  }): void {
    if (this.manualAuthCodeResolver) {
      this.clearCloudCallbackTimeout()
      this.clearLoopbackWaitTimeout()
      if (params.useManualRedirectForTokenExchange !== undefined) {
        this.manualTokenExchangeUseManualRedirect =
          params.useManualRedirectForTokenExchange
      }
      this.manualAuthCodeResolver(params.authorizationCode)
      this.manualAuthCodeResolver = null
      this.authCodeListener?.close()
    }
  }

  private formatTokens(
    response: OAuthTokenExchangeResponse,
    subscriptionType: SubscriptionType | null,
    rateLimitTier: RateLimitTier | null,
    profile?: OAuthProfileResponse,
  ): OAuthTokens {
    return {
      accessToken: response.access_token,
      refreshToken: response.refresh_token,
      expiresAt: Date.now() + response.expires_in * 1000,
      scopes: client.parseScopes(response.scope),
      subscriptionType,
      rateLimitTier,
      profile,
      tokenAccount: response.account
        ? {
            uuid: response.account.uuid,
            emailAddress: response.account.email_address,
            organizationUuid: response.organization?.uuid,
          }
        : undefined,
    }
  }

  cleanup(): void {
    this.clearCloudCallbackTimeout()
    this.clearLoopbackWaitTimeout()
    this.authCodeListener?.close()
    this.manualAuthCodeResolver = null
    this.manualTokenExchangeUseManualRedirect = undefined
  }
}
