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

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

pub struct GitTagListTool;

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

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

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

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

    fn description(&self) -> &'static str {
        "List all tags in a repository."
    }

    fn schema(&self) -> Value {
        json!({
            "type": "object",
            "properties": {
                "workspace": { "type": "string", "description": "Workspace name" },
                "repo": { "type": "string", "description": "Repository name" }
            },
            "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 repo =
            require_repo_member(git, ctx.user_id, workspace, repo_name).await?;

        let mut client = TagServiceClient::new(git.channel.clone());
        let resp = client
            .tag_list(p::TagListRequest {
                repo_id: repo.id.to_string(),
            })
            .await
            .map_err(rpc_err)?
            .into_inner();

        let tags: Vec<Value> = resp
            .tags
            .iter()
            .map(|t| {
                json!({
                    "name": t.name,
                    "oid": t.oid.as_ref().map(|o| &o.value),
                    "target": t.target.as_ref().map(|o| &o.value),
                    "is_annotated": t.is_annotated,
                    "message": t.message,
                    "tagger": t.tagger,
                })
            })
            .collect();

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

pub struct GitCreateTagTool;

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

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

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

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

    fn description(&self) -> &'static str {
        "Create a new tag pointing at a commit."
    }

    fn schema(&self) -> Value {
        json!({
            "type": "object",
            "properties": {
                "workspace": { "type": "string", "description": "Workspace name" },
                "repo": { "type": "string", "description": "Repository name" },
                "name": { "type": "string", "description": "Tag name" },
                "target_oid": { "type": "string", "description": "Target commit OID" },
                "message": { "type": "string", "description": "Tag message (for annotated tags)" },
                "force": { "type": "boolean", "description": "Force overwrite existing tag" }
            },
            "required": ["workspace", "repo", "name", "target_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 name = arg_str(&args, "name")?;
        let target_oid = arg_str(&args, "target_oid")?;
        let message = arg_opt_str(&args, "message").map(String::from);
        let force =
            args.get("force").and_then(|v| v.as_bool()).unwrap_or(false);

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

        let mut client = TagServiceClient::new(git.channel.clone());
        let resp = client
            .tag_init(p::TagInitRequest {
                repo_id: repo.id.to_string(),
                params: Some(p::TagInitParams {
                    name: name.to_string(),
                    target: Some(p::ObjectId {
                        value: target_oid.to_string(),
                    }),
                    message,
                    tagger: None,
                    force,
                }),
            })
            .await
            .map_err(rpc_err)?
            .into_inner();

        let oid = resp.oid.map(|o| o.value).unwrap_or_default();
        Ok(json!({ "success": true, "tag": name, "oid": oid }))
    }
}

pub struct GitTagInfoTool;

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

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

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

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

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

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

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

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

        let mut client = TagServiceClient::new(git.channel.clone());
        let resp = client
            .tag_info(p::TagInfoRequest {
                repo_id: repo.id.to_string(),
                name: name.to_string(),
            })
            .await
            .map_err(rpc_err)?
            .into_inner();

        let t = resp.tag.ok_or_else(|| {
            AiError::Config(format!("tag '{name}' not found"))
        })?;
        Ok(json!({
            "name": t.name,
            "oid": t.oid.as_ref().map(|o| &o.value),
            "target": t.target.as_ref().map(|o| &o.value),
            "is_annotated": t.is_annotated,
            "message": t.message,
            "tagger": t.tagger,
            "tagger_email": t.tagger_email,
        }))
    }
}

pub struct GitDeleteTagTool;

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

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

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

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

    fn description(&self) -> &'static str {
        "Delete a tag from the repository. Requires write access."
    }

    fn schema(&self) -> Value {
        json!({
            "type": "object",
            "properties": {
                "workspace": { "type": "string", "description": "Workspace name" },
                "repo": { "type": "string", "description": "Repository name" },
                "name": { "type": "string", "description": "Tag name to delete" }
            },
            "required": ["workspace", "repo", "name"]
        })
    }

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

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

        let mut client = TagServiceClient::new(git.channel.clone());
        client
            .tag_delete(p::TagDeleteRequest {
                repo_id: repo.id.to_string(),
                params: Some(p::TagDeleteParams {
                    name: name.to_string(),
                }),
            })
            .await
            .map_err(rpc_err)?;

        Ok(json!({ "success": true, "tag": name }))
    }
}
