use anyhow::Result;
use chrono::{DateTime, Utc};
use reqwest::Method;
use serde::{Deserialize, Serialize};

use crate::client::Client;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Release {
    pub id: u64,
    pub tag_name: String,
    #[serde(default)]
    pub name: String,
    #[serde(default)]
    pub body: String,
    pub target_commitish: String,
    pub draft: bool,
    pub prerelease: bool,
    pub html_url: String,
    pub created_at: DateTime<Utc>,
    pub published_at: Option<DateTime<Utc>>,
    #[serde(default)]
    pub assets: Vec<Asset>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Asset {
    pub id: u64,
    pub name: String,
    pub size: u64,
    pub browser_download_url: String,
    pub created_at: DateTime<Utc>,
}

pub async fn list(client: &Client, owner: &str, name: &str, limit: u32) -> Result<Vec<Release>> {
    let path = format!("/api/v1/repos/{owner}/{name}/releases");
    let query = vec![
        ("limit".into(), limit.clamp(1, 50).to_string()),
        ("page".into(), "1".into()),
    ];
    client.json(Method::GET, &path, &query, None::<&()>).await
}

pub async fn get_by_tag(client: &Client, owner: &str, name: &str, tag: &str) -> Result<Release> {
    let path = format!("/api/v1/repos/{owner}/{name}/releases/tags/{tag}");
    client.json(Method::GET, &path, &[], None::<&()>).await
}

#[derive(Debug, Clone, Serialize)]
pub struct CreateRelease<'a> {
    pub tag_name: &'a str,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub name: Option<&'a str>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub body: Option<&'a str>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub target_commitish: Option<&'a str>,
    #[serde(default)]
    pub draft: bool,
    #[serde(default)]
    pub prerelease: bool,
}

pub async fn create(
    client: &Client,
    owner: &str,
    name: &str,
    body: &CreateRelease<'_>,
) -> Result<Release> {
    let path = format!("/api/v1/repos/{owner}/{name}/releases");
    client.json(Method::POST, &path, &[], Some(body)).await
}

#[derive(Debug, Clone, Serialize, Default)]
pub struct EditRelease<'a> {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tag_name: Option<&'a str>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub name: Option<&'a str>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub body: Option<&'a str>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub draft: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub prerelease: Option<bool>,
}

pub async fn edit(
    client: &Client,
    owner: &str,
    name: &str,
    id: u64,
    body: &EditRelease<'_>,
) -> Result<Release> {
    let path = format!("/api/v1/repos/{owner}/{name}/releases/{id}");
    client.json(Method::PATCH, &path, &[], Some(body)).await
}

pub async fn delete(client: &Client, owner: &str, name: &str, id: u64) -> Result<()> {
    let path = format!("/api/v1/repos/{owner}/{name}/releases/{id}");
    let res = client.request(Method::DELETE, &path, &[], None).await?;
    res.error_for_status()?;
    Ok(())
}

/// Upload an asset (binary) to a release.
pub async fn upload_asset(
    client: &Client,
    owner: &str,
    name: &str,
    release_id: u64,
    asset_name: &str,
    bytes: Vec<u8>,
) -> Result<Asset> {
    let path = format!(
        "/api/v1/repos/{owner}/{name}/releases/{release_id}/assets?name={}",
        urlencode(asset_name)
    );
    // Build a multipart form with the file payload.
    let url = client.url(&path)?;
    let part = reqwest::multipart::Part::bytes(bytes)
        .file_name(asset_name.to_string())
        .mime_str("application/octet-stream")?;
    let form = reqwest::multipart::Form::new().part("attachment", part);
    let mut headers = reqwest::header::HeaderMap::new();
    headers.insert(
        reqwest::header::AUTHORIZATION,
        reqwest::header::HeaderValue::from_str(&format!("token {}", client.token()))?,
    );
    let res = client
        .http()
        .post(url)
        .headers(headers)
        .multipart(form)
        .send()
        .await?;
    let res = res.error_for_status()?;
    Ok(res.json().await?)
}

#[allow(dead_code)]
pub async fn delete_asset(client: &Client, owner: &str, name: &str, asset_id: u64) -> Result<()> {
    let path = format!("/api/v1/repos/{owner}/{name}/releases/assets/{asset_id}");
    let res = client.request(Method::DELETE, &path, &[], None).await?;
    res.error_for_status()?;
    Ok(())
}

fn urlencode(s: &str) -> String {
    // The asset name appears in a query parameter; URL-encode special chars
    // conservatively. `url` crate provides a Serializer for full forms.
    let mut out = String::with_capacity(s.len());
    for c in s.chars() {
        if c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.' | '~') {
            out.push(c);
        } else {
            for b in c.to_string().bytes() {
                out.push_str(&format!("%{b:02X}"));
            }
        }
    }
    out
}
