//! Thin blocking client for the Sprouts API.

use serde::Deserialize;

#[derive(Debug, Deserialize)]
pub struct User {
    pub id: String,
    pub email: Option<String>,
    pub wallet_address: Option<String>,
}

#[derive(Debug, Deserialize)]
pub struct Session {
    pub token: String,
    pub refresh_token: String,
    pub user: User,
}

#[derive(Debug)]
pub enum Error {
    /// The token was missing/expired (HTTP 401).
    Unauthorized,
    Other(String),
}

impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Error::Unauthorized => write!(f, "unauthorized"),
            Error::Other(msg) => write!(f, "{msg}"),
        }
    }
}

impl std::error::Error for Error {}

fn client() -> reqwest::blocking::Client {
    reqwest::blocking::Client::new()
}

/// Exchange a Privy access token for a Sprouts session (login).
pub fn verify(api_url: &str, privy_token: &str) -> Result<Session, Error> {
    let res = client()
        .post(format!("{api_url}/auth/verify"))
        .bearer_auth(privy_token)
        .send()
        .map_err(|e| Error::Other(e.to_string()))?;
    parse(res)
}

/// Rotate a session using a refresh token.
pub fn refresh(api_url: &str, refresh_token: &str) -> Result<Session, Error> {
    let res = client()
        .post(format!("{api_url}/auth/refresh"))
        .json(&serde_json::json!({ "refresh_token": refresh_token }))
        .send()
        .map_err(|e| Error::Other(e.to_string()))?;
    parse(res)
}

/// Fetch the current user (`GET /auth/me`).
pub fn me(api_url: &str, access_token: &str) -> Result<User, Error> {
    let res = client()
        .get(format!("{api_url}/auth/me"))
        .bearer_auth(access_token)
        .send()
        .map_err(|e| Error::Other(e.to_string()))?;
    parse(res)
}

/// A sprout reference returned by create/list.
#[derive(Debug, Deserialize)]
pub struct SproutRef {
    pub id: String,
    pub name: String,
}

/// Create a sprout (idempotent — returns the existing one if the name is taken).
pub fn create_sprout(api_url: &str, token: &str, name: &str) -> Result<SproutRef, Error> {
    let res = client()
        .post(format!("{api_url}/sprouts"))
        .bearer_auth(token)
        .json(&serde_json::json!({ "name": name }))
        .send()
        .map_err(|e| Error::Other(e.to_string()))?;
    parse(res)
}

/// List the caller's sprouts.
pub fn list_sprouts(api_url: &str, token: &str) -> Result<Vec<SproutRef>, Error> {
    let res = client()
        .get(format!("{api_url}/sprouts"))
        .bearer_auth(token)
        .send()
        .map_err(|e| Error::Other(e.to_string()))?;
    parse(res)
}

/// The webhook URLs a deploy registered.
#[derive(Debug, Deserialize)]
pub struct Webhook {
    pub trigger: String,
    pub path: String,
}

/// The result of a deploy.
#[derive(Debug, Deserialize)]
pub struct DeployResult {
    pub version: i32,
    #[serde(default)]
    pub webhooks: Vec<Webhook>,
}

/// Upload a `.wasm` artifact + `sprout.toml` manifest as a new version.
pub fn deploy(
    api_url: &str,
    token: &str,
    sprout_id: &str,
    wasm: &[u8],
    manifest: &str,
) -> Result<DeployResult, Error> {
    let wasm_part = reqwest::blocking::multipart::Part::bytes(wasm.to_vec())
        .file_name("sprout.wasm")
        .mime_str("application/wasm")
        .map_err(|e| Error::Other(e.to_string()))?;
    let form = reqwest::blocking::multipart::Form::new()
        .part("wasm", wasm_part)
        .text("manifest", manifest.to_string());

    let res = client()
        .post(format!("{api_url}/sprouts/{sprout_id}/deploy"))
        .bearer_auth(token)
        .multipart(form)
        .send()
        .map_err(|e| Error::Other(e.to_string()))?;
    parse(res)
}

/// One execution row in a sprout's history.
#[derive(Debug, Deserialize)]
pub struct Execution {
    pub id: String,
    pub handler: String,
    pub status: String,
    pub trigger_name: Option<String>,
    pub started_at: String,
    pub error_message: Option<String>,
}

#[derive(Debug, Deserialize)]
pub struct ExecutionsResponse {
    pub executions: Vec<Execution>,
}

/// Fetch one execution's detail (including its captured `logs`).
pub fn execution(api_url: &str, token: &str, execution_id: &str) -> Result<serde_json::Value, Error> {
    let res = client()
        .get(format!("{api_url}/executions/{execution_id}"))
        .bearer_auth(token)
        .send()
        .map_err(|e| Error::Other(e.to_string()))?;
    parse(res)
}

/// Fetch a sprout's recent executions.
pub fn executions(api_url: &str, token: &str, sprout_id: &str) -> Result<ExecutionsResponse, Error> {
    let res = client()
        .get(format!("{api_url}/sprouts/{sprout_id}/executions"))
        .bearer_auth(token)
        .send()
        .map_err(|e| Error::Other(e.to_string()))?;
    parse(res)
}

/// Manually trigger a handler run.
pub fn run_sprout(
    api_url: &str,
    token: &str,
    sprout_id: &str,
    handler: &str,
    payload: serde_json::Value,
) -> Result<serde_json::Value, Error> {
    let res = client()
        .post(format!("{api_url}/sprouts/{sprout_id}/run"))
        .bearer_auth(token)
        .json(&serde_json::json!({ "handler": handler, "payload": payload }))
        .send()
        .map_err(|e| Error::Other(e.to_string()))?;
    parse(res)
}

/// Roll a sprout back to a prior version.
pub fn rollback(
    api_url: &str,
    token: &str,
    sprout_id: &str,
    version: i32,
) -> Result<serde_json::Value, Error> {
    let res = client()
        .post(format!("{api_url}/sprouts/{sprout_id}/rollback"))
        .bearer_auth(token)
        .json(&serde_json::json!({ "version": version }))
        .send()
        .map_err(|e| Error::Other(e.to_string()))?;
    parse(res)
}

fn parse<T: serde::de::DeserializeOwned>(res: reqwest::blocking::Response) -> Result<T, Error> {
    let status = res.status();
    if status == reqwest::StatusCode::UNAUTHORIZED {
        return Err(Error::Unauthorized);
    }
    if !status.is_success() {
        let body = res.text().unwrap_or_default();
        return Err(Error::Other(format!("{status}: {body}")));
    }
    res.json::<T>().map_err(|e| Error::Other(e.to_string()))
}
