//! Gitea API Token Integration
//!
//! Manages Gitea Personal Access Tokens through the API Key system

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

/// Gitea token scope
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GiteaScope {
    ReadRepository,
    WriteRepository,
    ReadIssue,
    WriteIssue,
    ReadUser,
    WriteUser,
    ReadAdmin,
    WriteAdmin,
    ReadOrganization,
    WriteOrganization,
    ReadPackage,
    WritePackage,
    ReadNotification,
    WriteNotification,
    ReadActivitypub,
    WriteActivitypub,
    ReadMisc,
    WriteMisc,
}

impl GiteaScope {
    pub fn as_str(&self) -> &'static str {
        match self {
            GiteaScope::ReadRepository => "read:repository",
            GiteaScope::WriteRepository => "write:repository",
            GiteaScope::ReadIssue => "read:issue",
            GiteaScope::WriteIssue => "write:issue",
            GiteaScope::ReadUser => "read:user",
            GiteaScope::WriteUser => "write:user",
            GiteaScope::ReadAdmin => "read:admin",
            GiteaScope::WriteAdmin => "write:admin",
            GiteaScope::ReadOrganization => "read:organization",
            GiteaScope::WriteOrganization => "write:organization",
            GiteaScope::ReadPackage => "read:package",
            GiteaScope::WritePackage => "write:package",
            GiteaScope::ReadNotification => "read:notification",
            GiteaScope::WriteNotification => "write:notification",
            GiteaScope::ReadActivitypub => "read:activitypub",
            GiteaScope::WriteActivitypub => "write:activitypub",
            GiteaScope::ReadMisc => "read:misc",
            GiteaScope::WriteMisc => "write:misc",
        }
    }
}

impl std::str::FromStr for GiteaScope {
    type Err = String;

    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
        match s {
            "read:repository" => Ok(GiteaScope::ReadRepository),
            "write:repository" => Ok(GiteaScope::WriteRepository),
            "read:issue" => Ok(GiteaScope::ReadIssue),
            "write:issue" => Ok(GiteaScope::WriteIssue),
            "read:user" => Ok(GiteaScope::ReadUser),
            "write:user" => Ok(GiteaScope::WriteUser),
            "read:admin" => Ok(GiteaScope::ReadAdmin),
            "write:admin" => Ok(GiteaScope::WriteAdmin),
            "read:organization" => Ok(GiteaScope::ReadOrganization),
            "write:organization" => Ok(GiteaScope::WriteOrganization),
            "read:package" => Ok(GiteaScope::ReadPackage),
            "write:package" => Ok(GiteaScope::WritePackage),
            "read:notification" => Ok(GiteaScope::ReadNotification),
            "write:notification" => Ok(GiteaScope::WriteNotification),
            "read:activitypub" => Ok(GiteaScope::ReadActivitypub),
            "write:activitypub" => Ok(GiteaScope::WriteActivitypub),
            "read:misc" => Ok(GiteaScope::ReadMisc),
            "write:misc" => Ok(GiteaScope::WriteMisc),
            _ => Err(format!("Invalid Gitea scope: {}", s)),
        }
    }
}

/// Request to create a Gitea token
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateGiteaTokenRequest {
    pub username: String,
    pub password: String,
    pub token_name: String,
    pub scopes: Vec<GiteaScope>,
}

/// Response from creating a Gitea token
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GiteaTokenResponse {
    pub id: i64,
    pub name: String,
    pub sha1: String,
    pub token_last_eight: String,
}

/// List token response (without SHA1)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GiteaTokenInfo {
    pub id: i64,
    pub name: String,
    pub token_last_eight: String,
}

/// Gitea API client
pub struct GiteaClient {
    client: Client,
    base_url: String,
}

impl GiteaClient {
    pub fn new() -> Result<Self> {
        let base_url =
            std::env::var("GITEA_URL").unwrap_or_else(|_| "http://localhost:3001".to_string());

        Ok(Self {
            client: Client::new(),
            base_url,
        })
    }

    pub fn with_url(base_url: String) -> Self {
        Self {
            client: Client::new(),
            base_url,
        }
    }

    /// Create a new access token for a user
    pub async fn create_token(
        &self,
        request: &CreateGiteaTokenRequest,
    ) -> Result<GiteaTokenResponse> {
        let url = format!("{}/api/v1/users/{}/tokens", self.base_url, request.username);

        let scopes: Vec<String> = request
            .scopes
            .iter()
            .map(|s| s.as_str().to_string())
            .collect();

        let body = serde_json::json!({
            "name": request.token_name,
            "scopes": scopes,
        });

        let response = self
            .client
            .post(&url)
            .basic_auth(&request.username, Some(&request.password))
            .json(&body)
            .send()
            .await
            .context("Failed to send create token request")?;

        if !response.status().is_success() {
            let status = response.status();
            let text = response.text().await.unwrap_or_default();
            anyhow::bail!("Failed to create Gitea token: {} - {}", status, text);
        }

        let token: GiteaTokenResponse = response
            .json()
            .await
            .context("Failed to parse token response")?;

        Ok(token)
    }

    /// List all tokens for a user
    pub async fn list_tokens(&self, username: &str, password: &str) -> Result<Vec<GiteaTokenInfo>> {
        let url = format!("{}/api/v1/users/{}/tokens", self.base_url, username);

        let response = self
            .client
            .get(&url)
            .basic_auth(username, Some(password))
            .send()
            .await
            .context("Failed to send list tokens request")?;

        if !response.status().is_success() {
            let status = response.status();
            let text = response.text().await.unwrap_or_default();
            anyhow::bail!("Failed to list Gitea tokens: {} - {}", status, text);
        }

        let tokens: Vec<GiteaTokenInfo> = response
            .json()
            .await
            .context("Failed to parse tokens response")?;

        Ok(tokens)
    }

    /// Delete a token by name
    pub async fn delete_token(
        &self,
        username: &str,
        password: &str,
        token_name: &str,
    ) -> Result<()> {
        let url = format!(
            "{}/api/v1/users/{}/tokens/{}",
            self.base_url, username, token_name
        );

        let response = self
            .client
            .delete(&url)
            .basic_auth(username, Some(password))
            .send()
            .await
            .context("Failed to send delete token request")?;

        if !response.status().is_success() {
            let status = response.status();
            let text = response.text().await.unwrap_or_default();
            anyhow::bail!("Failed to delete Gitea token: {} - {}", status, text);
        }

        Ok(())
    }

    /// Verify a token is valid by making a test API call
    pub async fn verify_token(&self, token: &str) -> Result<bool> {
        let url = format!("{}/api/v1/user", self.base_url);

        let response = self
            .client
            .get(&url)
            .header("Authorization", format!("token {}", token))
            .send()
            .await
            .context("Failed to send verify token request")?;

        Ok(response.status().is_success())
    }

    /// Get base URL
    pub fn base_url(&self) -> &str {
        &self.base_url
    }
}

impl Default for GiteaClient {
    fn default() -> Self {
        Self::new().unwrap_or_else(|_| Self::with_url("http://localhost:3001".to_string()))
    }
}

/// Integrated token record
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GiteaTokenRecord {
    pub gitea_token_id: i64,
    pub gitea_user: String,
    pub token_name: String,
    pub token_last_eight: String,
    pub scopes: Vec<String>,
    pub api_key_id: Option<String>,
    pub created_at: DateTime<Utc>,
    pub last_verified: Option<DateTime<Utc>>,
}

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

    #[test]
    fn test_gitea_scope_as_str() {
        assert_eq!(GiteaScope::ReadRepository.as_str(), "read:repository");
        assert_eq!(GiteaScope::WriteIssue.as_str(), "write:issue");
        assert_eq!(GiteaScope::ReadAdmin.as_str(), "read:admin");
    }

    #[test]
    fn test_gitea_scope_from_str() {
        use std::str::FromStr;

        assert!(matches!(
            GiteaScope::from_str("read:repository").ok(),
            Some(GiteaScope::ReadRepository)
        ));
        assert!(matches!(
            GiteaScope::from_str("write:issue").ok(),
            Some(GiteaScope::WriteIssue)
        ));
        assert!(GiteaScope::from_str("invalid").is_err());
    }

    #[test]
    fn test_gitea_client_default() {
        let client = GiteaClient::default();
        assert_eq!(client.base_url(), "http://localhost:3001");
    }
}
