use crate::AppService;
use crate::error::AppError;
use crate::git::{TreeEntry, TreeInfo};
use redis::AsyncCommands;
use serde::{Deserialize, Serialize};
use session::Session;

#[derive(Debug, Clone, Deserialize)]
pub struct TreeGetQuery {
    #[serde(default)]
    pub oid: String,
}

#[derive(Debug, Clone, Deserialize)]
pub struct TreeEntryQuery {
    #[serde(default)]
    pub oid: String,
    pub index: usize,
}

#[derive(Debug, Clone, Deserialize)]
pub struct TreeEntryByPathQuery {
    #[serde(default)]
    pub oid: String,
    pub path: String,
}

#[derive(Debug, Clone, Deserialize)]
pub struct TreeEntryByCommitPathQuery {
    pub commit: String,
    pub path: String,
}

#[derive(Debug, Clone, Deserialize)]
pub struct TreeDiffQuery {
    pub old_tree: String,
    pub new_tree: String,
}

#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct TreeInfoResponse {
    pub oid: String,
    pub entry_count: usize,
    pub is_empty: bool,
}

impl From<TreeInfo> for TreeInfoResponse {
    fn from(t: TreeInfo) -> Self {
        Self {
            oid: t.oid.to_string(),
            entry_count: t.entry_count,
            is_empty: t.is_empty,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct TreeEntryResponse {
    pub name: String,
    pub oid: String,
    pub kind: String,
    pub filemode: u32,
    pub is_binary: bool,
}

impl From<TreeEntry> for TreeEntryResponse {
    fn from(e: TreeEntry) -> Self {
        Self {
            name: e.name,
            oid: e.oid.to_string(),
            kind: e.kind,
            filemode: e.filemode,
            is_binary: e.is_binary,
        }
    }
}

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

#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct TreeEntryCountResponse {
    pub oid: String,
    pub count: usize,
}

#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct TreeIsEmptyResponse {
    pub oid: String,
    pub is_empty: bool,
}

#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct TreeDiffStatsResponse {
    pub old_tree: String,
    pub new_tree: String,
    pub files_changed: usize,
    pub insertions: usize,
    pub deletions: usize,
}

impl AppService {
    pub async fn git_tree_get(
        &self,
        namespace: String,
        repo_name: String,
        query: TreeGetQuery,
        ctx: &Session,
    ) -> Result<TreeInfoResponse, AppError> {
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
        let oid_str = query.oid.clone();

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

        Ok(TreeInfoResponse::from(info))
    }

    pub async fn git_tree_exists(
        &self,
        namespace: String,
        repo_name: String,
        query: TreeGetQuery,
        ctx: &Session,
    ) -> Result<TreeExistsResponse, AppError> {
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
        let oid_str = query.oid.clone();

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

        Ok(TreeExistsResponse {
            oid: query.oid,
            exists,
        })
    }

    pub async fn git_tree_list(
        &self,
        namespace: String,
        repo_name: String,
        query: TreeGetQuery,
        ctx: &Session,
    ) -> Result<Vec<TreeEntryResponse>, AppError> {
        let repo = self
            .utils_find_repo(namespace.clone(), repo_name.clone(), ctx)
            .await?;
        let cache_key = format!("git:tree:list:{}:{}:{}", namespace, repo_name, query.oid);

        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 oid_str = query.oid.clone();

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

        let response: Vec<TreeEntryResponse> =
            entries.into_iter().map(TreeEntryResponse::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
            {
                slog::debug!(self.logs, "cache set failed (non-fatal): {}", e);
            }
        }

        Ok(response)
    }

    pub async fn git_tree_entry(
        &self,
        namespace: String,
        repo_name: String,
        query: TreeEntryQuery,
        ctx: &Session,
    ) -> Result<TreeEntryResponse, AppError> {
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
        let oid_str = query.oid.clone();
        let index = query.index;

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

        Ok(TreeEntryResponse::from(entry))
    }

    pub async fn git_tree_entry_by_path(
        &self,
        namespace: String,
        repo_name: String,
        query: TreeEntryByPathQuery,
        ctx: &Session,
    ) -> Result<TreeEntryResponse, AppError> {
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
        let oid_str = query.oid.clone();
        let path = query.path.clone();

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

        Ok(TreeEntryResponse::from(entry))
    }

    pub async fn git_tree_entry_by_commit_path(
        &self,
        namespace: String,
        repo_name: String,
        query: TreeEntryByCommitPathQuery,
        ctx: &Session,
    ) -> Result<TreeEntryResponse, AppError> {
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
        let commit_str = query.commit.clone();
        let path = query.path.clone();

        let entry = tokio::task::spawn_blocking(move || {
            let domain = git::GitDomain::from_model(repo)?;
            let commit = git::CommitOid::new(&commit_str);
            domain.tree_entry_by_path_from_commit(&commit, &path)
        })
        .await
        .map_err(|e| AppError::InternalServerError(format!("Task join error: {}", e)))?
        .map_err(AppError::from)?;

        Ok(TreeEntryResponse::from(entry))
    }

    pub async fn git_tree_entry_count(
        &self,
        namespace: String,
        repo_name: String,
        query: TreeGetQuery,
        ctx: &Session,
    ) -> Result<TreeEntryCountResponse, AppError> {
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
        let oid_str = query.oid.clone();

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

        Ok(TreeEntryCountResponse {
            oid: query.oid,
            count,
        })
    }

    pub async fn git_tree_is_empty(
        &self,
        namespace: String,
        repo_name: String,
        query: TreeGetQuery,
        ctx: &Session,
    ) -> Result<TreeIsEmptyResponse, AppError> {
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
        let oid_str = query.oid.clone();

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

        Ok(TreeIsEmptyResponse {
            oid: query.oid,
            is_empty,
        })
    }

    pub async fn git_tree_diffstats(
        &self,
        namespace: String,
        repo_name: String,
        query: TreeDiffQuery,
        ctx: &Session,
    ) -> Result<TreeDiffStatsResponse, AppError> {
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
        let old_tree_str = query.old_tree.clone();
        let new_tree_str = query.new_tree.clone();

        let stats = tokio::task::spawn_blocking(move || {
            let domain = git::GitDomain::from_model(repo)?;
            let old_tree = git::CommitOid::new(&old_tree_str);
            let new_tree = git::CommitOid::new(&new_tree_str);
            domain.tree_diffstats(&old_tree, &new_tree)
        })
        .await
        .map_err(|e| AppError::InternalServerError(format!("Task join error: {}", e)))?
        .map_err(AppError::from)?;

        Ok(TreeDiffStatsResponse {
            old_tree: query.old_tree,
            new_tree: query.new_tree,
            files_changed: stats.files_changed,
            insertions: stats.insertions,
            deletions: stats.deletions,
        })
    }
}
