//! Tools: project_list_issues, project_create_issue, project_update_issue

use agent::{ToolContext, ToolDefinition, ToolError, ToolParam, ToolSchema};
use chrono::Utc;
use models::issues::{issue, issue_assignee, issue_label, Issue, IssueAssignee, IssueLabel, IssueState};
use models::projects::{MemberRole, ProjectMember};
use models::projects::project_members;
use models::system::{Label, label};
use models::users::User;
use sea_orm::*;
use std::collections::HashMap;
use uuid::Uuid;

// ─── list ─────────────────────────────────────────────────────────────────────

pub async fn list_issues_exec(
    ctx: ToolContext,
    args: serde_json::Value,
) -> Result<serde_json::Value, ToolError> {
    let project_id = ctx.project_id();
    let db = ctx.db();

    let state_filter = args
        .get("state")
        .and_then(|v| v.as_str())
        .map(|s| s.to_lowercase());

    let mut query = issue::Entity::find().filter(issue::Column::Project.eq(project_id));

    if let Some(ref state) = state_filter {
        query = query.filter(issue::Column::State.eq(state));
    }

    let issues = query
        .order_by_desc(issue::Column::CreatedAt)
        .all(db)
        .await
        .map_err(|e| ToolError::ExecutionError(e.to_string()))?;

    let issue_ids: Vec<_> = issues.iter().map(|i| i.id).collect();

    let assignees = IssueAssignee::find()
        .filter(issue_assignee::Column::Issue.is_in(issue_ids.clone()))
        .all(db)
        .await
        .map_err(|e| ToolError::ExecutionError(e.to_string()))?;

    let assignee_user_ids: Vec<_> = assignees.iter().map(|a| a.user).collect();
    let assignee_users = User::find()
        .filter(models::users::user::Column::Uid.is_in(assignee_user_ids))
        .all(db)
        .await
        .map_err(|e| ToolError::ExecutionError(e.to_string()))?;
    let user_map: std::collections::HashMap<_, _> =
        assignee_users.into_iter().map(|u| (u.uid, u)).collect();

    let issue_labels = IssueLabel::find()
        .filter(issue_label::Column::Issue.is_in(issue_ids.clone()))
        .all(db)
        .await
        .map_err(|e| ToolError::ExecutionError(e.to_string()))?;

    let label_ids: Vec<_> = issue_labels.iter().map(|l| l.label).collect();
    let labels = Label::find()
        .filter(label::Column::Id.is_in(label_ids))
        .all(db)
        .await
        .map_err(|e| ToolError::ExecutionError(e.to_string()))?;
    let label_map: std::collections::HashMap<_, _> =
        labels.into_iter().map(|l| (l.id, l)).collect();

    let assignee_map: std::collections::HashMap<_, Vec<_>> = assignees
        .into_iter()
        .filter_map(|a| {
            let user = user_map.get(&a.user)?;
            Some((
                a.issue,
                serde_json::json!({
                    "id": a.user.to_string(),
                    "username": user.username,
                    "display_name": user.display_name,
                }),
            ))
        })
        .fold(
            std::collections::HashMap::new(),
            |mut acc, (issue_id, user)| {
                acc.entry(issue_id).or_default().push(user);
                acc
            },
        );

    let issue_label_map: std::collections::HashMap<_, Vec<_>> = issue_labels
        .into_iter()
        .filter_map(|il| {
            let label = label_map.get(&il.label)?;
            Some((
                il.issue,
                serde_json::json!({
                    "id": il.label,
                    "name": label.name,
                    "color": label.color,
                }),
            ))
        })
        .fold(
            std::collections::HashMap::new(),
            |mut acc, (issue_id, label)| {
                acc.entry(issue_id).or_default().push(label);
                acc
            },
        );

    let result: Vec<_> = issues
        .into_iter()
        .map(|i| {
            serde_json::json!({
                "id": i.id.to_string(),
                "number": i.number,
                "title": i.title,
                "body": i.body,
                "state": i.state,
                "author_id": i.author.to_string(),
                "milestone": i.milestone,
                "created_at": i.created_at.to_rfc3339(),
                "updated_at": i.updated_at.to_rfc3339(),
                "closed_at": i.closed_at.map(|t| t.to_rfc3339()),
                "assignees": assignee_map.get(&i.id).unwrap_or(&vec![]),
                "labels": issue_label_map.get(&i.id).unwrap_or(&vec![]),
            })
        })
        .collect();

    Ok(serde_json::to_value(result).map_err(|e| ToolError::ExecutionError(e.to_string()))?)
}

// ─── helpers ───────────────────────────────────────────────────────────────────

/// Check if the user is the issue author OR an admin/owner of the project.
async fn require_issue_modifier(
    db: &impl ConnectionTrait,
    project_id: Uuid,
    sender_id: Uuid,
    author_id: Uuid,
) -> Result<(), ToolError> {
    // Author can always modify their own issue
    if sender_id == author_id {
        return Ok(());
    }
    // Otherwise require admin or owner
    let member = ProjectMember::find()
        .filter(project_members::Column::Project.eq(project_id))
        .filter(project_members::Column::User.eq(sender_id))
        .one(db)
        .await
        .map_err(|e| ToolError::ExecutionError(e.to_string()))?;
    let member = member
        .ok_or_else(|| ToolError::ExecutionError("You are not a member of this project".into()))?;
    let role = member
        .scope_role()
        .map_err(|_| ToolError::ExecutionError("Unknown member role".into()))?;
    match role {
        MemberRole::Admin | MemberRole::Owner => Ok(()),
        MemberRole::Member => Err(ToolError::ExecutionError(
            "Only the issue author or admin/owner can modify this issue".into(),
        )),
    }
}

// ─── create ───────────────────────────────────────────────────────────────────

async fn next_issue_number(db: &impl ConnectionTrait, project_id: Uuid) -> Result<i64, ToolError> {
    let max_num: Option<Option<i64>> = Issue::find()
        .filter(issue::Column::Project.eq(project_id))
        .select_only()
        .column_as(issue::Column::Number.max(), "max_num")
        .into_tuple::<Option<i64>>()
        .one(db)
        .await
        .map_err(|e| ToolError::ExecutionError(e.to_string()))?;
    Ok(max_num.flatten().unwrap_or(0) + 1)
}

pub async fn create_issue_exec(
    ctx: ToolContext,
    args: serde_json::Value,
) -> Result<serde_json::Value, ToolError> {
    let project_id = ctx.project_id();
    let db = ctx.db();

    let title = args
        .get("title")
        .and_then(|v| v.as_str())
        .ok_or_else(|| ToolError::ExecutionError("title is required".into()))?
        .to_string();

    let body = args
        .get("body")
        .and_then(|v| v.as_str())
        .map(|s| s.to_string());

    let milestone = args
        .get("milestone")
        .and_then(|v| v.as_str())
        .map(|s| s.to_string());

    let assignee_ids: Vec<Uuid> = args
        .get("assignee_ids")
        .and_then(|v| v.as_array())
        .map(|arr| {
            arr.iter()
                .filter_map(|v| Uuid::parse_str(v.as_str()?).ok())
                .collect()
        })
        .unwrap_or_default();

    let label_ids: Vec<i64> = args
        .get("label_ids")
        .and_then(|v| v.as_array())
        .map(|arr| arr.iter().filter_map(|v| v.as_i64()).collect())
        .unwrap_or_default();

    let author_id = ctx
        .sender_id()
        .ok_or_else(|| ToolError::ExecutionError("No sender context".into()))?;

    // Membership check: only project members can create issues
    let member = ProjectMember::find()
        .filter(project_members::Column::Project.eq(project_id))
        .filter(project_members::Column::User.eq(author_id))
        .one(db)
        .await
        .map_err(|e| ToolError::ExecutionError(e.to_string()))?;
    if member.is_none() {
        return Err(ToolError::ExecutionError("You are not a member of this project".into()));
    }

    let number = next_issue_number(db, project_id).await?;
    let now = Utc::now();

    let active = issue::ActiveModel {
        id: Set(Uuid::now_v7()),
        project: Set(project_id),
        number: Set(number),
        title: Set(title.clone()),
        body: Set(body),
        state: Set(IssueState::Open.to_string()),
        author: Set(author_id),
        milestone: Set(milestone),
        created_at: Set(now),
        updated_at: Set(now),
        closed_at: Set(None),
        created_by_ai: Set(true),
        ..Default::default()
    };

    let model = active
        .insert(db)
        .await
        .map_err(|e| ToolError::ExecutionError(e.to_string()))?;

    // Add assignees (collect errors for partial failure reporting)
    let mut assignee_errors = Vec::new();
    for uid in &assignee_ids {
        let a = issue_assignee::ActiveModel {
            issue: Set(model.id),
            user: Set(*uid),
            assigned_at: Set(now),
            ..Default::default()
        };
        if let Err(e) = a.insert(db).await {
            assignee_errors.push(format!("assignee {}: {}", uid, e));
        }
    }

    // Add labels
    let mut label_errors = Vec::new();
    for lid in &label_ids {
        let l = issue_label::ActiveModel {
            issue: Set(model.id),
            label: Set(*lid),
            relation_at: Set(now),
            ..Default::default()
        };
        if let Err(e) = l.insert(db).await {
            label_errors.push(format!("label {}: {}", lid, e));
        }
    }

    // Build assignee/label maps for response
    let assignee_map: std::collections::HashMap<Uuid, serde_json::Value> =
        if !assignee_ids.is_empty() {
            let users = User::find()
                .filter(models::users::user::Column::Uid.is_in(assignee_ids.clone()))
                .all(db)
                .await
                .map_err(|e| ToolError::ExecutionError(e.to_string()))?;
            users
                .into_iter()
                .map(|u| {
                    (
                        u.uid,
                        serde_json::json!({
                            "id": u.uid.to_string(),
                            "username": u.username,
                            "display_name": u.display_name,
                        }),
                    )
                })
                .collect()
        } else {
            std::collections::HashMap::new()
        };

    let label_map: std::collections::HashMap<i64, serde_json::Value> = if !label_ids.is_empty() {
        let labels = Label::find()
            .filter(label::Column::Id.is_in(label_ids.clone()))
            .all(db)
            .await
            .map_err(|e| ToolError::ExecutionError(e.to_string()))?;
        labels
            .into_iter()
            .map(|l| {
                (
                    l.id,
                    serde_json::json!({
                        "id": l.id,
                        "name": l.name,
                        "color": l.color,
                    }),
                )
            })
            .collect()
    } else {
        std::collections::HashMap::new()
    };

    Ok(serde_json::json!({
        "id": model.id.to_string(),
        "number": model.number,
        "title": model.title,
        "body": model.body,
        "state": model.state,
        "author_id": model.author.to_string(),
        "milestone": model.milestone,
        "created_at": model.created_at.to_rfc3339(),
        "updated_at": model.updated_at.to_rfc3339(),
        "assignees": assignee_ids.iter().filter_map(|uid| assignee_map.get(uid)).collect::<Vec<_>>(),
        "labels": label_ids.iter().filter_map(|lid| label_map.get(lid)).collect::<Vec<_>>(),
        "warnings": if assignee_errors.is_empty() && label_errors.is_empty() {
            None
        } else {
            Some([assignee_errors, label_errors].concat())
        },
    }))
}

// ─── update ───────────────────────────────────────────────────────────────────

pub async fn update_issue_exec(
    ctx: ToolContext,
    args: serde_json::Value,
) -> Result<serde_json::Value, ToolError> {
    let project_id = ctx.project_id();
    let sender_id = ctx
        .sender_id()
        .ok_or_else(|| ToolError::ExecutionError("No sender context".into()))?;
    let db = ctx.db();

    let number = args
        .get("number")
        .and_then(|v| v.as_i64())
        .ok_or_else(|| ToolError::ExecutionError("number is required".into()))?;

    // Find the issue
    let issue = Issue::find()
        .filter(issue::Column::Project.eq(project_id))
        .filter(issue::Column::Number.eq(number))
        .one(db)
        .await
        .map_err(|e| ToolError::ExecutionError(e.to_string()))?
        .ok_or_else(|| ToolError::ExecutionError(format!("Issue #{} not found", number)))?;

    // Permission check: author OR admin/owner
    require_issue_modifier(db, project_id, sender_id, issue.author).await?;

    let mut active: issue::ActiveModel = issue.clone().into();
    let mut updated = false;
    let now = Utc::now();

    if let Some(title) = args.get("title").and_then(|v| v.as_str()) {
        active.title = Set(title.to_string());
        updated = true;
    }
    if let Some(body) = args.get("body").and_then(|v| v.as_str()) {
        active.body = Set(Some(body.to_string()));
        updated = true;
    }
    if let Some(state) = args.get("state").and_then(|v| v.as_str()) {
        let s = state.to_lowercase();
        if s == "open" || s == "closed" {
            active.state = Set(s.clone());
            active.updated_at = Set(now);
            if s == "closed" {
                active.closed_at = Set(Some(now));
            } else {
                active.closed_at = Set(None);
            }
            updated = true;
        }
    }
    if let Some(milestone) = args.get("milestone") {
        if milestone.is_null() {
            active.milestone = Set(None);
        } else if let Some(m) = milestone.as_str() {
            active.milestone = Set(Some(m.to_string()));
        }
        updated = true;
    }

    if updated {
        active.updated_at = Set(now);
        active
            .update(db)
            .await
            .map_err(|e| ToolError::ExecutionError(e.to_string()))?;
    }

    // Reload for response
    let updated_issue = Issue::find()
        .filter(issue::Column::Id.eq(issue.id))
        .one(db)
        .await
        .map_err(|e| ToolError::ExecutionError(e.to_string()))?
        .ok_or_else(|| ToolError::ExecutionError("Issue not found after update".into()))?;

    Ok(serde_json::json!({
        "id": updated_issue.id.to_string(),
        "number": updated_issue.number,
        "title": updated_issue.title,
        "body": updated_issue.body,
        "state": updated_issue.state,
        "author_id": updated_issue.author.to_string(),
        "milestone": updated_issue.milestone,
        "created_at": updated_issue.created_at.to_rfc3339(),
        "updated_at": updated_issue.updated_at.to_rfc3339(),
        "closed_at": updated_issue.closed_at.map(|t| t.to_rfc3339()),
    }))
}

// ─── tool definitions ─────────────────────────────────────────────────────────

pub fn list_tool_definition() -> ToolDefinition {
    let mut p = HashMap::new();
    p.insert("state".into(), ToolParam {
        name: "state".into(), param_type: "string".into(),
        description: Some("Filter by issue state: 'open' or 'closed'. Optional.".into()),
        required: false, properties: None, items: None,
    });
    ToolDefinition::new("project_list_issues")
        .description(
            "List all issues in the current project. \
             Returns issue number, title, body, state, author, assignees, labels, and timestamps.",
        )
        .parameters(ToolSchema {
            schema_type: "object".into(),
            properties: Some(p),
            required: None,
        })
}

pub fn create_tool_definition() -> ToolDefinition {
    let mut p = HashMap::new();
    p.insert("title".into(), ToolParam {
        name: "title".into(), param_type: "string".into(),
        description: Some("Issue title (required).".into()),
        required: true, properties: None, items: None,
    });
    p.insert("body".into(), ToolParam {
        name: "body".into(), param_type: "string".into(),
        description: Some("Issue body / description. Optional.".into()),
        required: false, properties: None, items: None,
    });
    p.insert("milestone".into(), ToolParam {
        name: "milestone".into(), param_type: "string".into(),
        description: Some("Milestone name. Optional.".into()),
        required: false, properties: None, items: None,
    });
    p.insert("assignee_ids".into(), ToolParam {
        name: "assignee_ids".into(), param_type: "array".into(),
        description: Some("Array of user UUIDs to assign. Optional.".into()),
        required: false, properties: None,
        items: Some(Box::new(ToolParam {
            name: "".into(), param_type: "string".into(),
            description: None, required: false, properties: None, items: None,
        })),
    });
    p.insert("label_ids".into(), ToolParam {
        name: "label_ids".into(), param_type: "array".into(),
        description: Some("Array of label IDs to apply. Optional.".into()),
        required: false, properties: None,
        items: Some(Box::new(ToolParam {
            name: "".into(), param_type: "integer".into(),
            description: None, required: false, properties: None, items: None,
        })),
    });
    ToolDefinition::new("project_create_issue")
        .description(
            "Create a new issue in the current project. \
             Returns the created issue with its number, id, and full details.",
        )
        .parameters(ToolSchema {
            schema_type: "object".into(),
            properties: Some(p),
            required: Some(vec!["title".into()]),
        })
}

pub fn update_tool_definition() -> ToolDefinition {
    let mut p = HashMap::new();
    p.insert("number".into(), ToolParam {
        name: "number".into(), param_type: "integer".into(),
        description: Some("Issue number (required).".into()),
        required: true, properties: None, items: None,
    });
    p.insert("title".into(), ToolParam {
        name: "title".into(), param_type: "string".into(),
        description: Some("New issue title. Optional.".into()),
        required: false, properties: None, items: None,
    });
    p.insert("body".into(), ToolParam {
        name: "body".into(), param_type: "string".into(),
        description: Some("New issue body. Optional.".into()),
        required: false, properties: None, items: None,
    });
    p.insert("state".into(), ToolParam {
        name: "state".into(), param_type: "string".into(),
        description: Some("New issue state: 'open' or 'closed'. Optional.".into()),
        required: false, properties: None, items: None,
    });
    p.insert("milestone".into(), ToolParam {
        name: "milestone".into(), param_type: "string".into(),
        description: Some("New milestone name. Set to null to remove. Optional.".into()),
        required: false, properties: None, items: None,
    });
    ToolDefinition::new("project_update_issue")
        .description(
            "Update an existing issue in the current project by its number. \
             Requires the issue author or a project admin/owner. \
             Returns the updated issue. At least one field must be provided.",
        )
        .parameters(ToolSchema {
            schema_type: "object".into(),
            properties: Some(p),
            required: Some(vec!["number".into()]),
        })
}
