use crate::AppService;
use crate::error::AppError;
use crate::git::{BlameOptions, CommitBlameHunk, CommitBlameLine};
use redis::AsyncCommands;
use serde::{Deserialize, Serialize};
use session::Session;
use utoipa::ToSchema;

#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
pub struct BlameQuery {
    #[serde(default)]
    pub commit_oid: String,
    #[serde(default)]
    pub path: String,
    #[serde(default)]
    pub min_line: Option<usize>,
    #[serde(default)]
    pub max_line: Option<usize>,
    #[serde(default)]
    pub track_copies_same_file: bool,
    #[serde(default)]
    pub track_copies_same_commit_moves: bool,
    #[serde(default)]
    pub ignore_whitespace: bool,
}

impl BlameQuery {
    fn to_blame_options(&self) -> BlameOptions {
        let mut opts = BlameOptions::new();
        if let Some(min) = self.min_line {
            opts.min_line = Some(min);
        }
        if let Some(max) = self.max_line {
            opts.max_line = Some(max);
        }
        opts.track_copies_same_file = self.track_copies_same_file;
        opts.track_copies_same_commit_moves = self.track_copies_same_commit_moves;
        opts.ignore_whitespace = self.ignore_whitespace;
        opts
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct BlameHunkResponse {
    pub commit_oid: String,
    pub final_start_line: u32,
    pub final_lines: u32,
    pub orig_start_line: u32,
    pub orig_lines: u32,
    pub boundary: bool,
    pub orig_path: Option<String>,
}

impl From<CommitBlameHunk> for BlameHunkResponse {
    fn from(h: CommitBlameHunk) -> Self {
        Self {
            commit_oid: h.commit_oid.to_string(),
            final_start_line: h.final_start_line,
            final_lines: h.final_lines,
            orig_start_line: h.orig_start_line,
            orig_lines: h.orig_lines,
            boundary: h.boundary,
            orig_path: h.orig_path,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct BlameLineResponse {
    pub commit_oid: String,
    pub line_no: u32,
    pub content: String,
    pub orig_path: Option<String>,
}

impl From<CommitBlameLine> for BlameLineResponse {
    fn from(l: CommitBlameLine) -> Self {
        Self {
            commit_oid: l.commit_oid.to_string(),
            line_no: l.line_no,
            content: l.content,
            orig_path: l.orig_path,
        }
    }
}

impl AppService {
    pub async fn git_blame_file(
        &self,
        namespace: String,
        repo_name: String,
        query: BlameQuery,
        ctx: &Session,
    ) -> Result<Vec<BlameHunkResponse>, AppError> {
        let repo = self
            .utils_find_repo(namespace.clone(), repo_name.clone(), ctx)
            .await?;
        let commit_oid_str = query.commit_oid.clone();
        let path = query.path.clone();
        let blame_opts = query.to_blame_options();
        let cache_key = format!(
            "git:cache:{}:{}:{}:{}:{:?}:{:?}:{}:{}:{}",
            namespace,
            repo_name,
            path,
            query.commit_oid,
            blame_opts.max_line,
            blame_opts.min_line,
            blame_opts.ignore_whitespace,
            blame_opts.track_copies_same_file,
            blame_opts.track_copies_same_commit_moves,
        );

        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 path_clone = path.clone();
        let opts_clone = blame_opts.clone();

        let hunks: Vec<CommitBlameHunk> = tokio::task::spawn_blocking(move || {
            let domain = git::GitDomain::from_model(repo_clone)?;
            let commit_oid = git::CommitOid::new(&commit_oid_str);
            domain.blame_file(&commit_oid, &path_clone, Some(opts_clone))
        })
        .await
        .map_err(|e| AppError::InternalServerError(format!("Task join error: {}", e)))?
        .map_err(AppError::from)?;

        let response: Vec<BlameHunkResponse> =
            hunks.into_iter().map(BlameHunkResponse::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_blame_lines(
        &self,
        namespace: String,
        repo_name: String,
        query: BlameQuery,
        ctx: &Session,
    ) -> Result<Vec<BlameLineResponse>, AppError> {
        let repo = self
            .utils_find_repo(namespace.clone(), repo_name.clone(), ctx)
            .await?;
        let commit_oid_str = query.commit_oid.clone();
        let path = query.path.clone();
        let blame_opts = query.to_blame_options();
        let cache_key = format!(
            "git:cache:blame_lines:{}:{}:{}:{}:{:?}:{:?}:{}:{}:{}",
            namespace,
            repo_name,
            path,
            query.commit_oid,
            blame_opts.max_line,
            blame_opts.min_line,
            blame_opts.ignore_whitespace,
            blame_opts.track_copies_same_file,
            blame_opts.track_copies_same_commit_moves,
        );

        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 path_clone = path.clone();
        let opts_clone = blame_opts.clone();

        let lines: Vec<CommitBlameLine> = tokio::task::spawn_blocking(move || {
            let domain = git::GitDomain::from_model(repo_clone)?;
            let commit_oid = git::CommitOid::new(&commit_oid_str);
            domain.blame_lines(&commit_oid, &path_clone, Some(opts_clone))
        })
        .await
        .map_err(|e| AppError::InternalServerError(format!("Task join error: {}", e)))?
        .map_err(AppError::from)?;

        let response: Vec<BlameLineResponse> =
            lines.into_iter().map(BlameLineResponse::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_blame_hunk_at(
        &self,
        namespace: String,
        repo_name: String,
        query: BlameQuery,
        line_no: usize,
        ctx: &Session,
    ) -> Result<BlameHunkResponse, AppError> {
        let repo = self
            .utils_find_repo(namespace.clone(), repo_name.clone(), ctx)
            .await?;
        let commit_oid_str = query.commit_oid.clone();
        let path = query.path.clone();

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

        Ok(BlameHunkResponse::from(hunk))
    }
}
