use crate::AppService;
use crate::error::AppError;
use crate::git::{TagInfo, TagSummary};
use models::repos::repo;
use models::repos::repo as repo_model;
use models::repos::repo_tag;
use redis::AsyncCommands;
use sea_orm::{ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter};
use serde::{Deserialize, Serialize};
use session::Session;
use uuid::Uuid;
#[derive(Debug, Clone, Deserialize)]
pub struct TagGetQuery {
    #[serde(default)]
    pub name: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct TagTargetQuery {
    #[serde(default)]
    pub name: String,
}
#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
pub struct TagRenameQuery {
    pub old_name: String,
    pub new_name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct TagInfoResponse {
    pub name: String,
    pub oid: String,
    pub target: String,
    pub is_annotated: bool,
    pub message: Option<String>,
    pub tagger: Option<String>,
    pub tagger_email: Option<String>,
}
impl From<TagInfo> for TagInfoResponse {
    fn from(t: TagInfo) -> Self {
        Self {
            name: t.name,
            oid: t.oid.to_string(),
            target: t.target.to_string(),
            is_annotated: t.is_annotated,
            message: t.message,
            tagger: t.tagger,
            tagger_email: t.tagger_email,
        }
    }
}

impl From<repo_tag::Model> for TagInfoResponse {
    fn from(t: repo_tag::Model) -> Self {
        // is_annotated: description field is set for annotated tags
        let is_annotated = t.description.is_some();
        Self {
            name: t.name,
            oid: t.oid.clone(),
            // For tags, oid IS the target commit OID (repo_tag.oid is the peeled target)
            target: t.oid,
            is_annotated,
            message: t.description,
            tagger: Some(t.tagger_name),
            tagger_email: Some(t.tagger_email),
        }
    }
}
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct TagSummaryResponse {
    pub total_count: usize,
}
impl From<TagSummary> for TagSummaryResponse {
    fn from(s: TagSummary) -> Self {
        Self {
            total_count: s.total_count,
        }
    }
}
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct TagExistsResponse {
    pub name: String,
    pub exists: bool,
}
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct TagTargetResponse {
    pub name: String,
    pub target: Option<String>,
}
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct TagIsAnnotatedResponse {
    pub name: String,
    pub is_annotated: bool,
}
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct TagMessageResponse {
    pub name: String,
    pub message: Option<String>,
}
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct TagTaggerResponse {
    pub name: String,
    pub tagger: Option<String>,
    pub tagger_email: Option<String>,
}
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct TagCountResponse {
    pub count: usize,
}
#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
pub struct TagCreateRequest {
    pub name: String,
    pub target: String,
    pub message: Option<String>,
    pub tagger_name: Option<String>,
    pub tagger_email: Option<String>,
    #[serde(default)]
    pub force: bool,
}
#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
pub struct TagCreateLightweightRequest {
    pub name: String,
    pub target: String,
    #[serde(default)]
    pub force: bool,
}
#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
pub struct TagUpdateMessageRequest {
    pub name: String,
    pub message: String,
    pub tagger_name: String,
    pub tagger_email: String,
}
impl AppService {
    pub async fn git_tag_list(
        &self,
        namespace: String,
        repo_name: String,
        ctx: &Session,
    ) -> Result<Vec<TagInfoResponse>, AppError> {
        let repo = self
            .utils_find_repo(namespace.clone(), repo_name.clone(), ctx)
            .await?;

        // Try DB first (fastest path after sync)
        let tags: Vec<TagInfoResponse> = repo_tag::Entity::find()
            .filter(repo_tag::Column::Repo.eq(repo.id))
            .all(&self.db)
            .await
            .map_err(AppError::from)?
            .into_iter()
            .map(TagInfoResponse::from)
            .collect();

        // Write to Redis cache for subsequent requests
        let cache_key = format!("git:tag:list:{}:{}", namespace, repo_name);
        if let Ok(mut conn) = self.cache.conn().await {
            if let Err(e) = conn
                .set_ex::<String, String, ()>(
                    cache_key,
                    serde_json::to_string(&tags).unwrap_or_default(),
                    60 * 60,
                )
                .await
            {
                slog::debug!(self.logs, "cache set failed (non-fatal): {}", e);
            }
        }
        Ok(tags)
    }

    pub async fn git_tag_list_names(
        &self,
        namespace: String,
        repo_name: String,
        ctx: &Session,
    ) -> Result<Vec<String>, AppError> {
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
        let names: Vec<String> = repo_tag::Entity::find()
            .filter(repo_tag::Column::Repo.eq(repo.id))
            .all(&self.db)
            .await
            .map_err(AppError::from)?
            .into_iter()
            .map(|t| t.name)
            .collect();
        Ok(names)
    }

    pub async fn git_tag_summary(
        &self,
        namespace: String,
        repo_name: String,
        ctx: &Session,
    ) -> Result<TagSummaryResponse, AppError> {
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
        let count: usize = repo_tag::Entity::find()
            .filter(repo_tag::Column::Repo.eq(repo.id))
            .count(&self.db)
            .await
            .map_err(AppError::from)? as usize;
        Ok(TagSummaryResponse { total_count: count })
    }

    pub async fn git_tag_count(
        &self,
        namespace: String,
        repo_name: String,
        ctx: &Session,
    ) -> Result<TagCountResponse, AppError> {
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
        let count: usize = repo_tag::Entity::find()
            .filter(repo_tag::Column::Repo.eq(repo.id))
            .count(&self.db)
            .await
            .map_err(AppError::from)? as usize;
        Ok(TagCountResponse { count })
    }

    pub async fn git_tag_get(
        &self,
        namespace: String,
        repo_name: String,
        query: TagGetQuery,
        ctx: &Session,
    ) -> Result<TagInfoResponse, AppError> {
        let repo = self
            .utils_find_repo(namespace.clone(), repo_name.clone(), ctx)
            .await?;

        // Try DB first
        if let Some(tag) = repo_tag::Entity::find()
            .filter(repo_tag::Column::Repo.eq(repo.id))
            .filter(repo_tag::Column::Name.eq(&query.name))
            .one(&self.db)
            .await
            .map_err(AppError::from)?
        {
            let response = TagInfoResponse::from(tag);
            // Populate cache for next time
            let cache_key = format!("git:tag:get:{}:{}:{}", namespace, repo_name, query.name);
            if let Ok(mut conn) = self.cache.conn().await {
                let _ = conn
                    .set_ex::<String, String, ()>(
                        cache_key,
                        serde_json::to_string(&response).unwrap_or_default(),
                        60 * 60,
                    )
                    .await;
            }
            return Ok(response);
        }

        // Fallback to git
        let cache_key = format!("git:tag:get:{}:{}:{}", namespace, repo_name, query.name);
        let domain = git::GitDomain::from_model(repo)?;
        let info = domain.tag_get(&query.name)?;
        let response = TagInfoResponse::from(info);
        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
            {
                slog::debug!(self.logs, "cache set failed (non-fatal): {}", e);
            }
        }
        Ok(response)
    }

    pub async fn git_tag_exists(
        &self,
        namespace: String,
        repo_name: String,
        query: TagGetQuery,
        ctx: &Session,
    ) -> Result<TagExistsResponse, AppError> {
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
        let exists = repo_tag::Entity::find()
            .filter(repo_tag::Column::Repo.eq(repo.id))
            .filter(repo_tag::Column::Name.eq(&query.name))
            .one(&self.db)
            .await
            .map_err(AppError::from)?
            .is_some();
        Ok(TagExistsResponse {
            name: query.name,
            exists,
        })
    }

    pub async fn git_tag_target(
        &self,
        namespace: String,
        repo_name: String,
        query: TagTargetQuery,
        ctx: &Session,
    ) -> Result<TagTargetResponse, AppError> {
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
        let target = repo_tag::Entity::find()
            .filter(repo_tag::Column::Repo.eq(repo.id))
            .filter(repo_tag::Column::Name.eq(&query.name))
            .one(&self.db)
            .await
            .map_err(AppError::from)?
            .map(|t| t.oid);
        Ok(TagTargetResponse {
            name: query.name,
            target,
        })
    }

    pub async fn git_tag_is_annotated(
        &self,
        namespace: String,
        repo_name: String,
        query: TagGetQuery,
        ctx: &Session,
    ) -> Result<TagIsAnnotatedResponse, AppError> {
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
        let is_annotated = repo_tag::Entity::find()
            .filter(repo_tag::Column::Repo.eq(repo.id))
            .filter(repo_tag::Column::Name.eq(&query.name))
            .one(&self.db)
            .await
            .map_err(AppError::from)?
            .map(|t| t.description.is_some())
            .unwrap_or(false);
        Ok(TagIsAnnotatedResponse {
            name: query.name,
            is_annotated,
        })
    }

    pub async fn git_tag_message(
        &self,
        namespace: String,
        repo_name: String,
        query: TagGetQuery,
        ctx: &Session,
    ) -> Result<TagMessageResponse, AppError> {
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
        let message = repo_tag::Entity::find()
            .filter(repo_tag::Column::Repo.eq(repo.id))
            .filter(repo_tag::Column::Name.eq(&query.name))
            .one(&self.db)
            .await
            .map_err(AppError::from)?
            .and_then(|t| t.description);
        Ok(TagMessageResponse {
            name: query.name,
            message,
        })
    }

    pub async fn git_tag_tagger(
        &self,
        namespace: String,
        repo_name: String,
        query: TagGetQuery,
        ctx: &Session,
    ) -> Result<TagTaggerResponse, AppError> {
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
        let tag = repo_tag::Entity::find()
            .filter(repo_tag::Column::Repo.eq(repo.id))
            .filter(repo_tag::Column::Name.eq(&query.name))
            .one(&self.db)
            .await
            .map_err(AppError::from)?;
        Ok(TagTaggerResponse {
            name: query.name,
            tagger: tag.as_ref().map(|t| t.tagger_name.clone()),
            tagger_email: tag.map(|t| t.tagger_email),
        })
    }
    pub async fn git_tag_create(
        &self,
        namespace: String,
        repo_name: String,
        request: TagCreateRequest,
        ctx: &Session,
    ) -> Result<TagInfoResponse, AppError> {
        let repo: repo::Model = self
            .utils_check_repo_admin(namespace.clone(), repo_name.clone(), ctx)
            .await?;
        let repo_id = repo.id;
        let domain = git::GitDomain::from_model(repo)?;
        let tagger = git::CommitSignature {
            name: request
                .tagger_name
                .unwrap_or_else(|| "Anonymous".to_string()),
            email: request
                .tagger_email
                .unwrap_or_else(|| "anonymous@example.com".to_string()),
            time_secs: chrono::Utc::now().timestamp(),
            offset_minutes: 0,
        };
        let target = git::CommitOid::new(&request.target);
        let info = domain.tag_create(
            &request.name,
            &target,
            request.message.as_deref().unwrap_or(""),
            &tagger,
            request.force,
        )?;
        if let Ok(mut conn) = self.cache.conn().await {
            let key = format!("git:tag:list:{}:{}", namespace, repo_name);
            if let Err(e) = conn.del::<String, ()>(key).await {
                slog::debug!(self.logs, "cache del failed (non-fatal): {}", e);
            }
        }
        let project_id = match repo_model::Entity::find_by_id(repo_id).one(&self.db).await {
            Ok(Some(r)) => r.project,
            Ok(None) => Uuid::nil(),
            Err(e) => {
                slog::warn!(
                    self.logs,
                    "failed to look up project_id for activity log: {}",
                    e
                );
                Uuid::nil()
            }
        };
        let user_uid = ctx.user().unwrap_or(Uuid::nil());
        let _ = self
            .project_log_activity(
                project_id,
                Some(repo_id),
                user_uid,
                super::super::project::activity::ActivityLogParams {
                    event_type: "tag_create".to_string(),
                    title: format!("{} created tag '{}'", user_uid, request.name),
                    repo_id: Some(repo_id),
                    content: None,
                    event_id: None,
                    event_sub_id: None,
                    metadata: Some(serde_json::json!({"tag_name": request.name})),
                    is_private: false,
                },
            )
            .await;
        Ok(TagInfoResponse::from(info))
    }
    pub async fn git_tag_create_lightweight(
        &self,
        namespace: String,
        repo_name: String,
        request: TagCreateLightweightRequest,
        ctx: &Session,
    ) -> Result<TagInfoResponse, AppError> {
        let repo: repo::Model = self
            .utils_check_repo_admin(namespace.clone(), repo_name.clone(), ctx)
            .await?;
        let repo_id = repo.id;
        let domain = git::GitDomain::from_model(repo)?;
        let target = git::CommitOid::new(&request.target);
        let info = domain.tag_create_lightweight(&request.name, &target, request.force)?;
        if let Ok(mut conn) = self.cache.conn().await {
            let key = format!("git:tag:list:{}:{}", namespace, repo_name);
            if let Err(e) = conn.del::<String, ()>(key).await {
                slog::debug!(self.logs, "cache del failed (non-fatal): {}", e);
            }
        }
        let project_id = match repo_model::Entity::find_by_id(repo_id).one(&self.db).await {
            Ok(Some(r)) => r.project,
            Ok(None) => Uuid::nil(),
            Err(e) => {
                slog::warn!(
                    self.logs,
                    "failed to look up project_id for activity log: {}",
                    e
                );
                Uuid::nil()
            }
        };
        let user_uid = ctx.user().unwrap_or(Uuid::nil());
        let _ = self
            .project_log_activity(
                project_id,
                Some(repo_id),
                user_uid,
                super::super::project::activity::ActivityLogParams {
                    event_type: "tag_create".to_string(),
                    title: format!("{} created tag '{}'", user_uid, request.name),
                    repo_id: Some(repo_id),
                    content: None,
                    event_id: None,
                    event_sub_id: None,
                    metadata: Some(serde_json::json!({"tag_name": request.name})),
                    is_private: false,
                },
            )
            .await;
        Ok(TagInfoResponse::from(info))
    }
    pub async fn git_tag_delete(
        &self,
        namespace: String,
        repo_name: String,
        query: TagGetQuery,
        ctx: &Session,
    ) -> Result<(), AppError> {
        let repo: repo::Model = self
            .utils_check_repo_admin(namespace.clone(), repo_name.clone(), ctx)
            .await?;
        let repo_id = repo.id;
        let domain = git::GitDomain::from_model(repo)?;
        domain.tag_delete(&query.name)?;
        if let Ok(mut conn) = self.cache.conn().await {
            let list_key = format!("git:tag:list:{}:{}", namespace, repo_name);
            let get_key = format!("git:tag:get:{}:{}:{}", namespace, repo_name, query.name);
            if let Err(e) = conn.del::<String, ()>(list_key).await {
                slog::debug!(self.logs, "cache del failed (non-fatal): {}", e);
            }
            if let Err(e) = conn.del::<String, ()>(get_key).await {
                slog::debug!(self.logs, "cache del failed (non-fatal): {}", e);
            }
        }
        let project_id = match repo_model::Entity::find_by_id(repo_id).one(&self.db).await {
            Ok(Some(r)) => r.project,
            Ok(None) => Uuid::nil(),
            Err(e) => {
                slog::warn!(
                    self.logs,
                    "failed to look up project_id for activity log: {}",
                    e
                );
                Uuid::nil()
            }
        };
        let user_uid = ctx.user().unwrap_or(Uuid::nil());
        let _ = self
            .project_log_activity(
                project_id,
                Some(repo_id),
                user_uid,
                super::super::project::activity::ActivityLogParams {
                    event_type: "tag_delete".to_string(),
                    title: format!("{} deleted tag '{}'", user_uid, query.name),
                    repo_id: Some(repo_id),
                    content: None,
                    event_id: None,
                    event_sub_id: None,
                    metadata: Some(serde_json::json!({"tag_name": query.name})),
                    is_private: false,
                },
            )
            .await;
        Ok(())
    }
    pub async fn git_tag_rename(
        &self,
        namespace: String,
        repo_name: String,
        query: TagRenameQuery,
        ctx: &Session,
    ) -> Result<TagInfoResponse, AppError> {
        let repo: repo::Model = self
            .utils_check_repo_admin(namespace.clone(), repo_name.clone(), ctx)
            .await?;
        let repo_id = repo.id;
        let domain = git::GitDomain::from_model(repo)?;
        let info = domain.tag_rename(&query.old_name, &query.new_name)?;
        if let Ok(mut conn) = self.cache.conn().await {
            let list_key = format!("git:tag:list:{}:{}", namespace, repo_name);
            let old_key = format!("git:tag:get:{}:{}:{}", namespace, repo_name, query.old_name);
            let new_key = format!("git:tag:get:{}:{}:{}", namespace, repo_name, query.new_name);
            if let Err(e) = conn.del::<String, ()>(list_key).await {
                slog::debug!(self.logs, "cache del failed (non-fatal): {}", e);
            }
            if let Err(e) = conn.del::<String, ()>(old_key).await {
                slog::debug!(self.logs, "cache del failed (non-fatal): {}", e);
            }
            if let Err(e) = conn.del::<String, ()>(new_key).await {
                slog::debug!(self.logs, "cache del failed (non-fatal): {}", e);
            }
        }
        let response = TagInfoResponse::from(info);
        let project_id = match repo_model::Entity::find_by_id(repo_id).one(&self.db).await {
            Ok(Some(r)) => r.project,
            Ok(None) => Uuid::nil(),
            Err(e) => {
                slog::warn!(
                    self.logs,
                    "failed to look up project_id for activity log: {}",
                    e
                );
                Uuid::nil()
            }
        };
        let user_uid = ctx.user().unwrap_or(Uuid::nil());
        let _ = self
            .project_log_activity(
                project_id,
                Some(repo_id),
                user_uid,
                super::super::project::activity::ActivityLogParams {
                    event_type: "tag_rename".to_string(),
                    title: format!(
                        "{} renamed tag '{}' to '{}'",
                        user_uid, query.old_name, query.new_name
                    ),
                    repo_id: Some(repo_id),
                    content: None,
                    event_id: None,
                    event_sub_id: None,
                    metadata: Some(
                        serde_json::json!({"old_name": query.old_name, "new_name": query.new_name}),
                    ),
                    is_private: false,
                },
            )
            .await;
        Ok(response)
    }
    pub async fn git_tag_update_message(
        &self,
        namespace: String,
        repo_name: String,
        request: TagUpdateMessageRequest,
        ctx: &Session,
    ) -> Result<TagInfoResponse, AppError> {
        let repo: repo::Model = self
            .utils_check_repo_admin(namespace.clone(), repo_name.clone(), ctx)
            .await?;
        let _repo_id = repo.id;
        let domain = git::GitDomain::from_model(repo)?;
        let tagger = git::CommitSignature {
            name: request.tagger_name,
            email: request.tagger_email,
            time_secs: chrono::Utc::now().timestamp(),
            offset_minutes: 0,
        };
        let info = domain.tag_update_message(&request.name, &request.message, &tagger)?;
        if let Ok(mut conn) = self.cache.conn().await {
            let list_key = format!("git:tag:list:{}:{}", namespace, repo_name);
            let get_key = format!("git:tag:get:{}:{}:{}", namespace, repo_name, request.name);
            if let Err(e) = conn.del::<String, ()>(list_key).await {
                slog::debug!(self.logs, "cache del failed (non-fatal): {}", e);
            }
            if let Err(e) = conn.del::<String, ()>(get_key).await {
                slog::debug!(self.logs, "cache del failed (non-fatal): {}", e);
            }
        }
        Ok(TagInfoResponse::from(info))
    }
}
