use crate::config::paths::Paths;
use crate::conversation::message::{Message, MessageContent};
use crate::model::ModelConfig;
use crate::providers::api_client::AuthProvider;
use crate::providers::base::{ConfigKey, MessageStream, Provider, ProviderDef, ProviderMetadata};
use crate::providers::errors::ProviderError;
use crate::providers::formats::openai_responses::responses_api_to_streaming_message;
use crate::providers::openai_compatible::handle_status;
use crate::providers::retry::ProviderRetry;
use crate::session_context::SESSION_ID_HEADER;
use anyhow::{anyhow, Result};
use async_stream::try_stream;
use async_trait::async_trait;
use axum::{extract::Query, response::Html, routing::get, Router};
use base64::Engine;
use chrono::{DateTime, Utc};
use futures::future::BoxFuture;
use futures::{StreamExt, TryStreamExt};
use jsonwebtoken::jwk::JwkSet;
use jsonwebtoken::{decode, decode_header, DecodingKey, Validation};
use reqwest::header::{HeaderName, HeaderValue};
use rmcp::model::{RawContent, Role, Tool};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use sha2::Digest;
use std::io;
use std::net::SocketAddr;
use std::ops::Deref;
use std::path::PathBuf;
use std::sync::{Arc, LazyLock};
use tokio::pin;
use tokio::sync::{oneshot, Mutex as TokioMutex};
use tokio_util::codec::{FramedRead, LinesCodec};
use tokio_util::io::StreamReader;

const CLIENT_ID: &str = "app_EMoamEEZ73f0CkXaXp7hrann";
const ISSUER: &str = "https://auth.openai.com";
const CODEX_API_ENDPOINT: &str = "https://chatgpt.com/backend-api/codex";
const OAUTH_SCOPES: &[&str] = &["openid", "profile", "email", "offline_access"];
// Canonical localhost callback port for Codex OAuth (default localhost:1455 per OpenAI docs).
// https://developers.openai.com/codex/auth/
const OAUTH_PORT: u16 = 1455;
// Allow time for users to complete the browser-based OAuth flow.
const OAUTH_TIMEOUT_SECS: u64 = 300;
const HTML_AUTO_CLOSE_TIMEOUT_MS: u64 = 2000;

const CHATGPT_CODEX_PROVIDER_NAME: &str = "chatgpt_codex";
pub const CHATGPT_CODEX_DEFAULT_MODEL: &str = "gpt-5.5";

#[derive(Debug)]
pub struct ChatGptCodexModelAttrs {
    pub name: &'static str,
    pub reasoning_levels: &'static [&'static str],
}

pub const CHATGPT_CODEX_KNOWN_MODELS: &[ChatGptCodexModelAttrs] = &[
    ChatGptCodexModelAttrs {
        name: "gpt-5.5",
        reasoning_levels: &["low", "medium", "high", "xhigh"],
    },
    ChatGptCodexModelAttrs {
        name: "gpt-5.4",
        reasoning_levels: &["low", "medium", "high", "xhigh"],
    },
    ChatGptCodexModelAttrs {
        name: "gpt-5.3-codex",
        reasoning_levels: &["low", "medium", "high", "xhigh"],
    },
];

const CHATGPT_CODEX_DOC_URL: &str = "https://openai.com/chatgpt";

const DEFAULT_REASONING_LEVELS: &[&str] = &["medium", "high"];

pub fn reasoning_levels_for_model(model_name: &str) -> &'static [&'static str] {
    CHATGPT_CODEX_KNOWN_MODELS
        .iter()
        .find(|m| m.name == model_name)
        .map(|m| m.reasoning_levels)
        .unwrap_or(DEFAULT_REASONING_LEVELS)
}

fn known_model_names() -> Vec<&'static str> {
    CHATGPT_CODEX_KNOWN_MODELS.iter().map(|m| m.name).collect()
}

const GPT_53_CODEX_TOOL_PREAMBLE: &str = "\
You are a coding agent. You have access to tools to accomplish tasks. \
Always use your tools to fulfill requests - do not just describe what you would do. \
Keep going until the query is completely resolved before yielding back to the user. \
Autonomously resolve the query using the tools available to you. \
Do NOT guess or make up an answer. \
Before making tool calls, send a brief message explaining what you're about to do.";

#[derive(Debug)]
struct ChatGptCodexAuthState {
    oauth_mutex: TokioMutex<()>,
    jwks_cache: TokioMutex<Option<JwkSet>>,
}

impl ChatGptCodexAuthState {
    fn new() -> Self {
        Self {
            oauth_mutex: TokioMutex::new(()),
            jwks_cache: TokioMutex::new(None),
        }
    }

    fn instance() -> Arc<Self> {
        Arc::clone(&CHATGPT_CODEX_AUTH_STATE)
    }
}

static CHATGPT_CODEX_AUTH_STATE: LazyLock<Arc<ChatGptCodexAuthState>> =
    LazyLock::new(|| Arc::new(ChatGptCodexAuthState::new()));

fn build_input_items(messages: &[Message]) -> Result<Vec<Value>> {
    let mut items = Vec::new();

    for message in messages {
        let role = match message.role {
            Role::User => Some("user"),
            Role::Assistant => Some("assistant"),
        };
        let mut content_items: Vec<Value> = Vec::new();

        let flush_text = |items: &mut Vec<Value>, role: Option<&str>, content: &mut Vec<Value>| {
            if let Some(role) = role {
                if !content.is_empty() {
                    items.push(json!({ "role": role, "content": std::mem::take(content) }));
                }
            } else {
                content.clear();
            }
        };

        for content in &message.content {
            match content {
                MessageContent::Text(text) => {
                    if !text.text.is_empty() {
                        let content_type = if message.role == Role::Assistant {
                            "output_text"
                        } else {
                            "input_text"
                        };
                        content_items.push(json!({ "type": content_type, "text": text.text }));
                    }
                }
                MessageContent::Image(img) => {
                    content_items.push(json!({
                        "type": "input_image",
                        "image_url": format!("data:{};base64,{}", img.mime_type, img.data),
                    }));
                }
                MessageContent::ToolRequest(request) => {
                    flush_text(&mut items, role, &mut content_items);
                    if let Ok(tool_call) = &request.tool_call {
                        let arguments_str = match tool_call.arguments.as_ref() {
                            Some(args) => serde_json::to_string(args)?,
                            None => "{}".to_string(),
                        };
                        items.push(json!({
                            "type": "function_call",
                            "call_id": request.id,
                            "name": tool_call.name,
                            "arguments": arguments_str
                        }));
                    }
                }
                MessageContent::ToolResponse(response) => {
                    flush_text(&mut items, role, &mut content_items);
                    match &response.tool_result {
                        Ok(contents) => {
                            let text_content: Vec<String> = contents
                                .content
                                .iter()
                                .filter_map(|c| {
                                    if let RawContent::Text(t) = c.deref() {
                                        Some(t.text.clone())
                                    } else {
                                        None
                                    }
                                })
                                .collect();
                            if !text_content.is_empty() {
                                items.push(json!({
                                    "type": "function_call_output",
                                    "call_id": response.id,
                                    "output": text_content.join("\n")
                                }));
                            }
                        }
                        Err(error_data) => {
                            items.push(json!({
                                "type": "function_call_output",
                                "call_id": response.id,
                                "output": format!("Error: {}", error_data.message)
                            }));
                        }
                    }
                }
                _ => {}
            }
        }

        flush_text(&mut items, role, &mut content_items);
    }

    Ok(items)
}

fn get_reasoning_effort(model_name: &str) -> String {
    let config = crate::config::Config::global();
    let effort = config
        .get_chatgpt_codex_reasoning_effort()
        .map(String::from)
        .unwrap_or_else(|_| "medium".to_string());

    let valid_levels = reasoning_levels_for_model(model_name);
    if valid_levels.contains(&effort.as_str()) {
        effort
    } else {
        tracing::warn!(
            "Invalid CHATGPT_CODEX_REASONING_EFFORT '{}' for model '{}', using 'medium'",
            effort,
            model_name
        );
        "medium".to_string()
    }
}

fn reasoning_effort_for_config(model_config: &ModelConfig) -> Option<String> {
    use crate::model::ThinkingEffort;

    model_config
        .thinking_effort()
        .map(|effort| {
            let valid_levels = reasoning_levels_for_model(&model_config.model_name);
            let preferred_levels: &[&str] = match effort {
                ThinkingEffort::Off => return None,
                ThinkingEffort::Low => &["low", "medium", "high", "xhigh"],
                ThinkingEffort::Medium => &["medium", "high", "low", "xhigh"],
                ThinkingEffort::High => &["high", "medium", "xhigh", "low"],
                ThinkingEffort::Max => &["xhigh", "high", "medium", "low"],
            };

            preferred_levels
                .iter()
                .find(|level| valid_levels.contains(level))
                .map(|level| (*level).to_string())
        })
        .unwrap_or_else(|| Some(get_reasoning_effort(&model_config.model_name)))
}

fn create_codex_request(
    model_config: &ModelConfig,
    system: &str,
    messages: &[Message],
    tools: &[Tool],
) -> Result<Value> {
    let input_items = build_input_items(messages)?;
    let reasoning_effort = reasoning_effort_for_config(model_config);

    let instructions = match model_config.model_name.as_str() {
        "gpt-5.3-codex" => format!("{GPT_53_CODEX_TOOL_PREAMBLE}\n\n{system}"),
        _ => system.to_string(),
    };

    let mut payload = json!({
        "model": model_config.model_name,
        "input": input_items,
        "store": false,
        "instructions": instructions,
    });

    let payload_obj = payload
        .as_object_mut()
        .ok_or_else(|| anyhow!("Codex payload must be a JSON object"))?;

    if !tools.is_empty() {
        let tools_spec: Vec<Value> = tools
            .iter()
            .map(|tool| {
                json!({
                    "type": "function",
                    "name": tool.name,
                    "description": tool.description,
                    "parameters": tool.input_schema,
                })
            })
            .collect();

        payload_obj.insert("tools".to_string(), json!(tools_spec));
        payload_obj.insert("tool_choice".to_string(), json!("auto"));
        payload_obj.insert("parallel_tool_calls".to_string(), json!(true));
    }

    if let Some(temp) = model_config.temperature {
        payload_obj.insert("temperature".to_string(), json!(temp));
    }

    if let Some(reasoning_effort) = reasoning_effort {
        payload_obj.insert(
            "reasoning".to_string(),
            json!({ "effort": reasoning_effort }),
        );
    }

    Ok(payload)
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct TokenData {
    access_token: String,
    refresh_token: String,
    id_token: Option<String>,
    expires_at: DateTime<Utc>,
    account_id: Option<String>,
}

#[derive(Debug, Clone)]
struct TokenCache {
    cache_path: PathBuf,
}

fn get_cache_path() -> PathBuf {
    Paths::in_config_dir("chatgpt_codex/tokens.json")
}

impl TokenCache {
    fn new() -> Self {
        let cache_path = get_cache_path();
        if let Some(parent) = cache_path.parent() {
            let _ = std::fs::create_dir_all(parent);
        }
        Self { cache_path }
    }

    fn load(&self) -> Option<TokenData> {
        if let Ok(contents) = std::fs::read_to_string(&self.cache_path) {
            serde_json::from_str(&contents).ok()
        } else {
            None
        }
    }

    fn save(&self, token_data: &TokenData) -> Result<()> {
        if let Some(parent) = self.cache_path.parent() {
            std::fs::create_dir_all(parent)?;
        }
        let contents = serde_json::to_string(token_data)?;
        std::fs::write(&self.cache_path, contents)?;
        Ok(())
    }

    fn clear(&self) {
        let _ = std::fs::remove_file(&self.cache_path);
    }
}

#[derive(Debug, Deserialize)]
struct JwtClaims {
    chatgpt_account_id: Option<String>,
    #[serde(rename = "https://api.openai.com/auth")]
    auth_claims: Option<AuthClaims>,
    organizations: Option<Vec<OrgInfo>>,
}

#[derive(Debug, Deserialize)]
struct AuthClaims {
    chatgpt_account_id: Option<String>,
}

#[derive(Debug, Deserialize)]
struct OrgInfo {
    id: String,
}

#[derive(Debug, Deserialize)]
struct OidcConfiguration {
    jwks_uri: String,
}

async fn fetch_jwks_for(issuer: &str) -> Result<JwkSet> {
    let client = reqwest::Client::new();
    let config_url = format!("{}/.well-known/openid-configuration", issuer);
    let config = client
        .get(config_url)
        .send()
        .await?
        .error_for_status()?
        .json::<OidcConfiguration>()
        .await?;

    let jwks = client
        .get(config.jwks_uri)
        .send()
        .await?
        .error_for_status()?
        .json::<JwkSet>()
        .await?;

    Ok(jwks)
}

async fn get_jwks(state: &ChatGptCodexAuthState) -> Result<JwkSet> {
    let mut cache = state.jwks_cache.lock().await;
    if let Some(jwks) = cache.clone() {
        return Ok(jwks);
    }
    let jwks = fetch_jwks_for(ISSUER).await?;
    *cache = Some(jwks.clone());
    Ok(jwks)
}

fn parse_jwt_claims_with_jwks(token: &str, jwks: &JwkSet) -> Result<JwtClaims> {
    let header = decode_header(token)?;
    let kid = header
        .kid
        .ok_or_else(|| anyhow!("JWT header missing kid"))?;
    let jwk = jwks
        .find(&kid)
        .ok_or_else(|| anyhow!("JWT signing key not found"))?;
    let decoding_key = DecodingKey::from_jwk(jwk)?;

    let mut validation = Validation::new(header.alg);
    validation.validate_aud = false;

    let token_data = decode::<JwtClaims>(token, &decoding_key, &validation)?;
    Ok(token_data.claims)
}

fn parse_jwt_claims_unverified(token: &str) -> Option<JwtClaims> {
    let parts: Vec<&str> = token.split('.').collect();
    if parts.len() != 3 {
        return None;
    }
    let payload = base64::engine::general_purpose::URL_SAFE_NO_PAD
        .decode(parts[1])
        .ok()?;
    serde_json::from_slice(&payload).ok()
}

async fn parse_jwt_claims(token: &str, state: &ChatGptCodexAuthState) -> Option<JwtClaims> {
    if let Ok(jwks) = get_jwks(state).await {
        if let Ok(claims) = parse_jwt_claims_with_jwks(token, &jwks) {
            return Some(claims);
        }
    }
    parse_jwt_claims_unverified(token)
}

fn account_id_from_claims(claims: &JwtClaims) -> Option<String> {
    if let Some(id) = claims.chatgpt_account_id.as_ref() {
        return Some(id.clone());
    }
    if let Some(auth) = claims.auth_claims.as_ref() {
        if let Some(id) = auth.chatgpt_account_id.as_ref() {
            return Some(id.clone());
        }
    }
    if let Some(orgs) = claims.organizations.as_ref() {
        if let Some(org) = orgs.first() {
            return Some(org.id.clone());
        }
    }
    None
}

async fn extract_account_id(
    token_data: &TokenData,
    state: &ChatGptCodexAuthState,
) -> Option<String> {
    if let Some(id_token) = token_data.id_token.as_deref() {
        if let Some(claims) = parse_jwt_claims(id_token, state).await {
            if let Some(account_id) = account_id_from_claims(&claims) {
                return Some(account_id);
            }
        }
    }

    parse_jwt_claims(&token_data.access_token, state)
        .await
        .and_then(|claims| account_id_from_claims(&claims))
}

struct PkceChallenge {
    verifier: String,
    challenge: String,
}

fn generate_pkce() -> PkceChallenge {
    let verifier = nanoid::nanoid!(43);
    let digest = sha2::Sha256::digest(verifier.as_bytes());
    let challenge = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(digest);
    PkceChallenge {
        verifier,
        challenge,
    }
}

fn generate_state() -> String {
    nanoid::nanoid!(32)
}

fn build_authorize_url(redirect_uri: &str, pkce: &PkceChallenge, state: &str) -> Result<String> {
    let scopes = OAUTH_SCOPES.join(" ");
    let params = [
        ("response_type", "code"),
        ("client_id", CLIENT_ID),
        ("redirect_uri", redirect_uri),
        ("scope", &scopes),
        ("code_challenge", &pkce.challenge),
        ("code_challenge_method", "S256"),
        ("id_token_add_organizations", "true"),
        ("codex_cli_simplified_flow", "true"),
        ("state", state),
        ("originator", "goose"),
    ];
    let query = serde_urlencoded::to_string(params)?;
    Ok(format!("{}/oauth/authorize?{}", ISSUER, query))
}

#[derive(Debug, Deserialize)]
struct TokenResponse {
    access_token: String,
    refresh_token: String,
    id_token: Option<String>,
    expires_in: Option<i64>,
}

async fn exchange_code_for_tokens_with_issuer(
    issuer: &str,
    code: &str,
    redirect_uri: &str,
    pkce: &PkceChallenge,
) -> Result<TokenResponse> {
    let client = reqwest::Client::new();
    let params = [
        ("grant_type", "authorization_code"),
        ("code", code),
        ("redirect_uri", redirect_uri),
        ("client_id", CLIENT_ID),
        ("code_verifier", &pkce.verifier),
    ];

    let resp = client
        .post(format!("{}/oauth/token", issuer))
        .header("Content-Type", "application/x-www-form-urlencoded")
        .form(&params)
        .send()
        .await?;

    if !resp.status().is_success() {
        let status = resp.status();
        let text = resp.text().await.unwrap_or_default();
        return Err(anyhow!("Token exchange failed ({}): {}", status, text));
    }

    Ok(resp.json().await?)
}

async fn refresh_access_token_with_issuer(
    issuer: &str,
    refresh_token: &str,
) -> Result<TokenResponse> {
    let client = reqwest::Client::new();
    let params = [
        ("grant_type", "refresh_token"),
        ("refresh_token", refresh_token),
        ("client_id", CLIENT_ID),
    ];

    let resp = client
        .post(format!("{}/oauth/token", issuer))
        .header("Content-Type", "application/x-www-form-urlencoded")
        .form(&params)
        .send()
        .await?;

    if !resp.status().is_success() {
        let status = resp.status();
        let text = resp.text().await.unwrap_or_default();
        return Err(anyhow!("Token refresh failed ({}): {}", status, text));
    }

    Ok(resp.json().await?)
}

const HTML_SUCCESS_TEMPLATE: &str = r#"<!doctype html>
<html>
  <head>
    <title>goose - ChatGPT 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 goose.</p>
    </div>
    <script>const AUTO_CLOSE_TIMEOUT_MS = __AUTO_CLOSE_TIMEOUT_MS__; setTimeout(() => window.close(), AUTO_CLOSE_TIMEOUT_MS)</script>
  </body>
</html>"#;

fn html_success() -> String {
    HTML_SUCCESS_TEMPLATE.replace(
        "__AUTO_CLOSE_TIMEOUT_MS__",
        &HTML_AUTO_CLOSE_TIMEOUT_MS.to_string(),
    )
}

fn html_error(error: &str) -> String {
    let safe_error = v_htmlescape::escape_fmt(error);
    format!(
        r#"<!doctype html>
<html>
  <head>
    <title>goose - ChatGPT 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; }}
      h1 {{ color: #fc533a; margin-bottom: 1rem; }}
      p {{ color: #b7b1b1; }}
      .error {{
        color: #ff917b;
        font-family: monospace;
        margin-top: 1rem;
        padding: 1rem;
        background: #3c140d;
        border-radius: 0.5rem;
      }}
    </style>
  </head>
  <body>
    <div class="container">
      <h1>Authorization Failed</h1>
      <p>An error occurred during authorization.</p>
      <div class="error">{}</div>
    </div>
  </body>
</html>"#,
        safe_error
    )
}

#[derive(Deserialize)]
struct CallbackParams {
    code: Option<String>,
    state: Option<String>,
    error: Option<String>,
    error_description: Option<String>,
}

fn oauth_callback_router(
    expected_state: String,
    tx: Arc<TokioMutex<Option<oneshot::Sender<Result<String>>>>>,
) -> Router {
    Router::new().route(
        "/auth/callback",
        get(move |Query(params): Query<CallbackParams>| {
            let tx = tx.clone();
            let expected = expected_state.clone();
            async move {
                if let Some(error) = params.error {
                    let msg = params.error_description.unwrap_or(error);
                    if let Some(sender) = tx.lock().await.take() {
                        let _ = sender.send(Err(anyhow!("{}", msg)));
                    }
                    return Html(html_error(&msg));
                }

                let code = match params.code {
                    Some(c) => c,
                    None => {
                        let msg = "Missing authorization code";
                        if let Some(sender) = tx.lock().await.take() {
                            let _ = sender.send(Err(anyhow!("{}", msg)));
                        }
                        return Html(html_error(msg));
                    }
                };

                if params.state.as_deref() != Some(&expected) {
                    let msg = "Invalid state - potential CSRF attack";
                    if let Some(sender) = tx.lock().await.take() {
                        let _ = sender.send(Err(anyhow!("{}", msg)));
                    }
                    return Html(html_error(msg));
                }

                if let Some(sender) = tx.lock().await.take() {
                    let _ = sender.send(Ok(code));
                }
                Html(html_success())
            }
        }),
    )
}

async fn spawn_oauth_server(app: Router) -> Result<tokio::task::JoinHandle<()>> {
    let addr = SocketAddr::from(([127, 0, 0, 1], OAUTH_PORT));
    let listener = tokio::net::TcpListener::bind(addr).await.map_err(|e| {
        if e.kind() == io::ErrorKind::AddrInUse {
            anyhow!(
                "OAuth callback server failed to bind to {}: port {} is already in use. \
                 Please stop the process using this port and try again.",
                addr,
                OAUTH_PORT
            )
        } else {
            anyhow!("OAuth callback server failed to bind to {}: {}", addr, e)
        }
    })?;
    Ok(tokio::spawn(async move {
        let server = axum::serve(listener, app);
        let _ = server.await;
    }))
}

struct ServerHandleGuard(Option<tokio::task::JoinHandle<()>>);

impl ServerHandleGuard {
    fn new(handle: tokio::task::JoinHandle<()>) -> Self {
        Self(Some(handle))
    }

    fn abort(&mut self) {
        if let Some(handle) = self.0.take() {
            handle.abort();
        }
    }
}

impl Drop for ServerHandleGuard {
    fn drop(&mut self) {
        self.abort();
    }
}

async fn wait_for_oauth_code(rx: oneshot::Receiver<Result<String>>) -> Result<String> {
    let code_result =
        tokio::time::timeout(std::time::Duration::from_secs(OAUTH_TIMEOUT_SECS), rx).await;
    code_result
        .map_err(|_| anyhow!("OAuth flow timed out"))??
        .map_err(|e| anyhow!("OAuth callback error: {}", e))
}

async fn perform_oauth_flow(auth_state: &ChatGptCodexAuthState) -> Result<TokenData> {
    let _guard = auth_state.oauth_mutex.try_lock().map_err(|_| {
        anyhow!("Another OAuth flow is already in progress; please try again later")
    })?;

    let pkce = generate_pkce();
    let csrf_state = generate_state();
    let redirect_uri = format!("http://localhost:{}/auth/callback", OAUTH_PORT);
    let auth_url = build_authorize_url(&redirect_uri, &pkce, &csrf_state)?;

    let (tx, rx) = oneshot::channel::<Result<String>>();
    let tx = Arc::new(TokioMutex::new(Some(tx)));
    let app = oauth_callback_router(csrf_state.clone(), tx);
    let server_handle = spawn_oauth_server(app).await?;
    let mut server_guard = ServerHandleGuard::new(server_handle);

    if webbrowser::open(&auth_url).is_err() {
        tracing::info!("Please open this URL in your browser:\n{}", auth_url);
    }

    let code_result = wait_for_oauth_code(rx).await;
    server_guard.abort();
    let code = code_result?;

    let tokens = exchange_code_for_tokens_with_issuer(ISSUER, &code, &redirect_uri, &pkce).await?;

    let expires_at = Utc::now() + chrono::Duration::seconds(tokens.expires_in.unwrap_or(3600));

    let mut token_data = TokenData {
        access_token: tokens.access_token,
        refresh_token: tokens.refresh_token,
        id_token: tokens.id_token,
        expires_at,
        account_id: None,
    };

    token_data.account_id = extract_account_id(&token_data, auth_state).await;

    Ok(token_data)
}

#[derive(Debug)]
struct ChatGptCodexAuthProvider {
    cache: TokenCache,
    state: Arc<ChatGptCodexAuthState>,
}

impl ChatGptCodexAuthProvider {
    fn new(state: Arc<ChatGptCodexAuthState>) -> Self {
        Self {
            cache: TokenCache::new(),
            state,
        }
    }

    fn clear_cached_tokens(&self) {
        self.cache.clear();
    }

    async fn get_valid_token(&self) -> Result<TokenData> {
        if let Some(mut token_data) = self.cache.load() {
            if token_data.expires_at > Utc::now() + chrono::Duration::seconds(60) {
                return Ok(token_data);
            }

            tracing::debug!("Token expired, attempting refresh");
            match refresh_access_token_with_issuer(ISSUER, &token_data.refresh_token).await {
                Ok(new_tokens) => {
                    token_data.access_token = new_tokens.access_token;
                    token_data.refresh_token = new_tokens.refresh_token;
                    if new_tokens.id_token.is_some() {
                        token_data.id_token = new_tokens.id_token;
                    }
                    token_data.expires_at = Utc::now()
                        + chrono::Duration::seconds(new_tokens.expires_in.unwrap_or(3600));
                    if token_data.account_id.is_none() {
                        token_data.account_id =
                            extract_account_id(&token_data, self.state.as_ref()).await;
                    }
                    self.cache.save(&token_data)?;
                    tracing::info!("Token refreshed successfully");
                    return Ok(token_data);
                }
                Err(e) => {
                    tracing::warn!("Token refresh failed, will re-authenticate: {}", e);
                    self.cache.clear();
                }
            }
        }

        tracing::info!("Starting OAuth flow for ChatGPT Codex");
        let token_data = perform_oauth_flow(self.state.as_ref()).await?;
        self.cache.save(&token_data)?;
        Ok(token_data)
    }
}

#[async_trait]
impl AuthProvider for ChatGptCodexAuthProvider {
    async fn get_auth_header(&self) -> Result<(String, String)> {
        let token_data = self.get_valid_token().await?;
        Ok((
            "Authorization".to_string(),
            format!("Bearer {}", token_data.access_token),
        ))
    }
}

#[derive(Debug, serde::Serialize)]
pub struct ChatGptCodexProvider {
    #[serde(skip)]
    auth_provider: Arc<ChatGptCodexAuthProvider>,
    model: ModelConfig,
    #[serde(skip)]
    name: String,
}

impl ChatGptCodexProvider {
    pub async fn cleanup() -> Result<()> {
        TokenCache::new().clear();
        Ok(())
    }

    pub async fn from_env(model: ModelConfig) -> Result<Self> {
        let auth_provider = Arc::new(ChatGptCodexAuthProvider::new(
            ChatGptCodexAuthState::instance(),
        ));

        Ok(Self {
            auth_provider,
            model,
            name: CHATGPT_CODEX_PROVIDER_NAME.to_string(),
        })
    }

    async fn post_streaming(
        &self,
        session_id: Option<&str>,
        payload: &Value,
    ) -> Result<reqwest::Response, ProviderError> {
        let token_data = self
            .auth_provider
            .get_valid_token()
            .await
            .map_err(|e| ProviderError::Authentication(e.to_string()))?;

        let mut headers = reqwest::header::HeaderMap::new();
        if let Some(account_id) = &token_data.account_id {
            headers.insert(
                reqwest::header::HeaderName::from_static("chatgpt-account-id"),
                reqwest::header::HeaderValue::from_str(account_id)
                    .map_err(|e| ProviderError::ExecutionError(e.to_string()))?,
            );
        }

        if let Some(session_id) = session_id.filter(|id| !id.is_empty()) {
            headers.insert(
                HeaderName::from_static(SESSION_ID_HEADER),
                HeaderValue::from_str(session_id)
                    .map_err(|e| ProviderError::ExecutionError(e.to_string()))?,
            );
        }

        let client = reqwest::Client::new();
        let response = client
            .post(format!("{}/responses", CODEX_API_ENDPOINT))
            .header(
                "Authorization",
                format!("Bearer {}", token_data.access_token),
            )
            .header("Content-Type", "application/json")
            .headers(headers)
            .json(payload)
            .send()
            .await
            .map_err(|e| ProviderError::RequestFailed(e.to_string()))?;

        handle_status(response).await
    }
}

impl ProviderDef for ChatGptCodexProvider {
    type Provider = Self;

    fn metadata() -> ProviderMetadata {
        ProviderMetadata::new(
            CHATGPT_CODEX_PROVIDER_NAME,
            "ChatGPT Codex",
            "Use your ChatGPT Plus/Pro subscription for GPT-5 Codex models via OAuth",
            CHATGPT_CODEX_DEFAULT_MODEL,
            known_model_names(),
            CHATGPT_CODEX_DOC_URL,
            vec![ConfigKey::new_oauth(
                "CHATGPT_CODEX_TOKEN",
                true,
                true,
                None,
                false,
            )],
        )
    }

    fn from_env(
        model: ModelConfig,
        _extensions: Vec<crate::config::ExtensionConfig>,
    ) -> BoxFuture<'static, Result<Self::Provider>> {
        Box::pin(Self::from_env(model))
    }

    fn inventory_configured() -> bool {
        TokenCache::new().load().is_some()
    }
}

#[async_trait]
impl Provider for ChatGptCodexProvider {
    fn get_name(&self) -> &str {
        &self.name
    }

    fn get_model_config(&self) -> ModelConfig {
        self.model.clone()
    }

    async fn stream(
        &self,
        model_config: &ModelConfig,
        session_id: &str,
        system: &str,
        messages: &[Message],
        tools: &[Tool],
    ) -> Result<MessageStream, ProviderError> {
        let mut payload = create_codex_request(model_config, system, messages, tools)
            .map_err(|e| ProviderError::ExecutionError(e.to_string()))?;
        payload["stream"] = serde_json::Value::Bool(true);

        let response = self
            .with_retry(|| async {
                let payload_clone = payload.clone();
                self.post_streaming(Some(session_id), &payload_clone).await
            })
            .await?;

        let stream = response.bytes_stream().map_err(io::Error::other);

        Ok(Box::pin(try_stream! {
            let stream_reader = StreamReader::new(stream);
            let framed = FramedRead::new(stream_reader, LinesCodec::new()).map_err(anyhow::Error::from);

            let message_stream = responses_api_to_streaming_message(framed);
            pin!(message_stream);
            while let Some(message) = message_stream.next().await {
                let (message, usage) = message.map_err(|e| ProviderError::RequestFailed(format!("Stream decode error: {}", e)))?;
                yield (message, usage);
            }
        }))
    }

    async fn configure_oauth(&self) -> Result<(), ProviderError> {
        let previous_token = self.auth_provider.cache.load();
        self.auth_provider.clear_cached_tokens();

        let result = perform_oauth_flow(self.auth_provider.state.as_ref())
            .await
            .and_then(|token_data| self.auth_provider.cache.save(&token_data));

        if let Err(e) = result {
            if let Some(previous_token) = previous_token.as_ref() {
                if self.auth_provider.cache.load().is_none() {
                    let _ = self.auth_provider.cache.save(previous_token);
                }
            }
            return Err(ProviderError::Authentication(format!(
                "OAuth flow failed: {}",
                e
            )));
        }

        Ok(())
    }

    async fn fetch_supported_models(&self) -> Result<Vec<String>, ProviderError> {
        Ok(known_model_names().into_iter().map(String::from).collect())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::conversation::message::Message;
    use goose_test_support::TEST_IMAGE_B64;
    use jsonwebtoken::{Algorithm, EncodingKey, Header};
    use rmcp::model::{CallToolRequestParams, CallToolResult, Content, ErrorCode, ErrorData};
    use rmcp::object;
    use test_case::test_case;
    use wiremock::matchers::{body_string_contains, method, path};
    use wiremock::{Mock, MockServer, ResponseTemplate};

    fn input_kinds(payload: &Value) -> Vec<String> {
        payload["input"]
            .as_array()
            .map(|items| {
                items
                    .iter()
                    .map(|item| {
                        if let Some(role) = item.get("role").and_then(|r| r.as_str()) {
                            format!("message:{role}")
                        } else {
                            item.get("type")
                                .and_then(|t| t.as_str())
                                .unwrap_or("unknown")
                                .to_string()
                        }
                    })
                    .collect()
            })
            .unwrap_or_default()
    }

    #[test]
    #[serial_test::serial]
    fn inventory_configured_uses_oauth_token_cache() {
        let root = tempfile::tempdir().unwrap();
        let root_path = root.path().to_string_lossy().to_string();
        let _guard = env_lock::lock_env([("GOOSE_PATH_ROOT", Some(root_path.as_str()))]);

        TokenCache::new().clear();
        assert!(!ChatGptCodexProvider::inventory_configured());

        TokenCache::new()
            .save(&TokenData {
                access_token: "access".to_string(),
                refresh_token: "refresh".to_string(),
                id_token: None,
                expires_at: Utc::now() + chrono::Duration::hours(1),
                account_id: Some("account".to_string()),
            })
            .unwrap();

        assert!(ChatGptCodexProvider::inventory_configured());
    }

    #[test_case(
        vec![
            Message::user().with_text("user text"),
            Message::assistant().with_text("assistant prelude").with_tool_request(
                "call-1",
                Ok(CallToolRequestParams::new("tool_name").with_arguments(object!({"param": "value"}))),
            ),
            Message::user().with_tool_response(
                "call-1",
                Ok(CallToolResult::success(vec![Content::text("tool output")])),
            ),
            Message::assistant().with_text("assistant follow-up"),
        ],
        vec![
            "message:user".to_string(),
            "message:assistant".to_string(),
            "function_call".to_string(),
            "function_call_output".to_string(),
            "message:assistant".to_string(),
        ];
        "preserves order when assistant includes text"
    )]
    #[test_case(
        vec![
            Message::user().with_text("user text"),
            Message::assistant().with_tool_request(
                "call-1",
                Ok(CallToolRequestParams::new("tool_name").with_arguments(object!({"param": "value"}))),
            ),
            Message::user().with_tool_response(
                "call-1",
                Ok(CallToolResult::success(vec![Content::text("tool output")])),
            ),
            Message::assistant().with_text("assistant follow-up"),
        ],
        vec![
            "message:user".to_string(),
            "function_call".to_string(),
            "function_call_output".to_string(),
            "message:assistant".to_string(),
        ];
        "skips empty assistant message and preserves tool order"
    )]
    #[test_case(
        vec![
            Message::user().with_text("user text"),
            Message::assistant().with_tool_request(
                "call-1",
                Ok(CallToolRequestParams::new("tool_name").with_arguments(object!({"param": "value"}))),
            ),
            Message::user().with_tool_response(
                "call-1",
                Err(ErrorData::new(ErrorCode::INTERNAL_ERROR, "boom", None)),
            ),
        ],
        vec![
            "message:user".to_string(),
            "function_call".to_string(),
            "function_call_output".to_string(),
        ];
        "includes tool error output"
    )]
    #[test_case(
        vec![
            Message::user()
                .with_text("describe this")
                .with_image(TEST_IMAGE_B64, "image/png"),
        ],
        vec![
            "message:user".to_string(),
        ];
        "image content included in user message"
    )]
    fn test_codex_input_order(messages: Vec<Message>, expected: Vec<String>) {
        let items = build_input_items(&messages).unwrap();
        let payload = json!({ "input": items });
        let kinds = input_kinds(&payload);
        assert_eq!(kinds, expected);
    }

    #[test]
    fn test_image_url_format() {
        let messages = vec![Message::user().with_image(TEST_IMAGE_B64, "image/png")];
        let items = build_input_items(&messages).unwrap();
        // The image is inside the content array of the user message
        let content = items[0]["content"].as_array().unwrap();
        let image_item = &content[0];
        assert_eq!(image_item["type"], "input_image");
        let url = image_item["image_url"].as_str().unwrap();
        assert!(
            url.starts_with("data:image/png;base64,"),
            "image_url should start with data:image/png;base64, but was: {}",
            url
        );
    }

    #[test]
    fn test_create_codex_request_reasoning_effort_from_unified_thinking() {
        let mut params = std::collections::HashMap::new();
        params.insert("thinking_effort".to_string(), json!("max"));
        let mut config = ModelConfig::new("gpt-5.3-codex").unwrap();
        config.request_params = Some(params);

        let payload = create_codex_request(&config, "sys", &[], &[]).unwrap();
        assert_eq!(payload["reasoning"]["effort"], "xhigh");
        assert!(payload.get("reasoning_effort").is_none());
    }

    #[test]
    fn test_create_codex_request_caps_unified_thinking_to_supported_level() {
        let mut params = std::collections::HashMap::new();
        params.insert("thinking_effort".to_string(), json!("max"));
        let mut config = ModelConfig::new("unknown-model").unwrap();
        config.request_params = Some(params);

        let payload = create_codex_request(&config, "sys", &[], &[]).unwrap();
        assert_eq!(payload["reasoning"]["effort"], "high");
        assert!(payload.get("reasoning_effort").is_none());
    }

    #[test]
    fn test_create_codex_request_off_omits_reasoning_for_codex_models() {
        let mut params = std::collections::HashMap::new();
        params.insert("thinking_effort".to_string(), json!("off"));
        let mut config = ModelConfig::new("gpt-5.2-codex").unwrap();
        config.request_params = Some(params);

        let payload = create_codex_request(&config, "sys", &[], &[]).unwrap();
        assert!(payload.get("reasoning").is_none());
        assert!(payload.get("reasoning_effort").is_none());
    }

    #[test_case(
        JwtClaims {
            chatgpt_account_id: Some("account-1".to_string()),
            auth_claims: None,
            organizations: None,
        },
        Some("account-1".to_string());
        "uses top-level account id"
    )]
    #[test_case(
        JwtClaims {
            chatgpt_account_id: None,
            auth_claims: Some(AuthClaims {
                chatgpt_account_id: Some("account-2".to_string()),
            }),
            organizations: None,
        },
        Some("account-2".to_string());
        "uses auth claims account id"
    )]
    #[test_case(
        JwtClaims {
            chatgpt_account_id: None,
            auth_claims: None,
            organizations: Some(vec![OrgInfo {
                id: "org-1".to_string(),
            }]),
        },
        Some("org-1".to_string());
        "falls back to first organization"
    )]
    fn test_account_id_from_claims(claims: JwtClaims, expected: Option<String>) {
        assert_eq!(account_id_from_claims(&claims), expected);
    }

    #[tokio::test]
    async fn test_exchange_code_for_tokens() {
        let server = MockServer::start().await;
        Mock::given(method("POST"))
            .and(path("/oauth/token"))
            .and(body_string_contains("grant_type=authorization_code"))
            .and(body_string_contains("code=code-123"))
            .and(body_string_contains(
                "redirect_uri=http%3A%2F%2Flocalhost%2Fcallback",
            ))
            .and(body_string_contains("code_verifier=verifier-123"))
            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
                "access_token": "access-1",
                "refresh_token": "refresh-1",
                "id_token": "id-1",
                "expires_in": 3600
            })))
            .mount(&server)
            .await;

        let pkce = PkceChallenge {
            verifier: "verifier-123".to_string(),
            challenge: "challenge-123".to_string(),
        };
        let tokens = exchange_code_for_tokens_with_issuer(
            &server.uri(),
            "code-123",
            "http://localhost/callback",
            &pkce,
        )
        .await
        .unwrap();

        assert_eq!(tokens.access_token, "access-1");
        assert_eq!(tokens.refresh_token, "refresh-1");
        assert_eq!(tokens.id_token.as_deref(), Some("id-1"));
        assert_eq!(tokens.expires_in, Some(3600));
    }

    #[tokio::test]
    async fn test_refresh_access_token() {
        let server = MockServer::start().await;
        Mock::given(method("POST"))
            .and(path("/oauth/token"))
            .and(body_string_contains("grant_type=refresh_token"))
            .and(body_string_contains("refresh_token=refresh-123"))
            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
                "access_token": "access-2",
                "refresh_token": "refresh-2",
                "id_token": "id-2",
                "expires_in": 1800
            })))
            .mount(&server)
            .await;

        let tokens = refresh_access_token_with_issuer(&server.uri(), "refresh-123")
            .await
            .unwrap();

        assert_eq!(tokens.access_token, "access-2");
        assert_eq!(tokens.refresh_token, "refresh-2");
        assert_eq!(tokens.id_token.as_deref(), Some("id-2"));
        assert_eq!(tokens.expires_in, Some(1800));
    }

    #[derive(Serialize)]
    struct TestClaims {
        exp: usize,
        chatgpt_account_id: Option<String>,
    }

    #[tokio::test]
    async fn test_parse_jwt_claims_verified_with_issuer() {
        let server = MockServer::start().await;
        let jwks_uri = format!("{}/jwks", server.uri());
        Mock::given(method("GET"))
            .and(path("/.well-known/openid-configuration"))
            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
                "jwks_uri": jwks_uri
            })))
            .mount(&server)
            .await;

        let secret = "test-secret";
        let key = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(secret);
        Mock::given(method("GET"))
            .and(path("/jwks"))
            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
                "keys": [{
                    "kty": "oct",
                    "alg": "HS256",
                    "kid": "test-kid",
                    "k": key
                }]
            })))
            .mount(&server)
            .await;

        let mut header = Header::new(Algorithm::HS256);
        header.kid = Some("test-kid".to_string());

        let claims = TestClaims {
            exp: (Utc::now() + chrono::Duration::seconds(60)).timestamp() as usize,
            chatgpt_account_id: Some("account-1".to_string()),
        };
        let token = jsonwebtoken::encode(
            &header,
            &claims,
            &EncodingKey::from_secret(secret.as_bytes()),
        )
        .unwrap();

        let jwks = fetch_jwks_for(&server.uri()).await.unwrap();
        let claims = parse_jwt_claims_with_jwks(&token, &jwks).unwrap();

        assert_eq!(claims.chatgpt_account_id.as_deref(), Some("account-1"));
    }

    #[test_case("unknown-model", &["medium", "high"]; "unknown model gets default reasoning levels")]
    fn test_reasoning_levels_for_model(model: &str, expected: &[&str]) {
        assert_eq!(reasoning_levels_for_model(model), expected);
    }

    #[test]
    fn test_gpt53_preamble_injected() {
        let model = ModelConfig::new("gpt-5.3-codex").unwrap();
        let payload = create_codex_request(&model, "system prompt", &[], &[]).unwrap();
        let instructions = payload["instructions"].as_str().unwrap();
        assert!(instructions.contains(GPT_53_CODEX_TOOL_PREAMBLE));
        assert!(instructions.contains("system prompt"));
    }

    #[test]
    fn test_other_models_no_preamble() {
        let model = ModelConfig::new("gpt-5.4").unwrap();
        let payload = create_codex_request(&model, "system prompt", &[], &[]).unwrap();
        let instructions = payload["instructions"].as_str().unwrap();
        assert_eq!(instructions, "system prompt");
    }
}
