use super::{actor, keys, types::ActorExt, CLIENT};
use activitystreams::activity::ActorAndObject;
use anyhow::anyhow;
use chrono::Utc;
use http_signature_normalization::Config;
use hyper::header::CONTENT_TYPE;
use log::{debug, error, info};
use openssl::{hash::MessageDigest, pkey::PKey, sha::Sha256, sign::Signer};
use reqwest::header::{ACCEPT, DATE, HOST};
use std::collections::BTreeMap;
use url::Url;

pub async fn fetch_actor(id: &str) -> anyhow::Result<ActorExt> {
    let actor = CLIENT
        .get(id)
        .header("Accept", "application/activity+json, application/json")
        .send()
        .await?
        .json::<ActorExt>()
        .await?;
    Ok(actor)
}

pub async fn sign_and_send<Kind: serde::Serialize>(
    activity: &ActorAndObject<Kind>,
    inbox: &str,
) -> anyhow::Result<()> {
    if std::env::var("SKIP_FEDERATE").is_ok() {
        info!("Skipping federation");
        return Ok(());
    }

    let inbox_url = Url::parse(inbox)?;

    let body = serde_json::to_string(activity)?;

    let mut headers = BTreeMap::new();

    let host = inbox_url
        .host_str()
        .ok_or(anyhow!("missing inbox host"))?
        .to_owned();
    headers.insert("host".into(), host.clone());
    // mastodon wants rfc 2616 dates, which are always in GMT
    // and GMT is the same as UTC so we just hardcode it in the format
    let date = Utc::now().format("%a, %d %b %Y %H:%M:%S GMT").to_string();
    headers.insert("date".into(), date.clone());
    let mut sha256 = Sha256::new();
    sha256.update(body.as_bytes());
    let digest = base64::encode(sha256.finish());
    let digest_header = format!("SHA-256={}", digest);
    headers.insert("digest".into(), digest_header.clone());

    let config = Config::new().mastodon_compat();
    let sig_header = config
        .begin_sign("POST", inbox_url.path(), headers)?
        .sign::<_, anyhow::Error>(actor::KEY_ID.to_string(), |signing_str| {
            debug!("Signing string: {}", signing_str);
            let keypair = PKey::private_key_from_pem(keys::PRIV_KEY_PEM.as_ref())?;
            let mut signer = Signer::new(MessageDigest::sha256(), &keypair)?;
            signer.update(signing_str.as_bytes())?;
            let sig = signer.sign_to_vec()?;
            Ok(base64::encode(&sig))
        })?
        .signature_header();

    debug!("Sending: {}\nSignature: {}", body, sig_header);

    let response = CLIENT
        .post(inbox_url)
        .body(body.into_bytes())
        .header(HOST, host)
        .header(DATE, date)
        .header("digest", digest_header)
        .header("signature", sig_header)
        .header(ACCEPT, "application/activity+json, application/json")
        .header(CONTENT_TYPE, "application/activity+json")
        .send()
        .await?;

    if (200..299).contains(&response.status().as_u16()) {
        Ok(())
    } else {
        let status = response.status();
        if let Ok(s) = response.text().await {
            error!("Failing response body: {}", s);
        }
        Err(anyhow!("Unexpected status code: {}", status))
    }
}
