//! Tool wrappers for `linear_tools` using agentic-tools-core.
//!
//! Each tool delegates to the corresponding method on [`LinearTools`].

use crate::LinearTools;
use crate::models::ArchiveIssueResult;
use crate::models::CommentResult;
use crate::models::CreateIssueResult;
use crate::models::GetMetadataResult;
use crate::models::IssueDetails;
use crate::models::IssueResult;
use crate::models::SearchResult;
use crate::models::SetRelationResult;
use agentic_tools_core::Tool;
use agentic_tools_core::ToolContext;
use agentic_tools_core::ToolError;
use agentic_tools_core::ToolRegistry;
use futures::future::BoxFuture;
use schemars::JsonSchema;
use serde::Deserialize;
use std::sync::Arc;

// ============================================================================
// SearchIssues Tool
// ============================================================================

/// Input for `search_issues` tool.
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct SearchIssuesInput {
    /// Full-text search term (searches title, description, and optionally comments)
    #[serde(default)]
    pub query: Option<String>,
    /// Include comments in full-text search (default: true, only applies when query is provided)
    #[serde(default)]
    pub include_comments: Option<bool>,
    /// Filter by priority (0=None, 1=Urgent, 2=High, 3=Normal, 4=Low)
    #[serde(default)]
    pub priority: Option<i32>,
    /// Workflow state ID (UUID)
    #[serde(default)]
    pub state_id: Option<String>,
    /// Assignee user ID (UUID)
    #[serde(default)]
    pub assignee_id: Option<String>,
    /// Creator user ID (UUID)
    #[serde(default)]
    pub creator_id: Option<String>,
    /// Team ID (UUID)
    #[serde(default)]
    pub team_id: Option<String>,
    /// Project ID (UUID)
    #[serde(default)]
    pub project_id: Option<String>,
    /// Only issues created after this ISO 8601 date
    #[serde(default)]
    pub created_after: Option<String>,
    /// Only issues created before this ISO 8601 date
    #[serde(default)]
    pub created_before: Option<String>,
    /// Only issues updated after this ISO 8601 date
    #[serde(default)]
    pub updated_after: Option<String>,
    /// Only issues updated before this ISO 8601 date
    #[serde(default)]
    pub updated_before: Option<String>,
    /// Page size (default 50, max 100)
    #[serde(default)]
    pub first: Option<i32>,
    /// Pagination cursor
    #[serde(default)]
    pub after: Option<String>,
}

/// Tool for searching Linear issues.
#[derive(Clone)]
pub struct SearchIssuesTool {
    linear: Arc<LinearTools>,
}

impl SearchIssuesTool {
    pub fn new(linear: Arc<LinearTools>) -> Self {
        Self { linear }
    }
}

impl Tool for SearchIssuesTool {
    type Input = SearchIssuesInput;
    type Output = SearchResult;
    const NAME: &'static str = "linear_search_issues";
    const DESCRIPTION: &'static str = "Search Linear issues using full-text search and/or filters";

    fn call(
        &self,
        input: Self::Input,
        _ctx: &ToolContext,
    ) -> BoxFuture<'static, Result<Self::Output, ToolError>> {
        let linear = Arc::clone(&self.linear);
        Box::pin(async move {
            linear
                .search_issues(
                    input.query,
                    input.include_comments,
                    input.priority,
                    input.state_id,
                    input.assignee_id,
                    input.creator_id,
                    input.team_id,
                    input.project_id,
                    input.created_after,
                    input.created_before,
                    input.updated_after,
                    input.updated_before,
                    input.first,
                    input.after,
                )
                .await
                .map_err(|e| map_anyhow_to_tool_error(&e))
        })
    }
}

// ============================================================================
// ReadIssue Tool
// ============================================================================

/// Input for `read_issue` tool.
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct ReadIssueInput {
    /// Issue ID, identifier (e.g., ENG-245), or URL
    pub issue: String,
}

/// Tool for reading a single Linear issue.
#[derive(Clone)]
pub struct ReadIssueTool {
    linear: Arc<LinearTools>,
}

impl ReadIssueTool {
    pub fn new(linear: Arc<LinearTools>) -> Self {
        Self { linear }
    }
}

impl Tool for ReadIssueTool {
    type Input = ReadIssueInput;
    type Output = IssueDetails;
    const NAME: &'static str = "linear_read_issue";
    const DESCRIPTION: &'static str =
        "Read a Linear issue by ID, identifier (e.g., ENG-245), or URL";

    fn call(
        &self,
        input: Self::Input,
        _ctx: &ToolContext,
    ) -> BoxFuture<'static, Result<Self::Output, ToolError>> {
        let linear = Arc::clone(&self.linear);
        Box::pin(async move {
            linear
                .read_issue(input.issue)
                .await
                .map_err(|e| map_anyhow_to_tool_error(&e))
        })
    }
}

// ============================================================================
// CreateIssue Tool
// ============================================================================

/// Input for `create_issue` tool.
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct CreateIssueInput {
    /// Team ID (UUID) to create the issue in
    pub team_id: String,
    /// Issue title
    pub title: String,
    /// Issue description (markdown supported)
    #[serde(default)]
    pub description: Option<String>,
    /// Priority (0=None, 1=Urgent, 2=High, 3=Normal, 4=Low)
    #[serde(default)]
    pub priority: Option<i32>,
    /// Assignee user ID (UUID)
    #[serde(default)]
    pub assignee_id: Option<String>,
    /// Project ID (UUID)
    #[serde(default)]
    pub project_id: Option<String>,
    /// Workflow state ID (UUID)
    #[serde(default)]
    pub state_id: Option<String>,
    /// Parent issue ID (UUID) for sub-issues
    #[serde(default)]
    pub parent_id: Option<String>,
    /// Label IDs (UUIDs)
    #[serde(default)]
    pub label_ids: Vec<String>,
}

/// Tool for creating a new Linear issue.
#[derive(Clone)]
pub struct CreateIssueTool {
    linear: Arc<LinearTools>,
}

impl CreateIssueTool {
    pub fn new(linear: Arc<LinearTools>) -> Self {
        Self { linear }
    }
}

impl Tool for CreateIssueTool {
    type Input = CreateIssueInput;
    type Output = CreateIssueResult;
    const NAME: &'static str = "linear_create_issue";
    const DESCRIPTION: &'static str = "Create a new Linear issue in a team";

    fn call(
        &self,
        input: Self::Input,
        _ctx: &ToolContext,
    ) -> BoxFuture<'static, Result<Self::Output, ToolError>> {
        let linear = Arc::clone(&self.linear);
        Box::pin(async move {
            linear
                .create_issue(
                    input.team_id,
                    input.title,
                    input.description,
                    input.priority,
                    input.assignee_id,
                    input.project_id,
                    input.state_id,
                    input.parent_id,
                    input.label_ids,
                )
                .await
                .map_err(|e| map_anyhow_to_tool_error(&e))
        })
    }
}

// ============================================================================
// AddComment Tool
// ============================================================================

/// Input for `add_comment` tool.
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct AddCommentInput {
    /// Issue ID, identifier (e.g., ENG-245), or URL
    pub issue: String,
    /// Comment body (markdown supported)
    pub body: String,
    /// Parent comment ID for replies (UUID)
    #[serde(default)]
    pub parent_id: Option<String>,
}

/// Tool for adding a comment to a Linear issue.
#[derive(Clone)]
pub struct AddCommentTool {
    linear: Arc<LinearTools>,
}

impl AddCommentTool {
    pub fn new(linear: Arc<LinearTools>) -> Self {
        Self { linear }
    }
}

impl Tool for AddCommentTool {
    type Input = AddCommentInput;
    type Output = CommentResult;
    const NAME: &'static str = "linear_add_comment";
    const DESCRIPTION: &'static str = "Add a comment to a Linear issue";

    fn call(
        &self,
        input: Self::Input,
        _ctx: &ToolContext,
    ) -> BoxFuture<'static, Result<Self::Output, ToolError>> {
        let linear = Arc::clone(&self.linear);
        Box::pin(async move {
            linear
                .add_comment(input.issue, input.body, input.parent_id)
                .await
                .map_err(|e| map_anyhow_to_tool_error(&e))
        })
    }
}

// ============================================================================
// GetIssueComments Tool
// ============================================================================

/// Input for `get_issue_comments` tool.
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct GetIssueCommentsInput {
    /// Issue ID, identifier (e.g., ENG-245), or URL
    pub issue: String,
}

/// Tool for fetching comments on a Linear issue with implicit pagination.
#[derive(Clone)]
pub struct GetIssueCommentsTool {
    linear: Arc<LinearTools>,
}

impl GetIssueCommentsTool {
    pub fn new(linear: Arc<LinearTools>) -> Self {
        Self { linear }
    }
}

impl Tool for GetIssueCommentsTool {
    type Input = GetIssueCommentsInput;
    type Output = crate::models::CommentsResult;
    const NAME: &'static str = "linear_get_issue_comments";
    const DESCRIPTION: &'static str = "Get comments on a Linear issue. Returns 10 comments per call with implicit pagination - call again with the same issue to get more comments.";

    fn call(
        &self,
        input: Self::Input,
        _ctx: &ToolContext,
    ) -> BoxFuture<'static, Result<Self::Output, ToolError>> {
        let linear = Arc::clone(&self.linear);
        Box::pin(async move {
            linear
                .get_issue_comments(input.issue)
                .await
                .map_err(|e| map_anyhow_to_tool_error(&e))
        })
    }
}

// ============================================================================
// ArchiveIssue Tool
// ============================================================================

/// Input for `archive_issue` tool.
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct ArchiveIssueInput {
    /// Issue ID, identifier (e.g., ENG-245), or URL
    pub issue: String,
}

/// Tool for archiving a Linear issue.
#[derive(Clone)]
pub struct ArchiveIssueTool {
    linear: Arc<LinearTools>,
}

impl ArchiveIssueTool {
    pub fn new(linear: Arc<LinearTools>) -> Self {
        Self { linear }
    }
}

impl Tool for ArchiveIssueTool {
    type Input = ArchiveIssueInput;
    type Output = ArchiveIssueResult;
    const NAME: &'static str = "linear_archive_issue";
    const DESCRIPTION: &'static str =
        "Archive a Linear issue by ID, identifier (e.g., ENG-245), or URL";

    fn call(
        &self,
        input: Self::Input,
        _ctx: &ToolContext,
    ) -> BoxFuture<'static, Result<Self::Output, ToolError>> {
        let linear = Arc::clone(&self.linear);
        Box::pin(async move {
            linear
                .archive_issue(input.issue)
                .await
                .map_err(|e| map_anyhow_to_tool_error(&e))
        })
    }
}

// ============================================================================
// UpdateIssue Tool
// ============================================================================

/// Input for updating an existing Linear issue
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct UpdateIssueInput {
    /// Issue identifier (UUID, key like ENG-245, or Linear URL)
    pub issue: String,
    /// New title for the issue
    #[serde(default)]
    pub title: Option<String>,
    /// New description (markdown supported)
    #[serde(default)]
    pub description: Option<String>,
    /// Priority: 0=None, 1=Urgent, 2=High, 3=Medium, 4=Low
    #[serde(default)]
    pub priority: Option<i32>,
    /// Assignee user ID (UUID)
    #[serde(default)]
    pub assignee_id: Option<String>,
    /// Workflow state ID (UUID)
    #[serde(default)]
    pub state_id: Option<String>,
    /// Project ID (UUID)
    #[serde(default)]
    pub project_id: Option<String>,
    /// Parent issue ID (UUID) for sub-issues
    #[serde(default)]
    pub parent_id: Option<String>,
    /// Replace all labels with these IDs (overrides existing)
    #[serde(default)]
    pub label_ids: Option<Vec<String>>,
    /// Add these label IDs (incremental)
    #[serde(default)]
    pub added_label_ids: Option<Vec<String>>,
    /// Remove these label IDs (incremental)
    #[serde(default)]
    pub removed_label_ids: Option<Vec<String>>,
    /// Due date in ISO 8601 format (YYYY-MM-DD)
    #[serde(default)]
    pub due_date: Option<String>,
}

/// Tool for updating an existing Linear issue.
#[derive(Clone)]
pub struct UpdateIssueTool {
    linear: Arc<LinearTools>,
}

impl UpdateIssueTool {
    pub fn new(linear: Arc<LinearTools>) -> Self {
        Self { linear }
    }
}

impl Tool for UpdateIssueTool {
    type Input = UpdateIssueInput;
    type Output = IssueResult;
    const NAME: &'static str = "linear_update_issue";
    const DESCRIPTION: &'static str = "Update an existing Linear issue. Use linear_get_metadata to look up user IDs, state IDs, project IDs, and label IDs.";

    fn call(
        &self,
        input: Self::Input,
        _ctx: &ToolContext,
    ) -> BoxFuture<'static, Result<Self::Output, ToolError>> {
        let linear = Arc::clone(&self.linear);
        Box::pin(async move {
            linear
                .update_issue(
                    input.issue,
                    input.title,
                    input.description,
                    input.priority,
                    input.assignee_id,
                    input.state_id,
                    input.project_id,
                    input.parent_id,
                    input.label_ids,
                    input.added_label_ids,
                    input.removed_label_ids,
                    input.due_date,
                )
                .await
                .map_err(|e| map_anyhow_to_tool_error(&e))
        })
    }
}

// ============================================================================
// SetRelation Tool
// ============================================================================

/// Input for setting or removing an issue relation
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct SetRelationInput {
    /// Source issue identifier (UUID, key like ENG-245, or URL)
    pub issue: String,
    /// Related issue identifier (UUID, key like ENG-245, or URL)
    pub related_issue: String,
    /// Relation type: "blocks", "duplicate", "related". Null/omitted to remove relation.
    #[serde(default)]
    pub relation_type: Option<String>,
}

/// Tool for setting or removing issue relations.
#[derive(Clone)]
pub struct SetRelationTool {
    linear: Arc<LinearTools>,
}

impl SetRelationTool {
    pub fn new(linear: Arc<LinearTools>) -> Self {
        Self { linear }
    }
}

impl Tool for SetRelationTool {
    type Input = SetRelationInput;
    type Output = SetRelationResult;
    const NAME: &'static str = "linear_set_relation";
    const DESCRIPTION: &'static str = "Set or remove a relation between two issues. Provide relation_type to create (blocks/duplicate/related), or omit/null to remove any existing relation.";

    fn call(
        &self,
        input: Self::Input,
        _ctx: &ToolContext,
    ) -> BoxFuture<'static, Result<Self::Output, ToolError>> {
        let linear = Arc::clone(&self.linear);
        Box::pin(async move {
            linear
                .set_relation(input.issue, input.related_issue, input.relation_type)
                .await
                .map_err(|e| map_anyhow_to_tool_error(&e))
        })
    }
}

// ============================================================================
// GetMetadata Tool
// ============================================================================

/// Input for `get_metadata` tool.
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct GetMetadataInput {
    /// Kind of metadata to retrieve
    pub kind: crate::models::MetadataKind,
    /// Optional search string (case-insensitive name match)
    #[serde(default)]
    pub search: Option<String>,
    /// Optional team ID to filter by (relevant for `workflow_states` and labels)
    #[serde(default)]
    pub team_id: Option<String>,
    /// Maximum number of results (default: 50)
    #[serde(default)]
    pub first: Option<i32>,
    /// Pagination cursor for next page
    #[serde(default)]
    pub after: Option<String>,
}

/// Tool for looking up Linear metadata (users, teams, projects, workflow states, labels).
#[derive(Clone)]
pub struct GetMetadataTool {
    linear: Arc<LinearTools>,
}

impl GetMetadataTool {
    pub fn new(linear: Arc<LinearTools>) -> Self {
        Self { linear }
    }
}

impl Tool for GetMetadataTool {
    type Input = GetMetadataInput;
    type Output = GetMetadataResult;
    const NAME: &'static str = "linear_get_metadata";
    const DESCRIPTION: &'static str = "Look up Linear metadata: users, teams, projects, workflow states, or labels. Use this to discover IDs for filtering and updating issues.";

    fn call(
        &self,
        input: Self::Input,
        _ctx: &ToolContext,
    ) -> BoxFuture<'static, Result<Self::Output, ToolError>> {
        let linear = Arc::clone(&self.linear);
        Box::pin(async move {
            linear
                .get_metadata(
                    input.kind,
                    input.search,
                    input.team_id,
                    input.first,
                    input.after,
                )
                .await
                .map_err(|e| map_anyhow_to_tool_error(&e))
        })
    }
}

// ============================================================================
// Registry Builder
// ============================================================================

/// Build a `ToolRegistry` containing all `linear_tools` tools.
pub fn build_registry(linear: Arc<LinearTools>) -> ToolRegistry {
    ToolRegistry::builder()
        .register::<SearchIssuesTool, ()>(SearchIssuesTool::new(Arc::clone(&linear)))
        .register::<ReadIssueTool, ()>(ReadIssueTool::new(Arc::clone(&linear)))
        .register::<CreateIssueTool, ()>(CreateIssueTool::new(Arc::clone(&linear)))
        .register::<AddCommentTool, ()>(AddCommentTool::new(Arc::clone(&linear)))
        .register::<GetIssueCommentsTool, ()>(GetIssueCommentsTool::new(Arc::clone(&linear)))
        .register::<ArchiveIssueTool, ()>(ArchiveIssueTool::new(Arc::clone(&linear)))
        .register::<UpdateIssueTool, ()>(UpdateIssueTool::new(Arc::clone(&linear)))
        .register::<SetRelationTool, ()>(SetRelationTool::new(Arc::clone(&linear)))
        .register::<GetMetadataTool, ()>(GetMetadataTool::new(linear))
        .finish()
}

// ============================================================================
// Error Conversion
// ============================================================================

/// Map `anyhow::Error` to `agentic_tools_core::ToolError` based on error message patterns.
fn map_anyhow_to_tool_error(e: &anyhow::Error) -> ToolError {
    let msg = e.to_string();
    let lc = msg.to_lowercase();
    if lc.contains("permission") || lc.contains("401") || lc.contains("403") {
        ToolError::Permission(msg)
    } else if lc.contains("not found") || lc.contains("404") {
        ToolError::NotFound(msg)
    } else if lc.contains("invalid") || lc.contains("bad request") {
        ToolError::InvalidInput(msg)
    } else if lc.contains("timeout") || lc.contains("network") || lc.contains("rate limit") {
        ToolError::External(msg)
    } else {
        ToolError::Internal(msg)
    }
}
