use crate::AppService;
use crate::error::AppError;
use crate::git::RefInfo;
use models::repos::repo;
use redis::AsyncCommands;
use serde::{Deserialize, Serialize};
use session::Session;

/// Delete all cached ref list entries for a given namespace/repo.
/// Redis DEL does not support glob patterns, so we SCAN and delete each key.
async fn invalidate_ref_cache(
    cache: &db::cache::AppCache,
    namespace: &str,
    repo_name: &str,
) {
    let prefix = format!("git:ref:list:{}:{}:", namespace, repo_name);
    if let Ok(mut conn) = cache.conn().await {
        let pattern = format!("{}*", prefix);
        let mut cursor: u64 = 0;
        loop {
            match redis::cmd("SCAN")
                .arg(cursor)
                .arg("MATCH")
                .arg(&pattern)
                .arg("COUNT")
                .arg(100)
                .query_async::<(u64, Vec<String>)>(&mut conn)
                .await
            {
                Ok((new_cursor, keys)) => {
                    for key in &keys {
                        let _: () = conn.del(key).await.unwrap_or(());
                    }
                    if new_cursor == 0 {
                        break;
                    }
                    cursor = new_cursor;
                }
                Err(e) => {
                    tracing::debug!(error = ?e, "cache scan failed (non-fatal)");
                    break;
                }
            }
        }
    }
}

#[derive(Debug, Clone, Deserialize, utoipa::IntoParams)]
pub struct RefListQuery {
    pub pattern: Option<String>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct RefGetQuery {
    pub name: String,
}

#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
pub struct RefCreateRequest {
    pub name: String,
    pub oid: String,
    #[serde(default)]
    pub force: bool,
    pub message: Option<String>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct RefDeleteQuery {
    pub name: String,
}

#[derive(Debug, Clone, Deserialize, utoipa::IntoParams)]
pub struct RefRenameQuery {
    pub old_name: String,
    pub new_name: String,
    #[serde(default)]
    pub force: bool,
}

#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
pub struct RefUpdateRequest {
    pub name: String,
    pub new_oid: String,
    pub expected_oid: Option<String>,
    pub message: Option<String>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct RefTargetQuery {
    pub name: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct RefInfoResponse {
    pub name: String,
    pub oid: Option<String>,
    pub target: Option<String>,
    pub is_symbolic: bool,
    pub is_branch: bool,
    pub is_remote: bool,
    pub is_tag: bool,
    pub is_note: bool,
}

impl From<RefInfo> for RefInfoResponse {
    fn from(r: RefInfo) -> Self {
        Self {
            name: r.name,
            oid: r.oid.map(|o| o.to_string()),
            target: r.target.map(|t| t.to_string()),
            is_symbolic: r.is_symbolic,
            is_branch: r.is_branch,
            is_remote: r.is_remote,
            is_tag: r.is_tag,
            is_note: r.is_note,
        }
    }
}

#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct RefExistsResponse {
    pub name: String,
    pub exists: bool,
}

#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct RefTargetResponse {
    pub name: String,
    pub oid: Option<String>,
}

#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct RefDeleteResponse {
    pub name: String,
    pub oid: String,
}

#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct RefUpdateResponse {
    pub name: String,
    pub old_oid: Option<String>,
    pub new_oid: Option<String>,
}

impl AppService {
    pub async fn git_ref_list(
        &self,
        namespace: String,
        repo_name: String,
        query: RefListQuery,
        ctx: &Session,
    ) -> Result<Vec<RefInfoResponse>, AppError> {
        let repo = self
            .utils_find_repo(namespace.clone(), repo_name.clone(), ctx)
            .await?;
        let cache_key = format!(
            "git:ref:list:{}:{}:{}",
            namespace,
            repo_name,
            query.pattern.as_deref().unwrap_or("*")
        );

        if let Ok(mut conn) = self.cache.conn().await {
            if let Ok(cached) = conn.get::<_, String>(cache_key.clone()).await {
                if let Ok(cached) = serde_json::from_str(&cached) {
                    return Ok(cached);
                }
            }
        }

        let repo_clone = repo.clone();
        let pattern_clone = query.pattern.clone();

        let refs = tokio::task::spawn_blocking(move || {
            let domain = git::GitDomain::from_model(repo_clone)?;
            domain.ref_list(pattern_clone.as_deref())
        })
        .await
        .map_err(|e| AppError::InternalServerError(format!("Task join error: {}", e)))?
        .map_err(AppError::from)?;

        let response: Vec<RefInfoResponse> = refs.into_iter().map(RefInfoResponse::from).collect();

        if let Ok(mut conn) = self.cache.conn().await {
            if let Err(e) = conn
                .set_ex::<String, String, ()>(
                    cache_key,
                    serde_json::to_string(&response).unwrap_or_default(),
                    60 * 60,
                )
                .await
            {
                tracing::debug!(error = ?e, "cache set failed (non-fatal)");
            }
        }

        Ok(response)
    }

    pub async fn git_ref_get(
        &self,
        namespace: String,
        repo_name: String,
        name: String,
        ctx: &Session,
    ) -> Result<RefInfoResponse, AppError> {
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;

        let info = tokio::task::spawn_blocking(move || {
            let domain = git::GitDomain::from_model(repo)?;
            domain.ref_get(&name)
        })
        .await
        .map_err(|e| AppError::InternalServerError(format!("Task join error: {}", e)))?
        .map_err(AppError::from)?;

        Ok(RefInfoResponse::from(info))
    }

    pub async fn git_ref_create(
        &self,
        namespace: String,
        repo_name: String,
        request: RefCreateRequest,
        ctx: &Session,
    ) -> Result<RefUpdateResponse, AppError> {
        let repo: repo::Model = self
            .utils_check_repo_admin(namespace.clone(), repo_name.clone(), ctx)
            .await?;
        let name = request.name.clone();
        let oid = git::CommitOid::new(&request.oid);
        let force = request.force;
        let message = request.message.clone();

        let result = tokio::task::spawn_blocking(move || {
            let domain = git::GitDomain::from_model(repo)?;
            domain.ref_create(&name, oid, force, message.as_deref())
        })
        .await
        .map_err(|e| AppError::InternalServerError(format!("Task join error: {}", e)))?
        .map_err(AppError::from)?;

        invalidate_ref_cache(&self.cache, &namespace, &repo_name).await;

        Ok(RefUpdateResponse {
            name: result.name,
            old_oid: result.old_oid.map(|o| o.to_string()),
            new_oid: result.new_oid.map(|o| o.to_string()),
        })
    }

    pub async fn git_ref_delete(
        &self,
        namespace: String,
        repo_name: String,
        name: String,
        ctx: &Session,
    ) -> Result<RefDeleteResponse, AppError> {
        let repo: repo::Model = self
            .utils_check_repo_admin(namespace.clone(), repo_name.clone(), ctx)
            .await?;
        let name_clone = name.clone();
        let oid = tokio::task::spawn_blocking(move || {
            let domain = git::GitDomain::from_model(repo)?;
            domain.ref_delete(&name_clone)
        })
        .await
        .map_err(|e| AppError::InternalServerError(format!("Task join error: {}", e)))?
        .map_err(AppError::from)?;

        invalidate_ref_cache(&self.cache, &namespace, &repo_name).await;

        Ok(RefDeleteResponse {
            name,
            oid: oid.to_string(),
        })
    }

    pub async fn git_ref_rename(
        &self,
        namespace: String,
        repo_name: String,
        query: RefRenameQuery,
        ctx: &Session,
    ) -> Result<RefInfoResponse, AppError> {
        let repo: repo::Model = self
            .utils_check_repo_admin(namespace.clone(), repo_name.clone(), ctx)
            .await?;
        let old_name = query.old_name.clone();
        let new_name = query.new_name.clone();
        let force = query.force;

        let info = tokio::task::spawn_blocking(move || {
            let domain = git::GitDomain::from_model(repo)?;
            domain.ref_rename(&old_name, &new_name, force)
        })
        .await
        .map_err(|e| AppError::InternalServerError(format!("Task join error: {}", e)))?
        .map_err(AppError::from)?;

        invalidate_ref_cache(&self.cache, &namespace, &repo_name).await;

        Ok(RefInfoResponse::from(info))
    }

    pub async fn git_ref_update(
        &self,
        namespace: String,
        repo_name: String,
        request: RefUpdateRequest,
        ctx: &Session,
    ) -> Result<RefUpdateResponse, AppError> {
        let repo: repo::Model = self
            .utils_check_repo_admin(namespace, repo_name, ctx)
            .await?;
        let name = request.name.clone();
        let new_oid = git::CommitOid::new(&request.new_oid);
        let expected_oid = request.expected_oid.map(|o| git::CommitOid::new(&o));
        let message = request.message.clone();

        let result = tokio::task::spawn_blocking(move || {
            let domain = git::GitDomain::from_model(repo)?;
            domain.ref_update(&name, new_oid, expected_oid, message.as_deref())
        })
        .await
        .map_err(|e| AppError::InternalServerError(format!("Task join error: {}", e)))?
        .map_err(AppError::from)?;

        Ok(RefUpdateResponse {
            name: result.name,
            old_oid: result.old_oid.map(|o| o.to_string()),
            new_oid: result.new_oid.map(|o| o.to_string()),
        })
    }

    pub async fn git_ref_exists(
        &self,
        namespace: String,
        repo_name: String,
        name: String,
        ctx: &Session,
    ) -> Result<RefExistsResponse, AppError> {
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
        let name_clone = name.clone();

        let exists = tokio::task::spawn_blocking(move || {
            let domain = git::GitDomain::from_model(repo)?;
            Ok::<_, git::GitError>(domain.ref_exists(&name_clone))
        })
        .await
        .map_err(|e| AppError::InternalServerError(format!("Task join error: {}", e)))?
        .map_err(AppError::from)?;

        Ok(RefExistsResponse { name, exists })
    }

    pub async fn git_ref_target(
        &self,
        namespace: String,
        repo_name: String,
        name: String,
        ctx: &Session,
    ) -> Result<RefTargetResponse, AppError> {
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
        let name_clone = name.clone();

        let oid = tokio::task::spawn_blocking(move || {
            let domain = git::GitDomain::from_model(repo)?;
            domain.ref_target(&name_clone)
        })
        .await
        .map_err(|e| AppError::InternalServerError(format!("Task join error: {}", e)))?
        .map_err(AppError::from)?;

        Ok(RefTargetResponse {
            name,
            oid: oid.map(|o| o.to_string()),
        })
    }
}
