import { AuthCodeListener } from '../oauth/auth-code-listener.js'
import {
  generateCodeChallenge,
  generateCodeVerifier,
  generateState,
} from '../oauth/crypto.js'
import {
  asTrimmedString,
  CODEX_OAUTH_ISSUER,
  CODEX_OAUTH_ORIGINATOR,
  CODEX_OAUTH_SCOPE,
  escapeHtml,
  getCodexOAuthCallbackHost,
  getCodexOAuthCallbackOrigin,
  exchangeCodexIdTokenForApiKey,
  getCodexOAuthCallbackPort,
  getCodexOAuthClientId,
  parseChatgptAccountId,
} from './codexOAuthShared.js'
import { createCombinedAbortSignal } from '../../utils/combinedAbortSignal.js'

type CodexOAuthTokenResponse = {
  id_token?: string
  access_token?: string
  refresh_token?: string
}

export type CodexOAuthTokens = {
  apiKey?: string
  accessToken: string
  refreshToken: string
  idToken?: string
  accountId?: string
}

function buildCodexAuthorizeUrl(options: {
  port: number
  host: string
  codeChallenge: string
  state: string
}): string {
  const redirectUri = `${getCodexOAuthCallbackOrigin(options.port, {
    CODEX_OAUTH_CALLBACK_HOST: options.host,
  } as NodeJS.ProcessEnv)}/auth/callback`
  const authUrl = new URL(`${CODEX_OAUTH_ISSUER}/oauth/authorize`)

  authUrl.searchParams.append('response_type', 'code')
  authUrl.searchParams.append('client_id', getCodexOAuthClientId())
  authUrl.searchParams.append('redirect_uri', redirectUri)
  authUrl.searchParams.append('scope', CODEX_OAUTH_SCOPE)
  authUrl.searchParams.append('code_challenge', options.codeChallenge)
  authUrl.searchParams.append('code_challenge_method', 'S256')
  authUrl.searchParams.append('id_token_add_organizations', 'true')
  authUrl.searchParams.append('codex_cli_simplified_flow', 'true')
  authUrl.searchParams.append('state', options.state)
  authUrl.searchParams.append('originator', CODEX_OAUTH_ORIGINATOR)

  return authUrl.toString()
}

function renderSuccessPage(): string {
  return `<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Codex Login Complete</title>
    <style>
      body { font-family: sans-serif; padding: 32px; line-height: 1.5; color: #111827; }
      h1 { margin: 0 0 12px; font-size: 22px; }
      p { margin: 0 0 10px; }
    </style>
  </head>
  <body>
    <h1>Codex login complete</h1>
    <p>You can return to OpenClaude now.</p>
    <p>OpenClaude will finish activating your new Codex OAuth login.</p>
  </body>
</html>`
}

function renderErrorPage(message: string): string {
  const safeMessage = escapeHtml(message)
  return `<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Codex Login Failed</title>
    <style>
      body { font-family: sans-serif; padding: 32px; line-height: 1.5; color: #111827; }
      h1 { margin: 0 0 12px; font-size: 22px; color: #991b1b; }
      p { margin: 0 0 10px; }
    </style>
  </head>
  <body>
    <h1>Codex login failed</h1>
    <p>${safeMessage}</p>
    <p>You can close this window and try again in OpenClaude.</p>
  </body>
</html>`
}

function renderCancelledPage(): string {
  return `<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Codex Login Cancelled</title>
    <style>
      body { font-family: sans-serif; padding: 32px; line-height: 1.5; color: #111827; }
      h1 { margin: 0 0 12px; font-size: 22px; }
      p { margin: 0 0 10px; }
    </style>
  </head>
  <body>
    <h1>Codex login cancelled</h1>
    <p>You can close this window and retry in OpenClaude.</p>
  </body>
</html>`
}

async function exchangeAuthorizationCode(options: {
  authorizationCode: string
  codeVerifier: string
  port: number
  host: string
  signal?: AbortSignal
}): Promise<CodexOAuthTokens> {
  const redirectUri = `${getCodexOAuthCallbackOrigin(options.port, {
    CODEX_OAUTH_CALLBACK_HOST: options.host,
  } as NodeJS.ProcessEnv)}/auth/callback`
  const body = new URLSearchParams({
    grant_type: 'authorization_code',
    code: options.authorizationCode,
    redirect_uri: redirectUri,
    client_id: getCodexOAuthClientId(),
    code_verifier: options.codeVerifier,
  })

  const { signal, cleanup } = createCombinedAbortSignal(options.signal, {
    timeoutMs: 15_000,
  })
  let payload: CodexOAuthTokenResponse
  try {
    const response = await fetch(`${CODEX_OAUTH_ISSUER}/oauth/token`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      body,
      signal,
    })

    if (!response.ok) {
      const errorText = await response.text().catch(() => '')
      throw new Error(
        errorText.trim()
          ? `Codex OAuth token exchange failed (${response.status}): ${errorText.trim()}`
          : `Codex OAuth token exchange failed with status ${response.status}.`,
      )
    }

    payload = (await response.json()) as CodexOAuthTokenResponse
  } finally {
    cleanup()
  }

  const accessToken = asTrimmedString(payload.access_token)
  const refreshToken = asTrimmedString(payload.refresh_token)
  if (!accessToken || !refreshToken) {
    throw new Error(
      'Codex OAuth completed, but the token response was missing credentials.',
    )
  }

  const idToken = asTrimmedString(payload.id_token)
  const apiKey = idToken
    ? await exchangeCodexIdTokenForApiKey(idToken).catch(() => undefined)
    : undefined

  return {
    apiKey,
    accessToken,
    refreshToken,
    idToken,
    accountId:
      parseChatgptAccountId(idToken) ?? parseChatgptAccountId(accessToken),
  }
}

type CodexOAuthServiceOptions = {
  callbackPort?: number
  callbackHost?: string
  createAuthCodeListener?: (callbackPath: string) => CodexOAuthListener
}

type CodexOAuthListener = Pick<
  AuthCodeListener,
  | 'start'
  | 'hasPendingResponse'
  | 'waitForAuthorization'
  | 'handleSuccessRedirect'
  | 'handleErrorRedirect'
  | 'cancelPendingAuthorization'
>

export class CodexOAuthService {
  private authCodeListener: CodexOAuthListener | null = null
  private port: number | null = null
  private tokenExchangeAbortController: AbortController | null = null

  constructor(private readonly options: CodexOAuthServiceOptions = {}) {}

  private buildCancellationError(): Error {
    return new Error('Codex OAuth flow was cancelled.')
  }

  async startOAuthFlow(
    authURLHandler: (authUrl: string) => Promise<void>,
  ): Promise<CodexOAuthTokens> {
    const codeVerifier = generateCodeVerifier()
    const callbackPort =
      this.options.callbackPort ?? getCodexOAuthCallbackPort()
    const callbackHost =
      this.options.callbackHost ?? getCodexOAuthCallbackHost()
    const authCodeListener =
      this.options.createAuthCodeListener?.('/auth/callback') ??
      new AuthCodeListener('/auth/callback')

    this.authCodeListener = authCodeListener
    this.port = null

    try {
      const port = await authCodeListener.start(callbackPort, callbackHost)
      this.port = port

      const state = generateState()
      const codeChallenge = await generateCodeChallenge(codeVerifier)
      const authUrl = buildCodexAuthorizeUrl({
        port,
        host: callbackHost,
        codeChallenge,
        state,
      })

      try {
        const authorizationCode = await authCodeListener.waitForAuthorization(
          state,
          async () => {
            await authURLHandler(authUrl)
          },
        )

        const tokenExchangeAbortController = new AbortController()
        this.tokenExchangeAbortController = tokenExchangeAbortController

        let tokens: CodexOAuthTokens
        try {
          tokens = await exchangeAuthorizationCode({
            authorizationCode,
            codeVerifier,
            port,
            host: callbackHost,
            signal: tokenExchangeAbortController.signal,
          })
        } finally {
          if (
            this.tokenExchangeAbortController === tokenExchangeAbortController
          ) {
            this.tokenExchangeAbortController = null
          }
        }

        if (this.authCodeListener !== authCodeListener) {
          throw this.buildCancellationError()
        }

        authCodeListener.handleSuccessRedirect([], res => {
          res.writeHead(200, {
            'Content-Type': 'text/html; charset=utf-8',
          })
          res.end(renderSuccessPage())
        })

        return tokens
      } catch (error) {
        const resolvedError =
          this.authCodeListener === authCodeListener
            ? error
            : this.buildCancellationError()

        if (authCodeListener.hasPendingResponse()) {
          const isCancellation =
            resolvedError instanceof Error &&
            resolvedError.message === 'Codex OAuth flow was cancelled.'

          authCodeListener.handleErrorRedirect(res => {
            res.writeHead(isCancellation ? 200 : 400, {
              'Content-Type': 'text/html; charset=utf-8',
            })
            res.end(
              isCancellation
                ? renderCancelledPage()
                : renderErrorPage(
                    resolvedError instanceof Error
                      ? resolvedError.message
                      : String(resolvedError),
                  ),
            )
          })
        }
        throw resolvedError
      } finally {
        this.cleanup()
      }
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error)
      if (
        message.includes('EADDRINUSE') ||
        message.includes(String(callbackPort))
      ) {
        throw new Error(
          `Codex OAuth needs ${callbackHost}:${callbackPort} for its callback. Close any app already using that port and try again.`,
        )
      }
      throw error
    }
  }

  cleanup(): void {
    const cancellationError = this.buildCancellationError()

    this.tokenExchangeAbortController?.abort(cancellationError)
    this.tokenExchangeAbortController = null

    if (this.authCodeListener?.hasPendingResponse()) {
      this.authCodeListener.handleErrorRedirect(res => {
        res.writeHead(200, {
          'Content-Type': 'text/html; charset=utf-8',
        })
        res.end(renderCancelledPage())
      })
    }

    this.authCodeListener?.cancelPendingAuthorization(cancellationError)
    this.authCodeListener = null
    this.port = null
  }
}
