use ai::error::{AiError, AiResult};
use ai::tool::tools::FunctionCall;
use async_trait::async_trait;
use git::rpc::proto as p;
use git::rpc::proto::diff_service_client::DiffServiceClient;
use serde_json::{Value, json};

use super::helpers::{arg_str, git_ctx, require_repo_member, rpc_err};
use crate::agent::run::AppAgentContext;

pub struct GitDiffStatsTool;

impl GitDiffStatsTool {
    pub fn new() -> Self {
        Self
    }
}

impl Default for GitDiffStatsTool {
    fn default() -> Self {
        Self::new()
    }
}

#[async_trait]
impl FunctionCall for GitDiffStatsTool {
    type Context = AppAgentContext;

    fn name(&self) -> &'static str {
        "git_diff_stats"
    }

    fn description(&self) -> &'static str {
        "Get diff statistics between two commits: files changed, insertions, deletions."
    }

    fn schema(&self) -> Value {
        json!({
            "type": "object",
            "properties": {
                "workspace": { "type": "string", "description": "Workspace name" },
                "repo": { "type": "string", "description": "Repository name" },
                "old_oid": { "type": "string", "description": "Base commit OID" },
                "new_oid": { "type": "string", "description": "Target commit OID" }
            },
            "required": ["workspace", "repo", "old_oid", "new_oid"]
        })
    }

    async fn call(
        &self,
        ctx: &mut AppAgentContext,
        args: Value,
    ) -> AiResult<Value> {
        let git = git_ctx(ctx)?;
        let workspace = arg_str(&args, "workspace")?;
        let repo_name = arg_str(&args, "repo")?;
        let old_oid = arg_str(&args, "old_oid")?;
        let new_oid = arg_str(&args, "new_oid")?;

        let repo =
            require_repo_member(git, ctx.user_id, workspace, repo_name).await?;

        let mut client = DiffServiceClient::new(git.channel.clone());
        let resp = client
            .diff_stats(p::DiffStatsRequest {
                repo_id: repo.id.to_string(),
                old_oid: Some(p::ObjectId {
                    value: old_oid.to_string(),
                }),
                new_oid: Some(p::ObjectId {
                    value: new_oid.to_string(),
                }),
                options: None,
            })
            .await
            .map_err(rpc_err)?
            .into_inner();

        let result = resp
            .result
            .ok_or_else(|| AiError::Response("no diff result".to_string()))?;
        let stats = result
            .stats
            .ok_or_else(|| AiError::Response("no stats".to_string()))?;

        let files: Vec<Value> = result.deltas.iter().map(|d| {
            let status = match p::DiffDeltaStatus::try_from(d.status) {
                Ok(p::DiffDeltaStatus::Added) => "added",
                Ok(p::DiffDeltaStatus::Deleted) => "deleted",
                Ok(p::DiffDeltaStatus::Modified) => "modified",
                Ok(p::DiffDeltaStatus::Renamed) => "renamed",
                _ => "unknown",
            };
            let old_path = d.old_file.as_ref().and_then(|f| f.path.as_deref()).unwrap_or("");
            let new_path = d.new_file.as_ref().and_then(|f| f.path.as_deref()).unwrap_or("");
            json!({ "status": status, "old_path": old_path, "new_path": new_path, "hunks": d.hunks.len() })
        }).collect();

        Ok(json!({
            "files_changed": stats.files_changed,
            "insertions": stats.insertions,
            "deletions": stats.deletions,
            "files": files,
        }))
    }
}

pub struct GitDiffPatchTool;

impl GitDiffPatchTool {
    pub fn new() -> Self {
        Self
    }
}

impl Default for GitDiffPatchTool {
    fn default() -> Self {
        Self::new()
    }
}

#[async_trait]
impl FunctionCall for GitDiffPatchTool {
    type Context = AppAgentContext;

    fn name(&self) -> &'static str {
        "git_diff_patch"
    }

    fn description(&self) -> &'static str {
        "Get the full diff (unified format) between two commits, including line-level changes."
    }

    fn schema(&self) -> Value {
        json!({
            "type": "object",
            "properties": {
                "workspace": { "type": "string", "description": "Workspace name" },
                "repo": { "type": "string", "description": "Repository name" },
                "old_oid": { "type": "string", "description": "Base commit OID" },
                "new_oid": { "type": "string", "description": "Target commit OID" },
                "context_lines": { "type": "integer", "description": "Lines of context around changes (default 3)" }
            },
            "required": ["workspace", "repo", "old_oid", "new_oid"]
        })
    }

    async fn call(
        &self,
        ctx: &mut AppAgentContext,
        args: Value,
    ) -> AiResult<Value> {
        let git = git_ctx(ctx)?;
        let workspace = arg_str(&args, "workspace")?;
        let repo_name = arg_str(&args, "repo")?;
        let old_oid = arg_str(&args, "old_oid")?;
        let new_oid = arg_str(&args, "new_oid")?;
        let ctx_lines = args
            .get("context_lines")
            .and_then(|v| v.as_u64())
            .unwrap_or(3) as u32;

        let repo =
            require_repo_member(git, ctx.user_id, workspace, repo_name).await?;

        let mut client = DiffServiceClient::new(git.channel.clone());
        let resp = client
            .diff_patch(p::DiffPatchRequest {
                repo_id: repo.id.to_string(),
                old_oid: Some(p::ObjectId {
                    value: old_oid.to_string(),
                }),
                new_oid: Some(p::ObjectId {
                    value: new_oid.to_string(),
                }),
                options: Some(p::DiffOptions {
                    context_lines: ctx_lines,
                    ..Default::default()
                }),
            })
            .await
            .map_err(rpc_err)?
            .into_inner();

        let result = resp
            .result
            .ok_or_else(|| AiError::Response("no diff result".to_string()))?;
        let stats = result
            .stats
            .ok_or_else(|| AiError::Response("no stats".to_string()))?;

        let mut patch_text = String::new();
        for delta in &result.deltas {
            let status = match p::DiffDeltaStatus::try_from(delta.status) {
                Ok(p::DiffDeltaStatus::Added) => "added",
                Ok(p::DiffDeltaStatus::Deleted) => "deleted",
                Ok(p::DiffDeltaStatus::Modified) => "modified",
                Ok(p::DiffDeltaStatus::Renamed) => "renamed",
                _ => "unknown",
            };
            let old = delta
                .old_file
                .as_ref()
                .and_then(|f| f.path.as_deref())
                .unwrap_or("unknown");
            let new = delta
                .new_file
                .as_ref()
                .and_then(|f| f.path.as_deref())
                .unwrap_or("unknown");
            patch_text.push_str(&format!(
                "--- {}\n+++ {}\n@@ status: {status} @@\n",
                old, new
            ));

            for hunk in &delta.hunks {
                patch_text.push_str(&hunk.header);
                patch_text.push('\n');
                for line in &delta.lines {
                    patch_text.push_str(&format!(
                        "{}{}\n",
                        line.origin, line.content
                    ));
                }
                patch_text.push('\n');
            }
        }

        let truncated = patch_text.len() > 32_000;
        if truncated {
            patch_text = format!("{}...(truncated)", &patch_text[..32_000]);
        }

        Ok(json!({
            "files_changed": stats.files_changed,
            "insertions": stats.insertions,
            "deletions": stats.deletions,
            "patch": patch_text,
            "truncated": truncated,
        }))
    }
}
