use hmac::{Hmac, Mac};
use sha2::Sha256;
use std::time::Duration;

type HmacSha256 = Hmac<Sha256>;

/// Signs a payload body using HMAC-SHA256 with the given secret.
/// Returns the "X-Hub-Signature-256" header value.
pub fn sign_payload(body: &[u8], secret: &str) -> Option<String> {
    if secret.is_empty() {
        return None;
    }
    let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).ok()?;
    mac.update(body);
    let bytes = mac.finalize().into_bytes();
    Some(format!(
        "sha256={}",
        bytes.iter().map(|b| format!("{:02x}", b)).collect::<String>()
    ))
}

/// Payload sent for a push event webhook.
#[derive(Debug, serde::Serialize)]
pub struct PushPayload<'a> {
    #[serde(rename = "ref")]
    pub r#ref: &'a str,
    pub before: &'a str,
    pub after: &'a str,
    pub repository: RepositoryPayload<'a>,
    pub pusher: PusherPayload<'a>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub commits: Vec<CommitPayload<'a>>,
}

/// Payload sent for a tag push event webhook.
#[derive(Debug, serde::Serialize)]
pub struct TagPushPayload<'a> {
    #[serde(rename = "ref")]
    pub r#ref: &'a str,
    pub before: &'a str,
    pub after: &'a str,
    pub repository: RepositoryPayload<'a>,
    pub pusher: PusherPayload<'a>,
}

#[derive(Debug, serde::Serialize)]
pub struct RepositoryPayload<'a> {
    pub id: i64,
    pub name: &'a str,
    pub full_name: &'a str,
    pub namespace: &'a str,
    pub default_branch: &'a str,
}

#[derive(Debug, serde::Serialize)]
pub struct PusherPayload<'a> {
    pub name: &'a str,
    pub email: &'a str,
}

#[derive(Debug, serde::Serialize)]
pub struct CommitPayload<'a> {
    pub id: &'a str,
    pub message: &'a str,
    pub author: AuthorPayload<'a>,
}

#[derive(Debug, serde::Serialize)]
pub struct AuthorPayload<'a> {
    pub name: &'a str,
    pub email: &'a str,
}

/// A configured webhook destination.
#[derive(Debug, Clone)]
pub struct WebhookTarget {
    pub id: i64,
    pub url: String,
    pub secret: Option<String>,
    pub content_type: String,
    pub events: WebhookEvents,
    pub active: bool,
}

#[derive(Debug, Default, Clone)]
pub struct WebhookEvents {
    pub push: bool,
    pub tag_push: bool,
    pub pull_request: bool,
    pub issue_comment: bool,
    pub release: bool,
}

/// Dispatches a webhook HTTP POST request.
pub async fn deliver(
    client: &reqwest::Client,
    url: &str,
    secret: Option<&str>,
    content_type: &str,
    body: &[u8],
) -> Result<(), DispatchError> {
    let mut req = client
        .post(url)
        .header("Content-Type", content_type)
        .header("User-Agent", "Code-Git-Hook/1.0")
        .header("X-Webhook-Event", "push")
        .timeout(Duration::from_secs(10))
        .body(body.to_vec());

    if let Some(secret) = secret {
        if let Some(sig) = sign_payload(body, secret) {
            req = req.header("X-Hub-Signature-256", sig);
        }
    }

    let resp = req.send().await.map_err(|e| {
        if e.is_timeout() {
            DispatchError::Timeout
        } else if e.is_connect() {
            DispatchError::ConnectionFailed
        } else {
            DispatchError::RequestFailed(e.to_string())
        }
    })?;

    let status = resp.status();
    if status.is_success() {
        Ok(())
    } else {
        Err(DispatchError::HttpError(status.as_u16()))
    }
}

#[derive(Debug)]
pub enum DispatchError {
    Timeout,
    ConnectionFailed,
    RequestFailed(String),
    HttpError(u16),
}
