use std::sync::Arc;

use reqwest::cookie::{CookieStore, Jar};
use reqwest::header::HeaderValue;

#[derive(Debug, Default)]
pub(crate) struct ChatGptCloudflareCookieStore {
    jar: Jar,
}

impl ChatGptCloudflareCookieStore {
    /// Current allowed Cloudflare `Cookie` header for `url`, if any. Lets the
    /// WebSocket passthrough (#597) replay the same `cf_clearance`/`__cf_bm`
    /// clearance the reqwest rail accumulated, so the upstream handshake to
    /// chatgpt.com is not bounced by Cloudflare.
    pub(super) fn cookie_header(&self, url: &reqwest::Url) -> Option<HeaderValue> {
        self.cookies(url)
    }
}

impl CookieStore for ChatGptCloudflareCookieStore {
    fn set_cookies(
        &self,
        cookie_headers: &mut dyn Iterator<Item = &HeaderValue>,
        url: &reqwest::Url,
    ) {
        if !is_chatgpt_cookie_url(url) {
            return;
        }

        let mut cloudflare_cookie_headers =
            cookie_headers.filter(|header| is_allowed_cloudflare_set_cookie_header(header));
        self.jar.set_cookies(&mut cloudflare_cookie_headers, url);
    }

    fn cookies(&self, url: &reqwest::Url) -> Option<HeaderValue> {
        if is_chatgpt_cookie_url(url) {
            self.jar
                .cookies(url)
                .and_then(|cookies| only_cloudflare_cookies(&cookies))
        } else {
            None
        }
    }
}

/// A fresh shared Cloudflare cookie store. Held by `ProxyState` so both the
/// reqwest rail and the WebSocket passthrough (#597) read/write the same jar.
pub(super) fn shared_chatgpt_cloudflare_cookie_store() -> Arc<ChatGptCloudflareCookieStore> {
    Arc::new(ChatGptCloudflareCookieStore::default())
}

pub(super) fn with_chatgpt_cloudflare_cookie_store(
    builder: reqwest::ClientBuilder,
    store: Arc<ChatGptCloudflareCookieStore>,
) -> reqwest::ClientBuilder {
    builder.cookie_provider(store)
}

fn is_chatgpt_cookie_url(url: &reqwest::Url) -> bool {
    if url.scheme() != "https" {
        return false;
    }
    let Some(host) = url.host_str() else {
        return false;
    };
    is_allowed_chatgpt_host(host)
}

fn is_allowed_chatgpt_host(host: &str) -> bool {
    const EXACT_HOSTS: &[&str] = &["chatgpt.com", "chat.openai.com", "chatgpt-staging.com"];
    const SUBDOMAIN_SUFFIXES: &[&str] = &[".chatgpt.com", ".chatgpt-staging.com"];

    EXACT_HOSTS.contains(&host)
        || SUBDOMAIN_SUFFIXES
            .iter()
            .any(|suffix| host.ends_with(suffix))
}

fn is_allowed_cloudflare_set_cookie_header(header: &HeaderValue) -> bool {
    header
        .to_str()
        .ok()
        .and_then(set_cookie_name)
        .is_some_and(is_allowed_cloudflare_cookie_name)
}

fn set_cookie_name(header: &str) -> Option<&str> {
    let (name, _) = header.split_once('=')?;
    let name = name.trim();
    (!name.is_empty()).then_some(name)
}

fn only_cloudflare_cookies(header: &HeaderValue) -> Option<HeaderValue> {
    let header = header.to_str().ok()?;
    let cookies = header
        .split(';')
        .filter_map(|cookie| {
            let cookie = cookie.trim();
            let name = cookie.split_once('=')?.0.trim();
            is_allowed_cloudflare_cookie_name(name).then_some(cookie)
        })
        .collect::<Vec<_>>()
        .join("; ");

    if cookies.is_empty() {
        None
    } else {
        HeaderValue::from_str(&cookies).ok()
    }
}

fn is_allowed_cloudflare_cookie_name(name: &str) -> bool {
    matches!(
        name,
        "__cf_bm"
            | "__cflb"
            | "__cfruid"
            | "__cfseq"
            | "__cfwaitingroom"
            | "_cfuvid"
            | "cf_clearance"
            | "cf_ob_info"
            | "cf_use_ob"
    ) || name.starts_with("cf_chl_")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn stores_only_cloudflare_cookies_for_chatgpt_hosts() {
        let store = ChatGptCloudflareCookieStore::default();
        let url = reqwest::Url::parse("https://chatgpt.com/backend-api/ps/mcp").unwrap();
        let cf = HeaderValue::from_static("cf_clearance=ok; Path=/; Secure; HttpOnly");
        let account = HeaderValue::from_static("__Secure-next-auth.session-token=secret; Path=/");

        store.set_cookies(&mut [&cf, &account].into_iter(), &url);

        let cookies = store.cookies(&url).unwrap();
        assert_eq!(cookies.to_str().unwrap(), "cf_clearance=ok");
    }

    #[test]
    fn rejects_non_chatgpt_cookie_urls() {
        let store = ChatGptCloudflareCookieStore::default();
        let url = reqwest::Url::parse("https://api.openai.com/v1/responses").unwrap();
        let cf = HeaderValue::from_static("cf_clearance=ok; Path=/; Secure; HttpOnly");

        store.set_cookies(&mut std::iter::once(&cf), &url);

        assert!(store.cookies(&url).is_none());
    }

    #[test]
    fn rejects_plain_http_chatgpt_cookie_urls() {
        let store = ChatGptCloudflareCookieStore::default();
        let http_url = reqwest::Url::parse("http://chatgpt.com/backend-api/ps/mcp").unwrap();
        let https_url = reqwest::Url::parse("https://chatgpt.com/backend-api/ps/mcp").unwrap();
        let cf = HeaderValue::from_static("cf_clearance=ok; Path=/; Secure; HttpOnly");

        store.set_cookies(&mut std::iter::once(&cf), &http_url);

        assert!(store.cookies(&https_url).is_none());
    }
}
