use crate::AppService;
use crate::error::AppError;
use crate::git::{DiffDelta, DiffOptions, DiffResult, DiffStats, GitError, SideBySideDiffResult};
use redis::AsyncCommands;
use serde::{Deserialize, Serialize};
use session::Session;

#[derive(Debug, Clone, Deserialize, utoipa::ToSchema, utoipa::IntoParams)]
pub struct DiffQuery {
    #[serde(default)]
    pub old_tree: Option<String>,
    #[serde(default)]
    pub new_tree: Option<String>,
    #[serde(default)]
    pub context_lines: Option<u32>,
    #[serde(default)]
    pub pathspec: Option<Vec<String>>,
    #[serde(default)]
    pub include_untracked: Option<bool>,
    #[serde(default)]
    pub include_ignored: Option<bool>,
    #[serde(default)]
    pub ignore_whitespace: Option<bool>,
    #[serde(default)]
    pub force_text: Option<bool>,
    #[serde(default)]
    pub reverse: Option<bool>,
}

impl DiffQuery {
    fn to_diff_options(&self) -> DiffOptions {
        let mut opts = DiffOptions::new();
        if let Some(n) = self.context_lines {
            opts = opts.context_lines(n);
        }
        if let Some(ref paths) = self.pathspec {
            for p in paths {
                opts = opts.pathspec(p);
            }
        }
        if self.include_untracked.unwrap_or(false) {
            opts = opts.include_untracked();
        }
        if self.include_ignored.unwrap_or(false) {
            opts = opts.include_ignored();
        }
        if self.ignore_whitespace.unwrap_or(false) {
            opts = opts.ignore_whitespace();
        }
        if self.force_text.unwrap_or(false) {
            opts = opts.force_text();
        }
        if self.reverse.unwrap_or(false) {
            opts = opts.reverse();
        }
        opts
    }
}

#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
pub struct DiffCommitQuery {
    #[serde(default)]
    pub commit: String,
    #[serde(flatten)]
    #[serde(default)]
    pub diff_opts: DiffQuery,
}

#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct DiffStatsResponse {
    pub files_changed: usize,
    pub insertions: usize,
    pub deletions: usize,
}

impl From<DiffStats> for DiffStatsResponse {
    fn from(s: DiffStats) -> Self {
        Self {
            files_changed: s.files_changed,
            insertions: s.insertions,
            deletions: s.deletions,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct DiffDeltaResponse {
    pub status: String,
    pub old_file: DiffFileResponse,
    pub new_file: DiffFileResponse,
    pub nfiles: u16,
    pub hunks: Vec<DiffHunkResponse>,
    pub lines: Vec<DiffLineResponse>,
}

#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct DiffFileResponse {
    pub oid: Option<String>,
    pub path: Option<String>,
    pub size: u64,
    pub is_binary: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct DiffHunkResponse {
    pub old_start: u32,
    pub old_lines: u32,
    pub new_start: u32,
    pub new_lines: u32,
    pub header: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct DiffLineResponse {
    pub content: String,
    pub origin: String,
    pub old_lineno: Option<u32>,
    pub new_lineno: Option<u32>,
    pub num_lines: u32,
    pub content_offset: i64,
}

#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct DiffResultResponse {
    pub stats: DiffStatsResponse,
    pub deltas: Vec<DiffDeltaResponse>,
}

impl From<DiffResult> for DiffResultResponse {
    fn from(r: DiffResult) -> Self {
        Self {
            stats: DiffStatsResponse::from(r.stats),
            deltas: r.deltas.into_iter().map(DiffDeltaResponse::from).collect(),
        }
    }
}

impl From<DiffDelta> for DiffDeltaResponse {
    fn from(d: DiffDelta) -> Self {
        Self {
            status: format!("{:?}", d.status).to_lowercase(),
            old_file: DiffFileResponse {
                oid: d.old_file.oid.map(|o| o.to_string()),
                path: d.old_file.path,
                size: d.old_file.size,
                is_binary: d.old_file.is_binary,
            },
            new_file: DiffFileResponse {
                oid: d.new_file.oid.map(|o| o.to_string()),
                path: d.new_file.path,
                size: d.new_file.size,
                is_binary: d.new_file.is_binary,
            },
            nfiles: d.nfiles,
            hunks: d
                .hunks
                .into_iter()
                .map(|h| DiffHunkResponse {
                    old_start: h.old_start,
                    old_lines: h.old_lines,
                    new_start: h.new_start,
                    new_lines: h.new_lines,
                    header: h.header,
                })
                .collect(),
            lines: d
                .lines
                .into_iter()
                .map(|l| DiffLineResponse {
                    content: l.content,
                    origin: l.origin.to_string(),
                    old_lineno: l.old_lineno,
                    new_lineno: l.new_lineno,
                    num_lines: l.num_lines,
                    content_offset: l.content_offset,
                })
                .collect(),
        }
    }
}

#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct DiffPatchIdResponse {
    pub old_tree: String,
    pub new_tree: String,
    pub patch_id: String,
}

// ---------------------------------------------------------------------------
// Side-by-side diff response types
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "lowercase")]
pub enum SideBySideChangeTypeResponse {
    Unchanged,
    Added,
    Removed,
    Modified,
    Empty,
}

impl From<crate::git::SideBySideChangeType> for SideBySideChangeTypeResponse {
    fn from(v: crate::git::SideBySideChangeType) -> Self {
        match v {
            crate::git::SideBySideChangeType::Unchanged => Self::Unchanged,
            crate::git::SideBySideChangeType::Added => Self::Added,
            crate::git::SideBySideChangeType::Removed => Self::Removed,
            crate::git::SideBySideChangeType::Modified => Self::Modified,
            crate::git::SideBySideChangeType::Empty => Self::Empty,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct SideBySideLineResponse {
    pub left_line_no: Option<u32>,
    pub right_line_no: Option<u32>,
    pub left_content: String,
    pub right_content: String,
    pub change_type: SideBySideChangeTypeResponse,
}

#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct SideBySideFileResponse {
    pub path: String,
    pub additions: usize,
    pub deletions: usize,
    pub is_binary: bool,
    pub is_rename: bool,
    pub lines: Vec<SideBySideLineResponse>,
}

#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct SideBySideDiffResponse {
    pub files: Vec<SideBySideFileResponse>,
    pub total_additions: usize,
    pub total_deletions: usize,
}

impl From<SideBySideDiffResult> for SideBySideDiffResponse {
    fn from(r: SideBySideDiffResult) -> Self {
        Self {
            files: r
                .files
                .into_iter()
                .map(|f| SideBySideFileResponse {
                    path: f.path,
                    additions: f.additions,
                    deletions: f.deletions,
                    is_binary: f.is_binary,
                    is_rename: f.is_rename,
                    lines: f
                        .lines
                        .into_iter()
                        .map(|l| SideBySideLineResponse {
                            left_line_no: l.left_line_no,
                            right_line_no: l.right_line_no,
                            left_content: l.left_content,
                            right_content: l.right_content,
                            change_type: l.change_type.into(),
                        })
                        .collect(),
                })
                .collect(),
            total_additions: r.total_additions,
            total_deletions: r.total_deletions,
        }
    }
}

/// Query parameters for side-by-side diff.
#[derive(Debug, Clone, Deserialize, utoipa::IntoParams, utoipa::ToSchema)]
pub struct SideBySideDiffQuery {
    /// OID (SHA) of the base / old commit or tree.
    pub base: String,
    /// OID (SHA) of the head / new commit or tree.
    pub head: String,
    /// Optional path filter — only include files matching this prefix.
    #[serde(default)]
    pub pathspec: Option<Vec<String>>,
    /// Number of context lines around changes (default 3).
    #[serde(default)]
    pub context_lines: Option<u32>,
}

impl AppService {
    pub async fn git_diff_tree_to_tree(
        &self,
        namespace: String,
        repo_name: String,
        query: DiffQuery,
        ctx: &Session,
    ) -> Result<DiffResultResponse, AppError> {
        let repo = self
            .utils_find_repo(namespace.clone(), repo_name.clone(), ctx)
            .await?;
        let cache_key = format!(
            "git:diff:{}:{}:{}:{}",
            namespace,
            repo_name,
            query.old_tree.as_deref().unwrap_or(""),
            query.new_tree.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 old_tree = query
            .old_tree
            .as_ref()
            .map(|s| git::CommitOid::new(s.as_str()));
        let new_tree = git::CommitOid::new(
            query
                .new_tree
                .as_deref()
                .ok_or_else(|| AppError::BadRequest("new_tree is required".into()))?,
        );
        let opts = query.to_diff_options();

        let result = tokio::task::spawn_blocking(move || {
            let domain = git::GitDomain::from_model(repo)?;
            domain.diff_tree_to_tree(old_tree.as_ref(), Some(&new_tree), Some(opts))
        })
        .await
        .map_err(|e| AppError::InternalServerError(format!("Task join error: {}", e)))?
        .map_err(AppError::from)?;

        let response = DiffResultResponse::from(result);

        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_diff_commit_to_workdir(
        &self,
        namespace: String,
        repo_name: String,
        query: DiffCommitQuery,
        ctx: &Session,
    ) -> Result<DiffResultResponse, AppError> {
        let repo = self
            .utils_find_repo(namespace.clone(), repo_name.clone(), ctx)
            .await?;
        let cache_key = format!("git:diff:c2wd:{}:{}:{}", namespace, repo_name, query.commit,);

        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 commit = git::CommitOid::new(&query.commit);
        let opts = query.diff_opts.to_diff_options();

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

        let response = DiffResultResponse::from(result);

        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_diff_commit_to_index(
        &self,
        namespace: String,
        repo_name: String,
        query: DiffCommitQuery,
        ctx: &Session,
    ) -> Result<DiffResultResponse, AppError> {
        let repo = self
            .utils_find_repo(namespace.clone(), repo_name.clone(), ctx)
            .await?;
        let cache_key = format!(
            "git:diff:c2idx:{}:{}:{}",
            namespace, repo_name, query.commit,
        );

        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 commit = git::CommitOid::new(&query.commit);
        let opts = query.diff_opts.to_diff_options();

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

        let response = DiffResultResponse::from(result);

        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_diff_workdir_to_index(
        &self,
        namespace: String,
        repo_name: String,
        query: DiffQuery,
        ctx: &Session,
    ) -> Result<DiffResultResponse, AppError> {
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
        let opts = query.to_diff_options();

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

        Ok(DiffResultResponse::from(result))
    }

    pub async fn git_diff_index_to_tree(
        &self,
        namespace: String,
        repo_name: String,
        query: DiffQuery,
        ctx: &Session,
    ) -> Result<DiffResultResponse, AppError> {
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
        let tree = git::CommitOid::new(
            query
                .new_tree
                .as_deref()
                .ok_or_else(|| AppError::BadRequest("new_tree is required".into()))?,
        );
        let opts = query.to_diff_options();

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

        Ok(DiffResultResponse::from(result))
    }

    pub async fn git_diff_stats(
        &self,
        namespace: String,
        repo_name: String,
        query: DiffQuery,
        ctx: &Session,
    ) -> Result<DiffStatsResponse, AppError> {
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
        let old_tree = git::CommitOid::new(
            query
                .old_tree
                .as_deref()
                .ok_or_else(|| AppError::BadRequest("old_tree is required".into()))?,
        );
        let new_tree = git::CommitOid::new(
            query
                .new_tree
                .as_deref()
                .ok_or_else(|| AppError::BadRequest("new_tree is required".into()))?,
        );

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

        Ok(DiffStatsResponse::from(stats))
    }

    pub async fn git_diff_patch_id(
        &self,
        namespace: String,
        repo_name: String,
        query: DiffQuery,
        ctx: &Session,
    ) -> Result<DiffPatchIdResponse, AppError> {
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
        let old_tree = git::CommitOid::new(
            query
                .old_tree
                .as_deref()
                .ok_or_else(|| AppError::BadRequest("old_tree is required".into()))?,
        );
        let new_tree = git::CommitOid::new(
            query
                .new_tree
                .as_deref()
                .ok_or_else(|| AppError::BadRequest("new_tree is required".into()))?,
        );

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

        Ok(DiffPatchIdResponse {
            old_tree: query.old_tree.unwrap_or_default(),
            new_tree: query.new_tree.unwrap_or_default(),
            patch_id,
        })
    }

    /// Generate a side-by-side diff between two commits or trees.
    pub async fn git_diff_side_by_side(
        &self,
        namespace: String,
        repo_name: String,
        query: SideBySideDiffQuery,
        ctx: &Session,
    ) -> Result<SideBySideDiffResponse, AppError> {
        let repo = self
            .utils_find_repo(namespace.clone(), repo_name.clone(), ctx)
            .await?;

        let cache_key = format!(
            "git:diff:sbs:{}:{}:{}:{}",
            namespace, repo_name, query.base, query.head,
        );

        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::<SideBySideDiffResponse>(&cached) {
                    return Ok(cached);
                }
            }
        }

        let base = git::CommitOid::new(&query.base);
        let head = git::CommitOid::new(&query.head);

        let mut opts = DiffOptions::new();
        if let Some(n) = query.context_lines {
            opts = opts.context_lines(n);
        }
        if let Some(ref paths) = query.pathspec {
            for p in paths {
                opts = opts.pathspec(p);
            }
        }

        let result = tokio::task::spawn_blocking(move || {
            let domain = git::GitDomain::from_model(repo)?;
            let diff_result = domain.diff_tree_to_tree(Some(&base), Some(&head), Some(opts))?;
            Ok::<_, GitError>(git::diff_to_side_by_side(&diff_result))
        })
        .await
        .map_err(|e| AppError::InternalServerError(format!("Task join error: {}", e)))?
        .map_err(AppError::from)?;

        let response = SideBySideDiffResponse::from(result);

        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)
    }
}
