//! Tool: project_list_repos, project_create_repo, project_create_commit

use agent::{ToolContext, ToolDefinition, ToolError, ToolParam, ToolSchema};
use chrono::Utc;
use git::commit::types::CommitOid;
use git::commit::types::CommitSignature;
use models::projects::{MemberRole, ProjectMember};
use models::projects::project_members;
use models::repos::repo;
use models::users::user_email;
use sea_orm::*;
use std::collections::HashMap;
use std::path::PathBuf;
use uuid::Uuid;

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

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

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

    let result: Vec<_> = repos
        .into_iter()
        .map(|r| {
            serde_json::json!({
                "id": r.id.to_string(),
                "name": r.repo_name,
                "description": r.description,
                "default_branch": r.default_branch,
                "is_private": r.is_private,
                "created_at": r.created_at.to_rfc3339(),
            })
        })
        .collect();

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

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

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

    // Admin/owner check
    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 => {}
        MemberRole::Member => {
            return Err(ToolError::ExecutionError(
                "Only admin or owner can create repositories".into(),
            ));
        }
    }

    let repo_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 is_private = args
        .get("is_private")
        .and_then(|v| v.as_bool())
        .unwrap_or(false);

    // Check name uniqueness within project
    let existing = repo::Entity::find()
        .filter(repo::Column::Project.eq(project_id))
        .filter(repo::Column::RepoName.eq(&repo_name))
        .one(db)
        .await
        .map_err(|e| ToolError::ExecutionError(e.to_string()))?;
    if existing.is_some() {
        return Err(ToolError::ExecutionError(format!(
            "Repository '{}' already exists in this project",
            repo_name
        )));
    }

    // Look up project name for storage_path
    let project = models::projects::project::Entity::find_by_id(project_id)
        .one(db)
        .await
        .map_err(|e| ToolError::ExecutionError(e.to_string()))?
        .ok_or_else(|| ToolError::ExecutionError("Project not found".into()))?;

    let repos_root = ctx
        .config()
        .repos_root()
        .map_err(|e| ToolError::ExecutionError(format!("repos_root config error: {}", e)))?;

    let repo_dir: PathBuf = [&repos_root, &project.name, &format!("{}.git", repo_name)]
        .iter()
        .collect();

    let now = Utc::now();
    let active = repo::ActiveModel {
        id: Set(Uuid::now_v7()),
        repo_name: Set(repo_name.clone()),
        project: Set(project_id),
        description: Set(description),
        default_branch: Set("main".to_string()),
        is_private: Set(is_private),
        storage_path: Set(repo_dir.to_string_lossy().to_string()),
        created_by: Set(sender_id),
        created_at: Set(now),
        updated_at: Set(now),
        ai_code_review_enabled: Set(false),
    };

    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.repo_name,
        "description": model.description,
        "default_branch": model.default_branch,
        "is_private": model.is_private,
        "storage_path": model.storage_path,
        "created_at": model.created_at.to_rfc3339(),
    }))
}

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

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

    // Admin/owner check
    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 => {}
        MemberRole::Member => {
            return Err(ToolError::ExecutionError(
                "Only admin or owner can update repositories".into(),
            ));
        }
    }

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

    let repo = repo::Entity::find()
        .filter(repo::Column::Project.eq(project_id))
        .filter(repo::Column::RepoName.eq(&repo_name))
        .one(db)
        .await
        .map_err(|e| ToolError::ExecutionError(e.to_string()))?
        .ok_or_else(|| {
            ToolError::ExecutionError(format!("Repository '{}' not found", repo_name))
        })?;

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

    if let Some(description) = args.get("description") {
        active.description = Set(description.as_str().map(|s| s.to_string()));
        updated = true;
    }
    if let Some(is_private) = args.get("is_private").and_then(|v| v.as_bool()) {
        active.is_private = Set(is_private);
        updated = true;
    }
    if let Some(default_branch) = args.get("default_branch").and_then(|v| v.as_str()) {
        active.default_branch = Set(default_branch.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.repo_name,
        "description": model.description,
        "default_branch": model.default_branch,
        "is_private": model.is_private,
        "created_at": model.created_at.to_rfc3339(),
        "updated_at": model.updated_at.to_rfc3339(),
    }))
}

// ─── create commit ────────────────────────────────────────────────────────────

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

    // Admin/owner check
    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 => {}
        MemberRole::Member => {
            return Err(ToolError::ExecutionError(
                "Only admin or owner can create commits".into(),
            ));
        }
    }

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

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

    let branch = args
        .get("branch")
        .and_then(|v| v.as_str())
        .unwrap_or("main")
        .to_string();

    let files = args
        .get("files")
        .and_then(|v| v.as_array())
        .ok_or_else(|| {
            ToolError::ExecutionError("files is required and must be an array".into())
        })?;

    if files.is_empty() {
        return Err(ToolError::ExecutionError(
            "files array cannot be empty".into(),
        ));
    }

    // Clone files data for spawn_blocking
    let files_data: Vec<serde_json::Value> = files.iter().cloned().collect();

    // Look up sender username and email
    let sender = models::users::user::Entity::find_by_id(sender_id)
        .one(db)
        .await
        .map_err(|e| ToolError::ExecutionError(e.to_string()))?
        .ok_or_else(|| ToolError::ExecutionError("Sender user not found".into()))?;

    let sender_email = user_email::Entity::find_by_id(sender_id)
        .one(db)
        .await
        .ok()
        .flatten()
        .map(|e| e.email)
        .unwrap_or_else(|| format!("{}@gitdata.ai", sender.username));

    let author_name = sender
        .display_name
        .unwrap_or_else(|| sender.username.clone());

    // Find repo
    let repo_model = repo::Entity::find()
        .filter(repo::Column::Project.eq(project_id))
        .filter(repo::Column::RepoName.eq(repo_name))
        .one(db)
        .await
        .map_err(|e| ToolError::ExecutionError(e.to_string()))?
        .ok_or_else(|| {
            ToolError::ExecutionError(format!("Repository '{}' not found", repo_name))
        })?;

    let storage_path = repo_model.storage_path.clone();

    // Run git operations in a blocking thread
    let result = tokio::task::spawn_blocking(move || {
        let domain = git::GitDomain::open(&storage_path)
            .map_err(|e| ToolError::ExecutionError(format!("Failed to open repo: {}", e)))?;

        let repo = domain.repo();

        // Get current head commit (parent)
        let parent_oid = repo.refname_to_id(&format!("refs/heads/{}", branch)).ok();
        let parent_ids: Vec<CommitOid> = parent_oid
            .map(|oid| CommitOid::from_git2(oid))
            .into_iter()
            .collect();

        // Build index with new files
        let mut index = repo
            .index()
            .map_err(|e| ToolError::ExecutionError(format!("Failed to get index: {}", e)))?;

        for file in files_data {
            let path = file
                .get("path")
                .and_then(|v| v.as_str())
                .ok_or_else(|| ToolError::ExecutionError("Each file must have a 'path'".into()))?;
            let content = file
                .get("content")
                .and_then(|v| v.as_str())
                .ok_or_else(|| ToolError::ExecutionError("Each file must have 'content'".into()))?;

            let _oid = repo.blob(content.as_bytes()).map_err(|e| {
                ToolError::ExecutionError(format!("Failed to write blob for '{}': {}", path, e))
            })?;

            index.add_path(path.as_ref()).map_err(|e| {
                ToolError::ExecutionError(format!("Failed to add '{}' to index: {}", path, e))
            })?;
        }

        let tree_oid = index
            .write_tree()
            .map_err(|e| ToolError::ExecutionError(format!("Failed to write tree: {}", e)))?;

        let tree_id = CommitOid::from_git2(tree_oid);

        // Author signature
        let author = CommitSignature {
            name: author_name.clone(),
            email: sender_email.clone(),
            time_secs: chrono::Utc::now().timestamp(),
            offset_minutes: 0,
        };

        // Committer signature: gitpanda <info@gitdata.ai>
        let committer = CommitSignature {
            name: "gitpanda".to_string(),
            email: "info@gitdata.ai".to_string(),
            time_secs: chrono::Utc::now().timestamp(),
            offset_minutes: 0,
        };

        let commit_oid = domain
            .commit_create(
                Some(&format!("refs/heads/{}", branch)),
                &author,
                &committer,
                &message,
                &tree_id,
                &parent_ids,
            )
            .map_err(|e| ToolError::ExecutionError(format!("Failed to create commit: {}", e)))?;

        Ok::<_, ToolError>(serde_json::json!({
            "commit_oid": commit_oid.to_string(),
            "branch": branch,
            "message": message,
            "author_name": author_name,
            "author_email": sender_email,
        }))
    })
    .await
    .map_err(|e| ToolError::ExecutionError(format!("Task join error: {}", e)))?;

    result
}

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

pub fn list_tool_definition() -> ToolDefinition {
    ToolDefinition::new("project_list_repos")
        .description(
            "List all repositories in the current project. \
             Returns repo name, description, default branch, privacy status, and creation time.",
        )
        .parameters(ToolSchema {
            schema_type: "object".into(),
            properties: None,
            required: None,
        })
}

pub fn create_tool_definition() -> ToolDefinition {
    let mut p = HashMap::new();
    p.insert("name".into(), ToolParam {
        name: "name".into(), param_type: "string".into(),
        description: Some("Repository name (required). Must be unique within the project.".into()),
        required: true, properties: None, items: None,
    });
    p.insert("description".into(), ToolParam {
        name: "description".into(), param_type: "string".into(),
        description: Some("Repository description. Optional.".into()),
        required: false, properties: None, items: None,
    });
    p.insert("is_private".into(), ToolParam {
        name: "is_private".into(), param_type: "boolean".into(),
        description: Some("Whether the repo is private. Defaults to false. Optional.".into()),
        required: false, properties: None, items: None,
    });
    ToolDefinition::new("project_create_repo")
        .description(
            "Create a new repository in the current project. \
             Requires admin or owner role. \
             The repo is initialized with a bare git structure.",
        )
        .parameters(ToolSchema {
            schema_type: "object".into(),
            properties: Some(p),
            required: Some(vec!["name".into()]),
        })
}

pub fn update_tool_definition() -> ToolDefinition {
    let mut p = HashMap::new();
    p.insert("name".into(), ToolParam {
        name: "name".into(), param_type: "string".into(),
        description: Some("Repository name (required).".into()),
        required: true, properties: None, items: None,
    });
    p.insert("description".into(), ToolParam {
        name: "description".into(), param_type: "string".into(),
        description: Some("New repository description. Optional.".into()),
        required: false, properties: None, items: None,
    });
    p.insert("is_private".into(), ToolParam {
        name: "is_private".into(), param_type: "boolean".into(),
        description: Some("New privacy setting. Optional.".into()),
        required: false, properties: None, items: None,
    });
    p.insert("default_branch".into(), ToolParam {
        name: "default_branch".into(), param_type: "string".into(),
        description: Some("New default branch name. Optional.".into()),
        required: false, properties: None, items: None,
    });
    ToolDefinition::new("project_update_repo")
        .description(
            "Update a repository's description, privacy, or default branch. \
             Requires admin or owner role.",
        )
        .parameters(ToolSchema {
            schema_type: "object".into(),
            properties: Some(p),
            required: Some(vec!["name".into()]),
        })
}

pub fn create_commit_tool_definition() -> ToolDefinition {
    let mut p = HashMap::new();
    p.insert("repo_name".into(), ToolParam {
        name: "repo_name".into(), param_type: "string".into(),
        description: Some("Repository name (required).".into()),
        required: true, properties: None, items: None,
    });
    p.insert("branch".into(), ToolParam {
        name: "branch".into(), param_type: "string".into(),
        description: Some("Branch to commit to. Defaults to 'main'. Optional.".into()),
        required: false, properties: None, items: None,
    });
    p.insert("message".into(), ToolParam {
        name: "message".into(), param_type: "string".into(),
        description: Some("Commit message (required).".into()),
        required: true, properties: None, items: None,
    });
    // files items
    let mut file_item = HashMap::new();
    file_item.insert("path".into(), ToolParam {
        name: "path".into(), param_type: "string".into(),
        description: Some("File path in the repo (required).".into()),
        required: true, properties: None, items: None,
    });
    file_item.insert("content".into(), ToolParam {
        name: "content".into(), param_type: "string".into(),
        description: Some("Full file content as string (required).".into()),
        required: true, properties: None, items: None,
    });
    p.insert("files".into(), ToolParam {
        name: "files".into(), param_type: "array".into(),
        description: Some("Array of files to commit (required, non-empty).".into()),
        required: true, properties: None,
        items: Some(Box::new(ToolParam {
            name: "".into(), param_type: "object".into(),
            description: None, required: true, properties: Some(file_item), items: None,
        })),
    });
    ToolDefinition::new("project_create_commit")
        .description(
            "Create a new commit in a repository. Commits the given files to the specified branch. \
             Requires admin or owner role. \
             Committer is always 'gitpanda <info@gitdata.ai>'. \
             Author is the sender's display name and email.",
        )
        .parameters(ToolSchema {
            schema_type: "object".into(),
            properties: Some(p),
            required: Some(vec!["repo_name".into(), "message".into(), "files".into()]),
        })
}
