//! Tools: project_list_boards, project_create_board, project_update_board,
//!       project_create_board_card, project_update_board_card, project_delete_board_card

use agent::{ToolContext, ToolDefinition, ToolError, ToolParam, ToolSchema};
use chrono::Utc;
use models::projects::{
    project_board, project_board_card, project_board_column, project_members,
};
use models::projects::{MemberRole, ProjectBoard, ProjectBoardCard, ProjectBoardColumn};
use models::users::user::Model as UserModel;
use sea_orm::*;
use std::collections::HashMap;
use uuid::Uuid;

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

/// Check if the sender is an admin or owner of the project.
async fn require_admin(
    db: &impl ConnectionTrait,
    project_id: Uuid,
    sender_id: Uuid,
) -> Result<(), ToolError> {
    let member = project_members::Entity::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 admin or owner can perform this action".into(),
        )),
    }
}

#[allow(dead_code)]
fn serde_user(u: &UserModel) -> serde_json::Value {
    serde_json::json!({
        "id": u.uid.to_string(),
        "username": u.username,
        "display_name": u.display_name,
    })
}

// ─── list boards ──────────────────────────────────────────────────────────────

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

    let boards = project_board::Entity::find()
        .filter(project_board::Column::Project.eq(project_id))
        .order_by_asc(project_board::Column::CreatedAt)
        .all(db)
        .await
        .map_err(|e| ToolError::ExecutionError(e.to_string()))?;

    let board_ids: Vec<_> = boards.iter().map(|b| b.id).collect();

    // Batch-load columns and cards
    let columns = project_board_column::Entity::find()
        .filter(project_board_column::Column::Board.is_in(board_ids.clone()))
        .order_by_asc(project_board_column::Column::Position)
        .all(db)
        .await
        .map_err(|e| ToolError::ExecutionError(e.to_string()))?;

    let column_ids: Vec<_> = columns.iter().map(|c| c.id).collect();
    let cards = project_board_card::Entity::find()
        .filter(project_board_card::Column::Column.is_in(column_ids.clone()))
        .order_by_asc(project_board_card::Column::Position)
        .all(db)
        .await
        .map_err(|e| ToolError::ExecutionError(e.to_string()))?;

    // Build column map
    let col_map: std::collections::HashMap<Uuid, Vec<serde_json::Value>> = columns
        .clone()
        .into_iter()
        .map(|c| {
            let cards_in_col: Vec<serde_json::Value> = cards
                .iter()
                .filter(|card| card.column == c.id)
                .map(|card| {
                    serde_json::json!({
                        "id": card.id.to_string(),
                        "issue_id": card.issue_id,
                        "title": card.title,
                        "description": card.description,
                        "position": card.position,
                        "assignee_id": card.assignee_id.map(|id| id.to_string()),
                        "due_date": card.due_date.map(|t| t.to_rfc3339()),
                        "priority": card.priority,
                        "created_at": card.created_at.to_rfc3339(),
                    })
                })
                .collect();
            (c.id, cards_in_col)
        })
        .collect();

    // Build column list per board
    let mut board_col_map: std::collections::HashMap<Uuid, Vec<serde_json::Value>> = columns
        .into_iter()
        .fold(std::collections::HashMap::new(), |mut acc, c| {
            let cards = col_map.get(&c.id).cloned().unwrap_or_default();
            acc.entry(c.board).or_default().push(serde_json::json!({
                "id": c.id.to_string(),
                "name": c.name,
                "position": c.position,
                "wip_limit": c.wip_limit,
                "color": c.color,
                "cards": cards,
            }));
            acc
        });

    let result: Vec<_> = boards
        .into_iter()
        .map(|b| {
            let cols = board_col_map.remove(&b.id).unwrap_or_default();
            serde_json::json!({
                "id": b.id.to_string(),
                "name": b.name,
                "description": b.description,
                "created_by": b.created_by.to_string(),
                "created_at": b.created_at.to_rfc3339(),
                "updated_at": b.updated_at.to_rfc3339(),
                "columns": cols,
            })
        })
        .collect();

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

// ─── create board ─────────────────────────────────────────────────────────────

pub async fn create_board_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();

    require_admin(db, project_id, sender_id).await?;

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

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

    let now = Utc::now();
    let active = project_board::ActiveModel {
        id: Set(Uuid::now_v7()),
        project: Set(project_id),
        name: Set(name.clone()),
        description: Set(description),
        created_by: Set(sender_id),
        created_at: Set(now),
        updated_at: Set(now),
    };

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

    Ok(serde_json::json!({
        "id": model.id.to_string(),
        "name": model.name,
        "description": model.description,
        "created_by": model.created_by.to_string(),
        "created_at": model.created_at.to_rfc3339(),
        "updated_at": model.updated_at.to_rfc3339(),
        "columns": Vec::<serde_json::Value>::new(),
    }))
}

// ─── update board ─────────────────────────────────────────────────────────────

pub async fn update_board_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();

    require_admin(db, project_id, sender_id).await?;

    let board_id = args
        .get("board_id")
        .and_then(|v| Uuid::parse_str(v.as_str()?).ok())
        .ok_or_else(|| ToolError::ExecutionError("board_id is required".into()))?;

    let board = ProjectBoard::find_by_id(board_id)
        .one(db)
        .await
        .map_err(|e| ToolError::ExecutionError(e.to_string()))?
        .ok_or_else(|| ToolError::ExecutionError("Board not found".into()))?;

    if board.project != project_id {
        return Err(ToolError::ExecutionError(
            "Board does not belong to this project".into(),
        ));
    }

    let mut active: project_board::ActiveModel = board.clone().into();
    let mut updated = false;

    if let Some(name) = args.get("name").and_then(|v| v.as_str()) {
        active.name = Set(name.to_string());
        updated = true;
    }
    if let Some(description) = args.get("description") {
        active.description = Set(description.as_str().map(|s| s.to_string()));
        updated = true;
    }

    if !updated {
        return Err(ToolError::ExecutionError(
            "At least one field must be provided".into(),
        ));
    }

    active.updated_at = Set(Utc::now());
    let model = active
        .update(db)
        .await
        .map_err(|e| ToolError::ExecutionError(e.to_string()))?;

    Ok(serde_json::json!({
        "id": model.id.to_string(),
        "name": model.name,
        "description": model.description,
        "created_by": model.created_by.to_string(),
        "created_at": model.created_at.to_rfc3339(),
        "updated_at": model.updated_at.to_rfc3339(),
    }))
}

// ─── create board card ───────────────────────────────────────────────────────

pub async fn create_board_card_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();

    require_admin(db, project_id, sender_id).await?;

    let board_id = args
        .get("board_id")
        .and_then(|v| Uuid::parse_str(v.as_str()?).ok())
        .ok_or_else(|| ToolError::ExecutionError("board_id is required".into()))?;

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

    let column_id = args
        .get("column_id")
        .and_then(|v| Uuid::parse_str(v.as_str()?).ok());

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

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

    let assignee_id = args
        .get("assignee_id")
        .and_then(|v| Uuid::parse_str(v.as_str()?).ok());

    // Verify board belongs to project
    let board = ProjectBoard::find_by_id(board_id)
        .one(db)
        .await
        .map_err(|e| ToolError::ExecutionError(e.to_string()))?
        .ok_or_else(|| ToolError::ExecutionError("Board not found".into()))?;

    if board.project != project_id {
        return Err(ToolError::ExecutionError(
            "Board does not belong to this project".into(),
        ));
    }

    // Get target column (first column if not specified)
    let target_column = if let Some(col_id) = column_id {
        let col = ProjectBoardColumn::find_by_id(col_id)
            .one(db)
            .await
            .map_err(|e| ToolError::ExecutionError(e.to_string()))?
            .ok_or_else(|| ToolError::ExecutionError("Column not found".into()))?;
        if col.board != board_id {
            return Err(ToolError::ExecutionError(
                "Column does not belong to this board".into(),
            ));
        }
        col
    } else {
        ProjectBoardColumn::find()
            .filter(project_board_column::Column::Board.eq(board_id))
            .order_by_asc(project_board_column::Column::Position)
            .one(db)
            .await
            .map_err(|e| ToolError::ExecutionError(e.to_string()))?
            .ok_or_else(|| ToolError::ExecutionError("No columns found in this board".into()))?
    };

    // Next position
    let max_pos: Option<Option<i32>> = ProjectBoardCard::find()
        .filter(project_board_card::Column::Column.eq(target_column.id))
        .select_only()
        .column_as(project_board_card::Column::Position.max(), "max_pos")
        .into_tuple::<Option<i32>>()
        .one(db)
        .await
        .map_err(|e| ToolError::ExecutionError(e.to_string()))?;
    let position = max_pos.flatten().unwrap_or(0) + 1;

    let now = Utc::now();
    let active = project_board_card::ActiveModel {
        id: Set(Uuid::now_v7()),
        column: Set(target_column.id),
        issue_id: Set(None),
        project: Set(Some(project_id)),
        title: Set(title),
        description: Set(description),
        position: Set(position),
        assignee_id: Set(assignee_id),
        due_date: Set(None),
        priority: Set(priority),
        created_by: Set(sender_id),
        created_at: Set(now),
        updated_at: Set(now),
    };

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

    Ok(serde_json::json!({
        "id": model.id.to_string(),
        "column_id": model.column.to_string(),
        "title": model.title,
        "description": model.description,
        "position": model.position,
        "assignee_id": model.assignee_id.map(|id| id.to_string()),
        "priority": model.priority,
        "created_at": model.created_at.to_rfc3339(),
        "updated_at": model.updated_at.to_rfc3339(),
    }))
}

// ─── update board card ────────────────────────────────────────────────────────

pub async fn update_board_card_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();

    require_admin(db, project_id, sender_id).await?;

    let card_id = args
        .get("card_id")
        .and_then(|v| Uuid::parse_str(v.as_str()?).ok())
        .ok_or_else(|| ToolError::ExecutionError("card_id is required".into()))?;

    let card = ProjectBoardCard::find_by_id(card_id)
        .one(db)
        .await
        .map_err(|e| ToolError::ExecutionError(e.to_string()))?
        .ok_or_else(|| ToolError::ExecutionError("Card not found".into()))?;

    // Verify card belongs to a column in this project's board
    let col = ProjectBoardColumn::find_by_id(card.column)
        .one(db)
        .await
        .map_err(|e| ToolError::ExecutionError(e.to_string()))?
        .ok_or_else(|| ToolError::ExecutionError("Column not found".into()))?;

    let board = ProjectBoard::find_by_id(col.board)
        .one(db)
        .await
        .map_err(|e| ToolError::ExecutionError(e.to_string()))?
        .ok_or_else(|| ToolError::ExecutionError("Board not found".into()))?;

    if board.project != project_id {
        return Err(ToolError::ExecutionError(
            "Card does not belong to this project".into(),
        ));
    }

    let mut active: project_board_card::ActiveModel = card.clone().into();
    let mut updated = false;

    if let Some(title) = args.get("title").and_then(|v| v.as_str()) {
        active.title = Set(title.to_string());
        updated = true;
    }
    if let Some(description) = args.get("description") {
        active.description = Set(description.as_str().map(|s| s.to_string()));
        updated = true;
    }
    if let Some(column_id) = args
        .get("column_id")
        .and_then(|v| Uuid::parse_str(v.as_str()?).ok())
    {
        // Verify column belongs to the same board
        let new_col = ProjectBoardColumn::find_by_id(column_id)
            .one(db)
            .await
            .map_err(|e| ToolError::ExecutionError(e.to_string()))?
            .ok_or_else(|| ToolError::ExecutionError("Column not found".into()))?;
        if new_col.board != col.board {
            return Err(ToolError::ExecutionError(
                "Column does not belong to this board".into(),
            ));
        }
        active.column = Set(column_id);
        updated = true;
    }
    if let Some(position) = args.get("position").and_then(|v| v.as_i64()) {
        active.position = Set(position as i32);
        updated = true;
    }
    if let Some(assignee_id) = args.get("assignee_id") {
        active.assignee_id = Set(assignee_id.as_str().and_then(|s| Uuid::parse_str(s).ok()));
        updated = true;
    }
    if let Some(priority) = args.get("priority") {
        active.priority = Set(priority.as_str().map(|s| s.to_string()));
        updated = true;
    }

    if !updated {
        return Err(ToolError::ExecutionError(
            "At least one field must be provided".into(),
        ));
    }

    active.updated_at = Set(Utc::now());
    let model = active
        .update(db)
        .await
        .map_err(|e| ToolError::ExecutionError(e.to_string()))?;

    Ok(serde_json::json!({
        "id": model.id.to_string(),
        "column_id": model.column.to_string(),
        "title": model.title,
        "description": model.description,
        "position": model.position,
        "assignee_id": model.assignee_id.map(|id| id.to_string()),
        "priority": model.priority,
        "created_at": model.created_at.to_rfc3339(),
        "updated_at": model.updated_at.to_rfc3339(),
    }))
}

// ─── delete board card ─────────────────────────────────────────────────────────

pub async fn delete_board_card_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();

    require_admin(db, project_id, sender_id).await?;

    let card_id = args
        .get("card_id")
        .and_then(|v| Uuid::parse_str(v.as_str()?).ok())
        .ok_or_else(|| ToolError::ExecutionError("card_id is required".into()))?;

    let card = ProjectBoardCard::find_by_id(card_id)
        .one(db)
        .await
        .map_err(|e| ToolError::ExecutionError(e.to_string()))?
        .ok_or_else(|| ToolError::ExecutionError("Card not found".into()))?;

    let col = ProjectBoardColumn::find_by_id(card.column)
        .one(db)
        .await
        .map_err(|e| ToolError::ExecutionError(e.to_string()))?
        .ok_or_else(|| ToolError::ExecutionError("Column not found".into()))?;

    let board = ProjectBoard::find_by_id(col.board)
        .one(db)
        .await
        .map_err(|e| ToolError::ExecutionError(e.to_string()))?
        .ok_or_else(|| ToolError::ExecutionError("Board not found".into()))?;

    if board.project != project_id {
        return Err(ToolError::ExecutionError(
            "Card does not belong to this project".into(),
        ));
    }

    ProjectBoardCard::delete_by_id(card_id)
        .exec(db)
        .await
        .map_err(|e| ToolError::ExecutionError(e.to_string()))?;

    Ok(serde_json::json!({ "deleted": true }))
}

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

pub fn list_tool_definition() -> ToolDefinition {
    ToolDefinition::new("project_list_boards")
        .description(
            "List all Kanban boards in the current project. \
             Returns boards with their columns and cards, including positions and priorities.",
        )
        .parameters(ToolSchema {
            schema_type: "object".into(),
            properties: None,
            required: None,
        })
}

pub fn create_board_tool_definition() -> ToolDefinition {
    let mut p = HashMap::new();
    p.insert("name".into(), ToolParam {
        name: "name".into(), param_type: "string".into(),
        description: Some("Board name (required).".into()),
        required: true, properties: None, items: None,
    });
    p.insert("description".into(), ToolParam {
        name: "description".into(), param_type: "string".into(),
        description: Some("Board description. Optional.".into()),
        required: false, properties: None, items: None,
    });
    ToolDefinition::new("project_create_board")
        .description(
            "Create a new Kanban board in the current project. Requires admin or owner role.",
        )
        .parameters(ToolSchema {
            schema_type: "object".into(),
            properties: Some(p),
            required: Some(vec!["name".into()]),
        })
}

pub fn update_board_tool_definition() -> ToolDefinition {
    let mut p = HashMap::new();
    p.insert("board_id".into(), ToolParam {
        name: "board_id".into(), param_type: "string".into(),
        description: Some("Board UUID (required).".into()),
        required: true, properties: None, items: None,
    });
    p.insert("name".into(), ToolParam {
        name: "name".into(), param_type: "string".into(),
        description: Some("New board name. Optional.".into()),
        required: false, properties: None, items: None,
    });
    p.insert("description".into(), ToolParam {
        name: "description".into(), param_type: "string".into(),
        description: Some("New board description. Optional.".into()),
        required: false, properties: None, items: None,
    });
    ToolDefinition::new("project_update_board")
        .description(
            "Update a Kanban board (name or description). Requires admin or owner role.",
        )
        .parameters(ToolSchema {
            schema_type: "object".into(),
            properties: Some(p),
            required: Some(vec!["board_id".into()]),
        })
}

pub fn create_card_tool_definition() -> ToolDefinition {
    let mut p = HashMap::new();
    p.insert("board_id".into(), ToolParam {
        name: "board_id".into(), param_type: "string".into(),
        description: Some("Board UUID (required).".into()),
        required: true, properties: None, items: None,
    });
    p.insert("column_id".into(), ToolParam {
        name: "column_id".into(), param_type: "string".into(),
        description: Some("Column UUID. Optional — defaults to first column.".into()),
        required: false, properties: None, items: None,
    });
    p.insert("title".into(), ToolParam {
        name: "title".into(), param_type: "string".into(),
        description: Some("Card title (required).".into()),
        required: true, properties: None, items: None,
    });
    p.insert("description".into(), ToolParam {
        name: "description".into(), param_type: "string".into(),
        description: Some("Card description. Optional.".into()),
        required: false, properties: None, items: None,
    });
    p.insert("priority".into(), ToolParam {
        name: "priority".into(), param_type: "string".into(),
        description: Some("Card priority (e.g. 'low', 'medium', 'high'). Optional.".into()),
        required: false, properties: None, items: None,
    });
    p.insert("assignee_id".into(), ToolParam {
        name: "assignee_id".into(), param_type: "string".into(),
        description: Some("Assignee user UUID. Optional.".into()),
        required: false, properties: None, items: None,
    });
    ToolDefinition::new("project_create_board_card")
        .description(
            "Create a card on a Kanban board. If column_id is not provided, \
             the card is added to the first column.",
        )
        .parameters(ToolSchema {
            schema_type: "object".into(),
            properties: Some(p),
            required: Some(vec!["board_id".into(), "title".into()]),
        })
}

pub fn update_card_tool_definition() -> ToolDefinition {
    let mut p = HashMap::new();
    p.insert("card_id".into(), ToolParam {
        name: "card_id".into(), param_type: "string".into(),
        description: Some("Card UUID (required).".into()),
        required: true, properties: None, items: None,
    });
    p.insert("title".into(), ToolParam {
        name: "title".into(), param_type: "string".into(),
        description: Some("New card title. Optional.".into()),
        required: false, properties: None, items: None,
    });
    p.insert("description".into(), ToolParam {
        name: "description".into(), param_type: "string".into(),
        description: Some("New card description. Optional.".into()),
        required: false, properties: None, items: None,
    });
    p.insert("column_id".into(), ToolParam {
        name: "column_id".into(), param_type: "string".into(),
        description: Some("Move card to a different column. Optional.".into()),
        required: false, properties: None, items: None,
    });
    p.insert("position".into(), ToolParam {
        name: "position".into(), param_type: "integer".into(),
        description: Some("New position within column. Optional.".into()),
        required: false, properties: None, items: None,
    });
    p.insert("priority".into(), ToolParam {
        name: "priority".into(), param_type: "string".into(),
        description: Some("New priority. Optional.".into()),
        required: false, properties: None, items: None,
    });
    p.insert("assignee_id".into(), ToolParam {
        name: "assignee_id".into(), param_type: "string".into(),
        description: Some("New assignee UUID. Optional.".into()),
        required: false, properties: None, items: None,
    });
    ToolDefinition::new("project_update_board_card")
        .description(
            "Update a board card (title, description, column, position, assignee, priority). \
             Requires admin or owner role.",
        )
        .parameters(ToolSchema {
            schema_type: "object".into(),
            properties: Some(p),
            required: Some(vec!["card_id".into()]),
        })
}

pub fn delete_card_tool_definition() -> ToolDefinition {
    let mut p = HashMap::new();
    p.insert("card_id".into(), ToolParam {
        name: "card_id".into(), param_type: "string".into(),
        description: Some("Card UUID (required).".into()),
        required: true, properties: None, items: None,
    });
    ToolDefinition::new("project_delete_board_card")
        .description("Delete a board card. Requires admin or owner role.")
        .parameters(ToolSchema {
            schema_type: "object".into(),
            properties: Some(p),
            required: Some(vec!["card_id".into()]),
        })
}
