//! Wire transports — HTTP, i-poll, cap-bus.
//!
//! v0.2 ships:
//! - [`HttpTransport`] — direct POST/pull against the brain-api
//!   `/api/iddrop/inbox/{aint}` endpoints. Lowest overhead.
//! - [`IpollTransport`] — wraps the i-poll push/pull endpoints so iddrop
//!   bundles ride the same AI-to-AI messaging plane as cap-bus.
//!
//! Both implement [`super::Transport`] so the CLI can swap with a flag.

use super::Transport;
use crate::errors::{IddropError, Result};
use base64::{engine::general_purpose::STANDARD_NO_PAD, Engine as _};

/// HTTP transport that posts iddrop objects to the brain-api.
///
/// v0.1: deliberately a thin shim that does not yet land in production.
/// Endpoint paths are placeholders pending Codex/Root-AI coordination on
/// the brain-api side.
pub struct HttpTransport {
    /// Base URL of the iddrop endpoint, e.g.
    /// `"https://brein.jaspervandemeent.nl"`.
    pub base_url: String,
}

impl HttpTransport {
    /// Create a new HTTP transport against the given base URL.
    pub fn new(base_url: impl Into<String>) -> Self {
        Self {
            base_url: base_url.into(),
        }
    }
}

impl Transport for HttpTransport {
    fn name(&self) -> &'static str {
        "http"
    }
    fn send(&self, peer: &str, bytes: &[u8]) -> Result<()> {
        let url = format!("{}/api/iddrop/inbox/{}", self.base_url, peer);
        let resp = ureq::post(&url)
            .set("Content-Type", crate::PAYLOAD_CONTENT_TYPE)
            .send_bytes(bytes)
            .map_err(|e| IddropError::Transport(format!("HTTP POST {}: {}", url, e)))?;
        if !(200..300).contains(&resp.status()) {
            return Err(IddropError::Transport(format!(
                "HTTP POST {} returned {}",
                url,
                resp.status()
            )));
        }
        Ok(())
    }
    fn recv(&self, peer: &str) -> Result<Option<Vec<u8>>> {
        let url = format!("{}/api/iddrop/inbox/{}/pull", self.base_url, peer);
        let resp = ureq::get(&url)
            .call()
            .map_err(|e| IddropError::Transport(format!("HTTP GET {}: {}", url, e)))?;
        if resp.status() == 204 {
            return Ok(None);
        }
        let mut buf = Vec::new();
        std::io::Read::read_to_end(&mut resp.into_reader(), &mut buf)?;
        Ok(Some(buf))
    }
}

/// I-Poll wire transport — wraps iddrop bundles in the AI-to-AI
/// messaging plane. Payload is base64-encoded into the i-poll message
/// `content` field with a `poll_type` of `PUSH` and an iddrop marker
/// header so receivers can route accordingly.
///
/// Spec mapping:
///   POST {base}/api/ipoll/push   → with body
///     { from_agent, to_agent, content (= base64 envelope),
///       poll_type: "PUSH",
///       headers: { "x-iddrop": "v0.2", "content-type": iddrop }}
///   GET  {base}/api/ipoll/pull/{agent}?mark_read=true
///     → returns list of messages; we filter on x-iddrop header.
pub struct IpollTransport {
    /// Base URL of the brain-api.
    pub base_url: String,
    /// Local agent identifier used as `from_agent`. Default = `"iddrop-cli"`.
    pub from_agent: String,
}

impl IpollTransport {
    /// New transport against the given base URL.
    pub fn new(base_url: impl Into<String>) -> Self {
        Self {
            base_url: base_url.into(),
            from_agent: "iddrop-cli".to_string(),
        }
    }

    /// Override the `from_agent` identifier sent to i-poll.
    pub fn with_from_agent(mut self, agent: impl Into<String>) -> Self {
        self.from_agent = agent.into();
        self
    }
}

impl IpollTransport {
    /// Attempt to self-register the local agent with i-poll. Idempotent
    /// against the brain-api side (= "already registered" is treated as
    /// success). Returns the raw JSON response body for diagnostics.
    pub fn try_register(&self) -> Result<serde_json::Value> {
        let url = format!("{}/api/ipoll/register", self.base_url);
        let body = serde_json::json!({
            "agent_id": self.from_agent,
            "description": format!(
                "tibet-iddrop {} — Identity Drop and Acceptance Protocol sender",
                env!("CARGO_PKG_VERSION")
            ),
        });
        let resp = match ureq::post(&url).send_json(body) {
            Ok(r) => r,
            Err(ureq::Error::Status(409, r)) => r, // already registered = OK
            Err(e) => {
                return Err(IddropError::Transport(format!(
                    "ipoll register POST {}: {}",
                    url, e
                )))
            }
        };
        let value: serde_json::Value = resp
            .into_json()
            .map_err(|e| IddropError::Transport(format!("ipoll register parse: {}", e)))?;
        Ok(value)
    }
}

impl Transport for IpollTransport {
    fn name(&self) -> &'static str {
        "ipoll"
    }
    fn send(&self, peer: &str, bytes: &[u8]) -> Result<()> {
        let body = serde_json::json!({
            "from_agent": self.from_agent,
            "to_agent": peer.trim_end_matches(".aint"),
            "content": STANDARD_NO_PAD.encode(bytes),
            "poll_type": "PUSH",
            "headers": {
                "x-iddrop": "v0.3",
                "content-type": crate::PAYLOAD_CONTENT_TYPE,
            },
        });
        let url = format!("{}/api/ipoll/push", self.base_url);

        // First attempt
        let send_once = |body: &serde_json::Value| -> Result<u16> {
            match ureq::post(&url)
                .set("Content-Type", "application/json")
                .send_json(body.clone())
            {
                Ok(r) => Ok(r.status()),
                Err(ureq::Error::Status(s, _)) => Ok(s),
                Err(e) => Err(IddropError::Transport(format!(
                    "ipoll POST {}: {}",
                    url, e
                ))),
            }
        };
        let status = send_once(&body)?;
        if (200..300).contains(&status) {
            return Ok(());
        }

        // 401/403 → try self-register, then retry once.
        if status == 401 || status == 403 {
            let _ = self.try_register()?;
            let retry = send_once(&body)?;
            if (200..300).contains(&retry) {
                return Ok(());
            }
            return Err(IddropError::Transport(format!(
                "ipoll POST {} returned {} (after self-register retry)",
                url, retry
            )));
        }

        Err(IddropError::Transport(format!(
            "ipoll POST {} returned {}",
            url, status
        )))
    }
    fn recv(&self, peer: &str) -> Result<Option<Vec<u8>>> {
        let agent = peer.trim_end_matches(".aint");
        let url = format!("{}/api/ipoll/pull/{}?mark_read=true", self.base_url, agent);
        let resp = ureq::get(&url)
            .call()
            .map_err(|e| IddropError::Transport(format!("ipoll GET {}: {}", url, e)))?;
        let body: serde_json::Value = resp
            .into_json()
            .map_err(|e| IddropError::Transport(format!("ipoll parse: {}", e)))?;
        let messages = body
            .get("messages")
            .and_then(|m| m.as_array())
            .cloned()
            .unwrap_or_default();
        for msg in messages {
            let is_iddrop = msg
                .get("headers")
                .and_then(|h| h.get("x-iddrop"))
                .is_some();
            if !is_iddrop {
                continue;
            }
            if let Some(content_b64) = msg.get("content").and_then(|c| c.as_str()) {
                let decoded = STANDARD_NO_PAD
                    .decode(content_b64.trim_end_matches('='))
                    .map_err(|e| IddropError::Transport(format!("base64 decode: {}", e)))?;
                return Ok(Some(decoded));
            }
        }
        Ok(None)
    }
}
