//! Start and call the Node `better-auth` reference server (`compat-tests/reference-server`).
#![allow(dead_code)]

use serde_json::Value;
use std::sync::Mutex;
use std::time::Duration;
use tokio::sync::Mutex as TokioMutex;

pub const REFERENCE_PORT: u16 = 3100;

fn reference_transport_host() -> String {
    std::env::var("REFERENCE_HOST")
        .ok()
        .filter(|value| !value.trim().is_empty())
        .unwrap_or_else(|| "localhost".to_string())
}

pub fn reference_transport_origin() -> String {
    format!("http://{}:{REFERENCE_PORT}", reference_transport_host())
}

fn reference_request_origin() -> String {
    std::env::var("REFERENCE_ORIGIN")
        .ok()
        .filter(|value| !value.trim().is_empty())
        .unwrap_or_else(|| format!("http://localhost:{REFERENCE_PORT}"))
}

fn reference_api_base() -> String {
    format!("{}/api/auth", reference_transport_origin())
}

pub async fn reference_server_available() -> bool {
    let client = reqwest::Client::builder()
        .timeout(Duration::from_secs(2))
        .build()
        .unwrap();

    client
        .get(format!("{}/__health", reference_transport_origin()))
        .send()
        .await
        .map(|r| r.status().is_success())
        .unwrap_or(false)
}

pub fn try_start_reference_server() -> Option<std::process::Child> {
    let server_dir = std::path::Path::new("compat-tests/reference-server");

    if !server_dir.join("node_modules").exists() {
        eprintln!(
            "[dual-server] node_modules not found in {}, skipping. Run:\n\
             cd compat-tests/reference-server && npm install",
            server_dir.display()
        );
        return None;
    }

    let child = std::process::Command::new("node")
        .arg("server.mjs")
        .current_dir(server_dir)
        .env("PORT", REFERENCE_PORT.to_string())
        .stdout(std::process::Stdio::piped())
        .stderr(std::process::Stdio::piped())
        .spawn()
        .ok()?;

    Some(child)
}

pub static SERIAL: TokioMutex<()> = TokioMutex::const_new(());

static REF_SERVER: Mutex<Option<std::process::Child>> = Mutex::new(None);

pub async fn ensure_reference_server() -> bool {
    if reference_server_available().await {
        return true;
    }

    {
        let mut slot = REF_SERVER.lock().unwrap();
        if slot.is_none() {
            match try_start_reference_server() {
                Some(child) => {
                    *slot = Some(child);
                }
                None => return false,
            }
        }
    }

    for _ in 0..20 {
        tokio::time::sleep(Duration::from_millis(500)).await;
        if reference_server_available().await {
            return true;
        }
    }

    eprintln!(
        "[dual-server] Reference server did not become ready in 10s at {}, skipping.",
        reference_transport_origin()
    );
    false
}

#[derive(Debug, Clone)]
pub struct SessionCookie {
    pub name: String,
    pub value: String,
}

pub struct RefClient {
    client: reqwest::Client,
    pub session_cookie: Option<SessionCookie>,
}

impl RefClient {
    pub fn new() -> Self {
        Self {
            client: reqwest::Client::builder()
                .redirect(reqwest::redirect::Policy::none())
                .timeout(Duration::from_secs(10))
                .build()
                .unwrap(),
            session_cookie: None,
        }
    }

    fn apply_headers(
        &self,
        mut req: reqwest::RequestBuilder,
        cookie_override: Option<&SessionCookie>,
    ) -> reqwest::RequestBuilder {
        req = req.header("origin", reference_request_origin());
        let session_cookie = cookie_override.or(self.session_cookie.as_ref());
        if let Some(cookie) = session_cookie {
            req = req.header("cookie", format!("{}={}", cookie.name, cookie.value));
        }
        req
    }

    fn capture_cookie(&mut self, resp: &reqwest::Response) {
        for value in resp.headers().get_all("set-cookie") {
            if let Ok(s) = value.to_str() {
                for cookie_name in ["better-auth.session_token", "athena-auth.session_token"] {
                    let prefix = format!("{cookie_name}=");
                    if let Some(rest) = s.strip_prefix(&prefix) {
                        let token = rest.split(';').next().unwrap_or(rest);
                        self.session_cookie = Some(SessionCookie {
                            name: cookie_name.to_string(),
                            value: token.to_string(),
                        });
                        return;
                    }
                }
            }
        }
    }

    pub fn current_session_cookie(&self) -> Option<SessionCookie> {
        self.session_cookie.clone()
    }

    pub async fn post(&mut self, path: &str, body: &Value) -> Result<(u16, Value), String> {
        let url = format!("{}{}", reference_api_base(), path);
        let req = self.apply_headers(
            self.client
                .post(&url)
                .json(body)
                .header("content-type", "application/json"),
            None,
        );
        let resp = req.send().await.map_err(|e| format!("POST {url}: {e}"))?;
        self.capture_cookie(&resp);
        let status = resp.status().as_u16();
        let json: Value = resp
            .json()
            .await
            .unwrap_or_else(|_| Value::String("non-json".into()));
        Ok((status, json))
    }

    pub async fn post_bearer(
        &self,
        path: &str,
        body: &Value,
        token: &str,
    ) -> Result<(u16, Value), String> {
        let url = format!("{}{}", reference_api_base(), path);
        let req = self.apply_headers(
            self.client
                .post(&url)
                .json(body)
                .header("content-type", "application/json")
                .header("authorization", format!("Bearer {token}")),
            None,
        );
        let resp = req.send().await.map_err(|e| format!("POST {url}: {e}"))?;
        let status = resp.status().as_u16();
        let json: Value = resp
            .json()
            .await
            .unwrap_or_else(|_| Value::String("non-json".into()));
        Ok((status, json))
    }

    pub async fn post_with_cookie(
        &self,
        path: &str,
        body: &Value,
        cookie: &SessionCookie,
    ) -> Result<(u16, Value), String> {
        let url = format!("{}{}", reference_api_base(), path);
        let req = self.apply_headers(
            self.client
                .post(&url)
                .json(body)
                .header("content-type", "application/json"),
            Some(cookie),
        );
        let resp = req.send().await.map_err(|e| format!("POST {url}: {e}"))?;
        let status = resp.status().as_u16();
        let json: Value = resp
            .json()
            .await
            .unwrap_or_else(|_| Value::String("non-json".into()));
        Ok((status, json))
    }

    pub async fn get(&self, path: &str) -> Result<(u16, Value), String> {
        let url = format!("{}{}", reference_api_base(), path);
        let req = self.apply_headers(self.client.get(&url), None);
        let resp = req.send().await.map_err(|e| format!("GET {url}: {e}"))?;
        let status = resp.status().as_u16();
        let json: Value = resp
            .json()
            .await
            .unwrap_or_else(|_| Value::String("non-json".into()));
        Ok((status, json))
    }

    pub async fn get_bearer(&self, path: &str, token: &str) -> Result<(u16, Value), String> {
        let url = format!("{}{}", reference_api_base(), path);
        let req = self.apply_headers(
            self.client
                .get(&url)
                .header("authorization", format!("Bearer {token}")),
            None,
        );
        let resp = req.send().await.map_err(|e| format!("GET {url}: {e}"))?;
        let status = resp.status().as_u16();
        let json: Value = resp
            .json()
            .await
            .unwrap_or_else(|_| Value::String("non-json".into()));
        Ok((status, json))
    }

    pub async fn get_with_cookie(
        &self,
        path: &str,
        cookie: &SessionCookie,
    ) -> Result<(u16, Value), String> {
        let url = format!("{}{}", reference_api_base(), path);
        let req = self.apply_headers(self.client.get(&url), Some(cookie));
        let resp = req.send().await.map_err(|e| format!("GET {url}: {e}"))?;
        let status = resp.status().as_u16();
        let json: Value = resp
            .json()
            .await
            .unwrap_or_else(|_| Value::String("non-json".into()));
        Ok((status, json))
    }
}
