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::commit_service_client::CommitServiceClient;
use serde_json::{Value, json};

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

pub struct GitCommitHistoryTool;

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

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

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

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

    fn description(&self) -> &'static str {
        "List recent commits on a branch. Returns commit OID, message, author, and timestamp."
    }

    fn schema(&self) -> Value {
        json!({
            "type": "object",
            "properties": {
                "workspace": {
                    "type": "string",
                    "description": "Workspace name (e.g. 'my-org')"
                },
                "repo": {
                    "type": "string",
                    "description": "Repository name"
                },
                "branch": {
                    "type": "string",
                    "description": "Branch name (optional, defaults to default branch)"
                },
                "limit": {
                    "type": "integer",
                    "description": "Max commits to return (default 20, max 100)"
                },
                "skip": {
                    "type": "integer",
                    "description": "Number of commits to skip (for pagination)"
                }
            },
            "required": ["workspace", "repo"]
        })
    }

    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 branch = arg_opt_str(&args, "branch").map(String::from);
        let limit = arg_u64(&args, "limit", 20).min(100);
        let skip = arg_u64(&args, "skip", 0);

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

        let mut client = CommitServiceClient::new(git.channel.clone());
        let resp = client
            .commit_history(p::CommitHistoryRequest {
                repo_id: repo.id.to_string(),
                limit,
                skip,
                sort: p::CommitWalkSort::Time as i32,
                branch,
            })
            .await
            .map_err(rpc_err)?
            .into_inner();

        let commits: Vec<Value> = resp
            .commits
            .iter()
            .map(|c| {
                json!({
                    "oid": c.oid.as_ref().map(|o| &o.value).unwrap_or(&String::new()),
                    "summary": c.summary,
                    "message": c.message,
                    "author_name": c.author.as_ref().map(|a| &a.name).unwrap_or(&String::new()),
                    "author_email": c.author.as_ref().map(|a| &a.email).unwrap_or(&String::new()),
                    "time": c.author.as_ref().map(|a| a.time_secs).unwrap_or(0),
                })
            })
            .collect();

        Ok(json!({ "commits": commits, "count": commits.len() }))
    }
}

pub struct GitCommitInfoTool;

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

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

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

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

    fn description(&self) -> &'static str {
        "Get detailed information about a specific commit by its OID."
    }

    fn schema(&self) -> Value {
        json!({
            "type": "object",
            "properties": {
                "workspace": { "type": "string", "description": "Workspace name" },
                "repo": { "type": "string", "description": "Repository name" },
                "oid": { "type": "string", "description": "Commit OID (SHA)" }
            },
            "required": ["workspace", "repo", "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 oid = arg_str(&args, "oid")?;

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

        let mut client = CommitServiceClient::new(git.channel.clone());
        let resp = client
            .commit_info(p::CommitInfoRequest {
                repo_id: repo.id.to_string(),
                oid: Some(p::ObjectId {
                    value: oid.to_string(),
                }),
            })
            .await
            .map_err(rpc_err)?
            .into_inner();

        let c = resp
            .commit
            .ok_or_else(|| AiError::Response("commit not found".to_string()))?;
        let parent_ids: Vec<String> =
            c.parent_ids.iter().map(|o| o.value.clone()).collect();

        Ok(json!({
            "oid": c.oid.as_ref().map(|o| &o.value),
            "summary": c.summary,
            "message": c.message,
            "author_name": c.author.as_ref().map(|a| &a.name),
            "author_email": c.author.as_ref().map(|a| &a.email),
            "author_time": c.author.as_ref().map(|a| a.time_secs),
            "committer_name": c.committer.as_ref().map(|a| &a.name),
            "tree_id": c.tree_id.as_ref().map(|o| &o.value),
            "parent_ids": parent_ids,
        }))
    }
}

pub struct GitCommitExistsTool;

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

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

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

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

    fn description(&self) -> &'static str {
        "Check whether a specific commit OID exists in the repository."
    }

    fn schema(&self) -> Value {
        json!({
            "type": "object",
            "properties": {
                "workspace": { "type": "string", "description": "Workspace name" },
                "repo": { "type": "string", "description": "Repository name" },
                "oid": { "type": "string", "description": "Commit OID (SHA)" }
            },
            "required": ["workspace", "repo", "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 oid = arg_str(&args, "oid")?;

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

        let mut client = CommitServiceClient::new(git.channel.clone());
        let resp = client
            .commit_exists(p::CommitExistsRequest {
                repo_id: repo.id.to_string(),
                oid: Some(p::ObjectId {
                    value: oid.to_string(),
                }),
            })
            .await
            .map_err(rpc_err)?
            .into_inner();

        Ok(json!({ "exists": resp.exists }))
    }
}

pub struct GitCherryPickTool;

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

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

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

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

    fn description(&self) -> &'static str {
        "Cherry-pick a commit onto the current branch. Requires write access."
    }

    fn schema(&self) -> Value {
        json!({
            "type": "object",
            "properties": {
                "workspace": { "type": "string", "description": "Workspace name" },
                "repo": { "type": "string", "description": "Repository name" },
                "oid": { "type": "string", "description": "Commit OID to cherry-pick" },
                "message": { "type": "string", "description": "Override commit message (optional)" },
                "update_ref": { "type": "string", "description": "Branch ref to update (optional)" }
            },
            "required": ["workspace", "repo", "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 oid = arg_str(&args, "oid")?;
        let message = args
            .get("message")
            .and_then(|v| v.as_str())
            .map(String::from);
        let update_ref = arg_opt_str(&args, "update_ref").map(String::from);

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

        let mut client = CommitServiceClient::new(git.channel.clone());
        let resp = client
            .cherry_pick(p::CherryPickRequest {
                repo_id: repo.id.to_string(),
                params: Some(p::CommitCherryPickParams {
                    cherrypick_oid: Some(p::ObjectId {
                        value: oid.to_string(),
                    }),
                    message,
                    update_ref,
                    ..Default::default()
                }),
            })
            .await
            .map_err(rpc_err)?
            .into_inner();

        Ok(json!({
            "success": true,
            "new_oid": resp.oid.as_ref().map(|o| &o.value),
        }))
    }
}

pub struct GitCommitCreateTool;

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

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

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

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

    fn description(&self) -> &'static str {
        "Create a commit with new or updated files in a workspace repository. Author is set to the requesting user, committer is redpanda <redpanda@gitdata.ai>. Provide workspace name, repo name, branch, commit message, and a list of files (path and content for each)."
    }

    fn schema(&self) -> Value {
        json!({
            "type": "object",
            "properties": {
                "workspace": {
                    "type": "string",
                    "description": "Workspace name (e.g. 'my-org')"
                },
                "repo": {
                    "type": "string",
                    "description": "Repository name"
                },
                "branch": {
                    "type": "string",
                    "description": "Branch name to commit to (e.g. 'main'). If the branch does not exist, it will be created."
                },
                "message": {
                    "type": "string",
                    "description": "Commit message"
                },
                "files": {
                    "type": "array",
                    "description": "List of file changes",
                    "items": {
                        "type": "object",
                        "properties": {
                            "path": {
                                "type": "string",
                                "description": "File path relative to repo root (e.g. 'src/main.rs')"
                            },
                            "content": {
                                "type": "string",
                                "description": "File content as a string"
                            }
                        },
                        "required": ["path", "content"]
                    }
                }
            },
            "required": ["workspace", "repo", "branch", "message", "files"]
        })
    }

    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 branch = arg_str(&args, "branch")?;
        let message = arg_str(&args, "message")?;

        let files_val = args
            .get("files")
            .and_then(|v| v.as_array())
            .ok_or_else(|| {
                AiError::Config(
                    "'files' must be an array of {path, content} objects"
                        .to_string(),
                )
            })?;

        let mut file_changes: Vec<super::helpers::FileChange> = Vec::new();
        for f in files_val {
            let path =
                f.get("path").and_then(|v| v.as_str()).ok_or_else(|| {
                    AiError::Config(
                        "each file must have a 'path' field".to_string(),
                    )
                })?;
            let content =
                f.get("content").and_then(|v| v.as_str()).ok_or_else(|| {
                    AiError::Config(
                        "each file must have a 'content' field".to_string(),
                    )
                })?;
            file_changes.push(super::helpers::FileChange {
                path: path.to_string(),
                content: content.as_bytes().to_vec(),
            });
        }

        if file_changes.is_empty() {
            return Err(AiError::Config(
                "'files' array must not be empty".to_string(),
            ));
        }

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

        let mut client = CommitServiceClient::new(git.channel.clone());
        let resp = client
            .create_commit(tonic::Request::new(p::CreateCommitRequest {
                repo_id: repo.id.to_string(),
                branch: branch.to_string(),
                message: message.to_string(),
                author_name: ctx.user_id.to_string(),
                author_email: format!("{}@gitdata.ai", ctx.user_id),
                committer_name: "redpanda".to_string(),
                committer_email: "redpanda@gitdata.ai".to_string(),
                files: file_changes
                    .into_iter()
                    .map(|fc| p::FileChange {
                        path: fc.path,
                        content: fc.content,
                    })
                    .collect(),
            }))
            .await
            .map_err(rpc_err)?
            .into_inner();

        Ok(json!({
            "success": true,
            "oid": resp.oid.as_ref().map(|o| &o.value),
            "files_committed": files_val.len(),
        }))
    }
}
