use axum::{
    body::Bytes,
    extract::Path,
    http::{HeaderMap, StatusCode},
    response::{IntoResponse, Response},
    Json,
};
use serde_json::json;

use crate::{db, db::log_event};

/// push payload 中的一条 commit
#[derive(Debug, PartialEq)]
pub struct PushCommit {
    pub sha: String,
    pub message: String,
    pub timestamp: Option<String>,
}

/// HMAC-SHA256 验签（X-Gitea-Signature 头）
pub fn verify_hmac_sha256(secret: &str, body: &[u8], sig_hex: &str) -> bool {
    use hmac::{Hmac, Mac};
    use sha2::Sha256;
    let mut mac = match Hmac::<Sha256>::new_from_slice(secret.as_bytes()) {
        Ok(m) => m,
        Err(_) => return false,
    };
    mac.update(body);
    let expected: String = mac
        .finalize()
        .into_bytes()
        .iter()
        .map(|b| format!("{b:02x}"))
        .collect();
    expected.eq_ignore_ascii_case(sig_hex.trim())
}

/// 解析 Gitea push payload 的 commits 数组
pub fn parse_push_payload(body: &[u8]) -> Result<Vec<PushCommit>, String> {
    let json: serde_json::Value =
        serde_json::from_slice(body).map_err(|e| format!("payload 非法 JSON: {e}"))?;
    let commits = json["commits"]
        .as_array()
        .ok_or("payload 缺少 commits 数组")?;
    Ok(commits
        .iter()
        .filter_map(|c| {
            let sha = c["id"].as_str()?.to_string();
            let message = c["message"].as_str().unwrap_or("").to_string();
            let timestamp = c["timestamp"].as_str().map(|s| s.to_string());
            Some(PushCommit { sha, message, timestamp })
        })
        .collect())
}

/// 从请求头提取签名：优先 X-Gitea-Signature，兼容 X-Hub-Signature-256
fn extract_signature(headers: &HeaderMap) -> Option<String> {
    if let Some(v) = headers.get("x-gitea-signature").and_then(|v| v.to_str().ok()) {
        return Some(v.to_string());
    }
    headers
        .get("x-hub-signature-256")
        .and_then(|v| v.to_str().ok())
        .map(|v| v.trim_start_matches("sha256=").to_string())
}

pub async fn handle_gitea_webhook(
    Path(project_id): Path<String>,
    headers: HeaderMap,
    body: Bytes,
) -> Response {
    let secret: Option<String> = db::pool().get().ok().and_then(|c| {
        c.query_row(
            "SELECT webhook_secret FROM gitea_repos WHERE project_id = ?1",
            [&project_id],
            |r| r.get(0),
        )
        .ok()
    });
    let secret = match secret {
        Some(s) => s,
        None => {
            return (
                StatusCode::NOT_FOUND,
                Json(json!({"error": "项目未绑定 Gitea 仓库或未设置 webhook"})),
            )
                .into_response()
        }
    };

    let sig = extract_signature(&headers);
    let verified = sig
        .as_deref()
        .map(|s| verify_hmac_sha256(&secret, &body, s))
        .unwrap_or(false);
    if !verified {
        log_event(
            "gitea_webhook",
            "warn",
            &format!(
                "项目 {project_id} webhook 验签失败（签名{}）",
                if sig.is_some() { "不匹配" } else { "缺失" }
            ),
            None,
        );
        return (StatusCode::UNAUTHORIZED, Json(json!({"error": "签名验证失败"}))).into_response();
    }

    let event = headers
        .get("x-gitea-event")
        .and_then(|v| v.to_str().ok())
        .unwrap_or("");

    match event {
        "push" => handle_push(&project_id, &body).await,
        "pull_request" => handle_pull_request(&project_id, &body).await,
        other => Json(json!({"ignored": other})).into_response(),
    }
}

async fn handle_push(project_id: &str, body: &Bytes) -> Response {
    let commits = match parse_push_payload(body) {
        Ok(c) => c,
        Err(e) => return (StatusCode::BAD_REQUEST, Json(json!({"error": e}))).into_response(),
    };
    let conn = match db::pool().get() {
        Ok(c) => c,
        Err(e) => {
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(json!({"error": e.to_string()})),
            )
                .into_response()
        }
    };
    let mut received = 0;
    for c in &commits {
        if crate::commands::commit_metrics::record_commit(
            &conn,
            project_id,
            &c.sha,
            &c.message,
            c.timestamp.as_deref(),
        )
        .is_ok()
        {
            received += 1;
        }
    }
    log_event(
        "gitea_webhook",
        "info",
        &format!(
            "项目 {project_id} push 事件已接收，写入 {received}/{} 条 commit",
            commits.len()
        ),
        None,
    );
    Json(json!({"received": received})).into_response()
}

async fn handle_pull_request(project_id: &str, body: &Bytes) -> Response {
    let payload: serde_json::Value = match serde_json::from_slice(body) {
        Ok(v) => v,
        Err(e) => {
            return (
                StatusCode::BAD_REQUEST,
                Json(json!({"error": e.to_string()})),
            )
                .into_response()
        }
    };

    let raw_action = payload["action"].as_str().unwrap_or("");
    let pr = &payload["pull_request"];
    let pr_number = pr["number"].as_i64().unwrap_or(0);

    let action = match raw_action {
        "opened" | "reopened" => "opened",
        "closed" => {
            if pr["merged"].as_bool().unwrap_or(false) {
                "merged"
            } else {
                "closed"
            }
        }
        _ => return Json(json!({"ignored": raw_action})).into_response(),
    };

    let title = pr["title"].as_str().unwrap_or("").to_string();
    let head_branch = pr["head"]["label"].as_str().unwrap_or("").to_string();
    let base_branch = pr["base"]["label"].as_str().unwrap_or("").to_string();
    let author = pr["user"]["login"].as_str().unwrap_or("").to_string();
    let id = uuid::Uuid::new_v4().to_string();

    let conn = match db::pool().get() {
        Ok(c) => c,
        Err(e) => {
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(json!({"error": e.to_string()})),
            )
                .into_response()
        }
    };
    let result = conn.execute(
        "INSERT INTO pr_events (id, project_id, pr_number, title, action, head_branch, base_branch, author)
         VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
        rusqlite::params![id, project_id, pr_number, title, action, head_branch, base_branch, author],
    );

    match result {
        Ok(_) => {
            log_event(
                "gitea_webhook",
                "info",
                &format!("项目 {project_id} PR #{pr_number} {action}"),
                None,
            );
            Json(json!({"recorded": action, "pr_number": pr_number})).into_response()
        }
        Err(e) => (
            StatusCode::INTERNAL_SERVER_ERROR,
            Json(json!({"error": e.to_string()})),
        )
            .into_response(),
    }
}

// ── 测试 ─────────────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;

    fn sign(secret: &str, body: &[u8]) -> String {
        use hmac::{Hmac, Mac};
        use sha2::Sha256;
        let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes()).unwrap();
        mac.update(body);
        mac.finalize()
            .into_bytes()
            .iter()
            .map(|b| format!("{b:02x}"))
            .collect()
    }

    #[test]
    fn hmac_verify_accepts_valid_signature() {
        let body = br#"{"commits":[]}"#;
        let sig = sign("my-secret", body);
        assert!(verify_hmac_sha256("my-secret", body, &sig));
        assert!(verify_hmac_sha256("my-secret", body, &sig.to_uppercase()));
    }

    #[test]
    fn hmac_verify_rejects_wrong_secret_or_tampered_body() {
        let body = br#"{"commits":[]}"#;
        let sig = sign("my-secret", body);
        assert!(!verify_hmac_sha256("other-secret", body, &sig));
        assert!(!verify_hmac_sha256("my-secret", br#"{"commits":[1]}"#, &sig));
        assert!(!verify_hmac_sha256("my-secret", body, "deadbeef"));
    }

    #[test]
    fn parse_push_payload_extracts_commits() {
        let body = br#"{
            "ref": "refs/heads/master",
            "commits": [
                {"id": "abc123", "message": "feat(core): add thing", "timestamp": "2026-07-02T10:00:00+08:00"},
                {"id": "def456", "message": "fix(ui): repair", "timestamp": null}
            ]
        }"#;
        let commits = parse_push_payload(body).unwrap();
        assert_eq!(commits.len(), 2);
        assert_eq!(commits[0].sha, "abc123");
        assert_eq!(commits[0].message, "feat(core): add thing");
        assert_eq!(commits[0].timestamp.as_deref(), Some("2026-07-02T10:00:00+08:00"));
        assert_eq!(commits[1].timestamp, None);
    }

    #[test]
    fn parse_push_payload_rejects_invalid() {
        assert!(parse_push_payload(b"not json").is_err());
        assert!(parse_push_payload(br#"{"no_commits": true}"#).is_err());
        assert_eq!(parse_push_payload(br#"{"commits":[]}"#).unwrap().len(), 0);
    }
}
