use crate::api::MyceliumClient;
use crate::error::AppResult;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DefaultAction {
    pub accept: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SourceBody {
    pub subnet: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct ForwardBody {
    pub socket_path: String,
}

impl MyceliumClient {
    pub async fn topics_default_get(&self) -> AppResult<DefaultAction> {
        let resp = self
            .http()
            .get(self.url("/messages/topics/default"))
            .send()
            .await?;
        Self::parse(resp).await
    }

    pub async fn topics_default_set(&self, action: &DefaultAction) -> AppResult<()> {
        let resp = self
            .http()
            .put(self.url("/messages/topics/default"))
            .json(action)
            .send()
            .await?;
        Self::check_status(resp).await
    }

    pub async fn topics_list(&self) -> AppResult<Vec<String>> {
        let resp = self.http().get(self.url("/messages/topics")).send().await?;
        Self::parse(resp).await
    }

    /// The daemon expects the raw base64 topic as the request body, not
    /// wrapped in JSON.
    pub async fn topic_add(&self, topic_b64: &str) -> AppResult<()> {
        let resp = self
            .http()
            .post(self.url("/messages/topics"))
            .header(reqwest::header::CONTENT_TYPE, "text/plain")
            .body(topic_b64.to_string())
            .send()
            .await?;
        Self::check_status(resp).await
    }

    pub async fn topic_remove(&self, topic_b64: &str) -> AppResult<()> {
        let encoded = encode_segment(topic_b64);
        let resp = self
            .http()
            .delete(self.url(&format!("/messages/topics/{encoded}")))
            .send()
            .await?;
        Self::check_status(resp).await
    }

    pub async fn topic_sources_list(&self, topic_b64: &str) -> AppResult<Vec<String>> {
        let encoded = encode_segment(topic_b64);
        let resp = self
            .http()
            .get(self.url(&format!("/messages/topics/{encoded}/sources")))
            .send()
            .await?;
        Self::parse(resp).await
    }

    pub async fn topic_source_add(&self, topic_b64: &str, subnet: &str) -> AppResult<()> {
        let encoded = encode_segment(topic_b64);
        let resp = self
            .http()
            .post(self.url(&format!("/messages/topics/{encoded}/sources")))
            .json(&SourceBody {
                subnet: subnet.to_string(),
            })
            .send()
            .await?;
        Self::check_status(resp).await
    }

    pub async fn topic_source_remove(&self, topic_b64: &str, subnet: &str) -> AppResult<()> {
        let topic = encode_segment(topic_b64);
        let sub = encode_segment(subnet);
        let resp = self
            .http()
            .delete(self.url(&format!("/messages/topics/{topic}/sources/{sub}")))
            .send()
            .await?;
        Self::check_status(resp).await
    }

    /// Returns Ok(None) if no forward is configured (the daemon yields
    /// `null`).
    pub async fn topic_forward_get(&self, topic_b64: &str) -> AppResult<Option<String>> {
        let encoded = encode_segment(topic_b64);
        let resp = self
            .http()
            .get(self.url(&format!("/messages/topics/{encoded}/forward")))
            .send()
            .await?;
        let v: serde_json::Value = Self::parse(resp).await?;
        Ok(match v {
            serde_json::Value::Null => None,
            serde_json::Value::String(s) => Some(s),
            other => Some(other.to_string()),
        })
    }

    pub async fn topic_forward_set(&self, topic_b64: &str, socket_path: &str) -> AppResult<()> {
        let encoded = encode_segment(topic_b64);
        let resp = self
            .http()
            .put(self.url(&format!("/messages/topics/{encoded}/forward")))
            .json(&ForwardBody {
                socket_path: socket_path.to_string(),
            })
            .send()
            .await?;
        Self::check_status(resp).await
    }

    pub async fn topic_forward_remove(&self, topic_b64: &str) -> AppResult<()> {
        let encoded = encode_segment(topic_b64);
        let resp = self
            .http()
            .delete(self.url(&format!("/messages/topics/{encoded}/forward")))
            .send()
            .await?;
        Self::check_status(resp).await
    }
}

/// Percent-encodes path segments. Topics are base64 (which contains `/` and
/// `+` and may end with `=`); subnets carry `/` and `:`. We encode anything
/// outside the unreserved set.
fn encode_segment(s: &str) -> String {
    let mut out = String::with_capacity(s.len());
    for b in s.bytes() {
        match b {
            b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
                out.push(b as char)
            }
            _ => out.push_str(&format!("%{:02X}", b)),
        }
    }
    out
}
