//! DB-shaped HTTP client profile (mirrors `http_request_profiles`).

use anyhow::{anyhow, Result};
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use url::Url;

fn default_timeout_secs() -> u64 {
    30
}

fn default_bool_true() -> bool {
    true
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HttpClientProfile {
    pub name: String,
    #[serde(default)]
    pub proxy_url: Option<String>,
    #[serde(default = "default_timeout_secs")]
    pub timeout_secs: u64,
    #[serde(default)]
    pub connect_timeout_secs: Option<u64>,
    #[serde(default = "default_bool_true")]
    pub follow_redirects: bool,
    #[serde(default)]
    pub cookie_store: bool,
    #[serde(default)]
    pub default_headers: Option<Map<String, Value>>,
    #[serde(default)]
    pub user_agent: Option<String>,
    #[serde(default)]
    pub danger_accept_invalid_certs: bool,
    #[serde(default = "default_bool_true")]
    pub enabled: bool,
}

impl HttpClientProfile {
    /// Row for `athena_rs` insert into `http_request_profiles`.
    pub fn to_insert_payload(&self, set_updated_at: bool) -> Value {
        let headers = self
            .default_headers
            .as_ref()
            .map(|m| Value::Object(m.clone()))
            .unwrap_or(Value::Null);
        let mut v = serde_json::json!({
            "name": self.name,
            "proxy_url": self.proxy_url,
            "timeout_secs": self.timeout_secs as i64,
            "connect_timeout_secs": self.connect_timeout_secs.map(|x| x as i64),
            "follow_redirects": self.follow_redirects,
            "cookie_store": self.cookie_store,
            "default_headers": headers,
            "user_agent": self.user_agent,
            "danger_accept_invalid_certs": self.danger_accept_invalid_certs,
            "enabled": self.enabled,
        });
        if set_updated_at {
            if let Value::Object(ref mut o) = v {
                o.insert(
                    "updated_at".into(),
                    Value::String(chrono::Utc::now().to_rfc3339()),
                );
            }
        }
        v
    }

    pub fn to_update_payload(&self) -> Value {
        let headers = self
            .default_headers
            .as_ref()
            .map(|m| Value::Object(m.clone()))
            .unwrap_or(Value::Null);
        serde_json::json!({
            "proxy_url": self.proxy_url,
            "timeout_secs": self.timeout_secs as i64,
            "connect_timeout_secs": self.connect_timeout_secs.map(|x| x as i64),
            "follow_redirects": self.follow_redirects,
            "cookie_store": self.cookie_store,
            "default_headers": headers,
            "user_agent": self.user_agent,
            "danger_accept_invalid_certs": self.danger_accept_invalid_certs,
            "enabled": self.enabled,
            "updated_at": chrono::Utc::now().to_rfc3339(),
        })
    }

    /// Parse gateway fetch row (`serde_json::Value` object).
    pub fn from_athena_row(row: &Value) -> Result<Self> {
        let o = row.as_object().ok_or_else(|| anyhow!("profile row not an object"))?;
        let gv = |k: &str| o.get(k).cloned();
        let name = gv("name")
            .and_then(|v| v.as_str().map(String::from))
            .ok_or_else(|| anyhow!("missing name"))?;
        let proxy_url = gv("proxy_url").and_then(|v| {
            if v.is_null() {
                None
            } else {
                v.as_str().map(String::from)
            }
        });
        let timeout_secs = gv("timeout_secs")
            .and_then(|v| v.as_u64().or_else(|| v.as_i64().map(|i| i as u64)))
            .unwrap_or(30);
        let connect_timeout_secs = gv("connect_timeout_secs").and_then(|v| {
            if v.is_null() {
                None
            } else {
                v.as_u64().or_else(|| v.as_i64().map(|i| i.max(0) as u64))
            }
        });
        let follow_redirects = gv("follow_redirects")
            .and_then(|v| v.as_bool())
            .unwrap_or(true);
        let cookie_store = gv("cookie_store").and_then(|v| v.as_bool()).unwrap_or(false);
        let default_headers = gv("default_headers").and_then(|v| {
            if v.is_null() {
                None
            } else {
                v.as_object().cloned()
            }
        });
        let user_agent = gv("user_agent").and_then(|v| {
            if v.is_null() {
                None
            } else {
                v.as_str().map(String::from)
            }
        });
        let danger_accept_invalid_certs = gv("danger_accept_invalid_certs")
            .and_then(|v| v.as_bool())
            .unwrap_or(false);
        let enabled = gv("enabled").and_then(|v| v.as_bool()).unwrap_or(true);

        Ok(Self {
            name,
            proxy_url,
            timeout_secs,
            connect_timeout_secs,
            follow_redirects,
            cookie_store,
            default_headers,
            user_agent,
            danger_accept_invalid_certs,
            enabled,
        })
    }

    /// Response-safe clone with masked `proxy_url` userinfo.
    pub fn masked_for_api(&self) -> Self {
        let mut m = self.clone();
        if let Some(ref u) = m.proxy_url {
            m.proxy_url = Some(mask_proxy_credentials(u));
        }
        m
    }

    pub fn validate_proxy_url(proxy: Option<&str>) -> Result<()> {
        let Some(s) = proxy.filter(|x| !x.is_empty()) else {
            return Ok(());
        };
        let u = Url::parse(s).map_err(|e| anyhow!("invalid proxy_url: {}", e))?;
        let scheme = u.scheme();
        match scheme {
            "http" | "https" | "socks5" | "socks5h" => Ok(()),
            _ => Err(anyhow!(
                "proxy_url scheme must be http, https, socks5, or socks5h (got {})",
                scheme
            )),
        }
    }
}

pub fn mask_proxy_credentials(url_str: &str) -> String {
    let Ok(mut u) = Url::parse(url_str) else {
        return url_str.to_string();
    };
    if u.username().is_empty() && u.password().is_none() {
        return url_str.to_string();
    }
    let _ = u.set_username("");
    let _ = u.set_password(None);
    u.to_string()
}
