//! Git commit-related tools.

use super::ctx::GitToolCtx;
use agent::{ToolDefinition, ToolHandler, ToolParam, ToolRegistry, ToolSchema};
use chrono::TimeZone;
use std::collections::HashMap;

// --- Execution functions for each tool ---

async fn git_log_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde_json::Value, String> {
    let p: serde_json::Map<String, serde_json::Value> = serde_json::from_value(args).map_err(|e| e.to_string())?;
    let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
    let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
    let rev = p.get("rev").and_then(|v| v.as_str()).map(|s| s.to_string());
    let limit = p.get("limit").and_then(|v| v.as_u64()).unwrap_or(20) as usize;
    let skip = p.get("skip").and_then(|v| v.as_u64()).unwrap_or(0) as usize;

    let domain = ctx.open_repo(project_name, repo_name).await?;
    let commits = domain.commit_log(rev.as_deref(), skip, limit)
        .map_err(|e| e.to_string())?;

    // Flatten to simple JSON
    let result: Vec<_> = commits.iter().map(|c| {
        use chrono::TimeZone;
        let ts = c.author.time_secs - (c.author.offset_minutes as i64 * 60);
        let time_str = chrono::Utc.timestamp_opt(ts, 0).single()
            .map(|dt| dt.to_rfc3339()).unwrap_or_else(|| format!("{}", c.author.time_secs));

        let oid = c.oid.to_string();
        let short_oid = oid.get(..7).unwrap_or(&oid).to_string();

        serde_json::json!({
            "oid": oid,
            "short_oid": short_oid,
            "message": c.message,
            "summary": c.summary,
            "author_name": c.author.name,
            "author_email": c.author.email,
            "author_time": time_str,
            "committer_name": c.committer.name,
            "committer_email": c.committer.email,
            "parent_oids": c.parent_ids.iter().map(|p| p.to_string()).collect::<Vec<_>>(),
            "tree_oid": c.tree_id.to_string()
        })
    }).collect();

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

/// Resolve a rev string to commit metadata. Tries full OID first (exactly 40 hex chars),
/// falls back to prefix lookup (branch, tag, short hash).
fn resolve_commit(domain: &git::GitDomain, rev: &str) -> Result<git::commit::types::CommitMeta, String> {
    if rev.len() == 40 && rev.chars().all(|c| c.is_ascii_hexdigit()) {
        domain.commit_get(&git::commit::types::CommitOid::new(rev)).map_err(|e| e.to_string())
    } else {
        domain.commit_get_prefix(rev).map_err(|e| e.to_string())
    }
}

async fn git_show_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde_json::Value, String> {
    let p: serde_json::Map<String, serde_json::Value> = serde_json::from_value(args).map_err(|e| e.to_string())?;
    let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
    let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
    let rev = p.get("rev").and_then(|v| v.as_str()).ok_or("missing rev")?;

    let domain = ctx.open_repo(project_name, repo_name).await?;
    let meta = resolve_commit(&domain, rev).map_err(|e| e.to_string())?;

    let refs = domain.commit_refs(&meta.oid).map_err(|e| e.to_string())?;

    use chrono::TimeZone;
    let ts = meta.author.time_secs - (meta.author.offset_minutes as i64 * 60);
    let author_time = chrono::Utc.timestamp_opt(ts, 0).single()
        .map(|dt| dt.to_rfc3339()).unwrap_or_else(|| format!("{}", meta.author.time_secs));

    let oid = meta.oid.to_string();
    let short_oid = oid.get(..7).unwrap_or(&oid).to_string();

    Ok(serde_json::json!({
        "commit": {
            "oid": oid, "short_oid": short_oid, "message": meta.message, "summary": meta.summary,
            "author_name": meta.author.name, "author_email": meta.author.email, "author_time": author_time,
            "committer_name": meta.committer.name, "committer_email": meta.committer.email,
            "parent_oids": meta.parent_ids.iter().map(|p| p.to_string()).collect::<Vec<_>>(),
            "tree_oid": meta.tree_id.to_string()
        },
        "refs": refs.into_iter().map(|r| serde_json::json!({ "name": r.name, "is_tag": r.is_tag })).collect::<Vec<_>>()
    }))
}

async fn git_search_commits_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde_json::Value, String> {
    let p: serde_json::Map<String, serde_json::Value> = serde_json::from_value(args).map_err(|e| e.to_string())?;
    let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
    let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
    let query = p.get("query").and_then(|v| v.as_str()).ok_or("missing query")?;
    let limit = p.get("limit").and_then(|v| v.as_u64()).unwrap_or(20) as usize;

    let domain = ctx.open_repo(project_name, repo_name).await?;
    // Fetch extra commits to have enough candidates after filtering
    let walk_limit = limit.saturating_mul(2).max(100);
    let commits = domain.commit_log(Some("HEAD"), 0, walk_limit).map_err(|e| e.to_string())?;
    let q = query.to_lowercase();

    let result: Vec<_> = commits.iter()
        .filter(|c| c.message.to_lowercase().contains(&q))
        .take(limit)
        .map(|c| flatten_commit(c))
        .collect();

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

fn flatten_commit(c: &git::commit::types::CommitMeta) -> serde_json::Value {
    use chrono::TimeZone;
    let ts = c.author.time_secs - (c.author.offset_minutes as i64 * 60);
    let author_time = chrono::Utc.timestamp_opt(ts, 0).single()
        .map(|dt| dt.to_rfc3339()).unwrap_or_else(|| format!("{}", c.author.time_secs));
    let oid = c.oid.to_string();
    serde_json::json!({
        "oid": oid.clone(),
        "short_oid": oid.get(..7).unwrap_or(&oid).to_string(),
        "message": c.message, "summary": c.summary,
        "author_name": c.author.name, "author_email": c.author.email, "author_time": author_time,
        "committer_name": c.committer.name, "committer_email": c.committer.email,
        "parent_oids": c.parent_ids.iter().map(|p| p.to_string()).collect::<Vec<_>>(),
        "tree_oid": c.tree_id.to_string()
    })
}

async fn git_commit_info_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde_json::Value, String> {
    let p: serde_json::Map<String, serde_json::Value> = serde_json::from_value(args).map_err(|e| e.to_string())?;
    let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
    let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
    let rev = p.get("rev").and_then(|v| v.as_str()).ok_or("missing rev")?;

    let domain = ctx.open_repo(project_name, repo_name).await?;
    let meta = resolve_commit(&domain, rev).map_err(|e| e.to_string())?;

    Ok(flatten_commit(&meta))
}

async fn git_graph_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde_json::Value, String> {
    let p: serde_json::Map<String, serde_json::Value> = serde_json::from_value(args).map_err(|e| e.to_string())?;
    let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
    let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
    let rev = p.get("rev").and_then(|v| v.as_str()).map(|s| s.to_string());
    let limit = p.get("limit").and_then(|v| v.as_u64()).unwrap_or(20) as usize;

    let domain = ctx.open_repo(project_name, repo_name).await?;
    let commits = domain.commit_log(rev.as_deref(), 0, limit).map_err(|e| e.to_string())?;

    let mut col_map: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
    let lines: Vec<_> = commits.iter().map(|m| {
        let lane_index = *col_map.get(m.oid.as_str()).unwrap_or(&0);
        let oid = m.oid.to_string();
        let refs = match domain.commit_refs(&m.oid) {
            Ok(refs) => refs.iter().map(|r| {
                if r.is_tag { format!("tag: {}", r.name.trim_start_matches("refs/tags/")) }
                else if r.is_remote { r.name.trim_start_matches("refs/remotes/").to_string() }
                else { r.name.trim_start_matches("refs/heads/").to_string() }
            }).collect::<Vec<_>>().join(", "),
            Err(_) => String::new(),
        };
        for (i, p) in m.parent_ids.iter().enumerate() {
            if i == 0 { col_map.insert(p.to_string(), lane_index); } else { col_map.remove(p.as_str()); }
        }
        let ts = m.author.time_secs - (m.author.offset_minutes as i64 * 60);
        let author_time = chrono::Utc.timestamp_opt(ts, 0).single()
            .map(|dt| dt.to_rfc3339()).unwrap_or_else(|| format!("{}", m.author.time_secs));

        serde_json::json!({
            "oid": oid.clone(),
            "short_oid": oid.get(..7).unwrap_or(&oid).to_string(),
            "refs": refs,
            "short_message": m.summary,
            "lane_index": lane_index,
            "author_name": m.author.name,
            "author_email": m.author.email,
            "author_time": author_time,
            "parent_oids": m.parent_ids.iter().map(|p| p.to_string()).collect::<Vec<_>>()
        })
    }).collect();

    Ok(serde_json::to_value(lines).map_err(|e| e.to_string())?)
}

async fn git_reflog_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde_json::Value, String> {
    let p: serde_json::Map<String, serde_json::Value> = serde_json::from_value(args).map_err(|e| e.to_string())?;
    let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
    let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
    let ref_name = p.get("ref_name").and_then(|v| v.as_str()).map(|s| s.to_string());
    let limit = p.get("limit").and_then(|v| v.as_u64()).unwrap_or(50) as usize;

    let domain = ctx.open_repo(project_name, repo_name).await?;
    let entries = domain.reflog_entries(ref_name.as_deref()).map_err(|e| e.to_string())?;

    let result: Vec<_> = entries.iter()
        .take(limit)
        .map(|e| {
            // Convert to UTC by subtracting the timezone offset, consistent
            // with all other timestamp conversions in this module.
            let ts = e.time_secs - (e.offset_minutes as i64 * 60);
            let time_str = chrono::Utc.timestamp_opt(ts, 0).single()
                .map(|dt| dt.to_rfc3339()).unwrap_or_else(|| format!("{}", e.time_secs));
            serde_json::json!({
                "oid_new": e.oid_new.to_string(), "oid_old": e.oid_old.to_string(),
                "committer_name": e.committer_name, "committer_email": e.committer_email,
                "time": time_str, "message": e.message, "ref_name": e.ref_name
            })
        })
        .collect();

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

/// Common required params used across all git tools.
fn common_params() -> HashMap<String, ToolParam> {
    HashMap::from([
        ("project_name".into(), ToolParam {
            name: "project_name".into(),
            param_type: "string".into(),
            description: Some("Project name (slug)".into()),
            required: true,
            properties: None,
            items: None,
        }),
        ("repo_name".into(), ToolParam {
            name: "repo_name".into(),
            param_type: "string".into(),
            description: Some("Repository name".into()),
            required: true,
            properties: None,
            items: None,
        }),
    ])
}

pub fn register_git_tools(registry: &mut ToolRegistry) {
    // git_log
    let mut p = common_params();
    p.insert("rev".into(), ToolParam {
        name: "rev".into(), param_type: "string".into(),
        description: Some("Revision/range specifier (branch name, commit hash, etc.)".into()),
        required: false, properties: None, items: None,
    });
    p.insert("limit".into(), ToolParam {
        name: "limit".into(), param_type: "integer".into(),
        description: Some("Maximum number of commits to return".into()),
        required: false, properties: None, items: None,
    });
    p.insert("skip".into(), ToolParam {
        name: "skip".into(), param_type: "integer".into(),
        description: Some("Number of commits to skip".into()),
        required: false, properties: None, items: None,
    });
    let schema = ToolSchema { schema_type: "object".into(), properties: Some(p), required: Some(vec!["project_name".into(), "repo_name".into()]) };
    registry.register(
        ToolDefinition::new("git_log").description("List commits in a repository, optionally filtered by revision range.").parameters(schema),
        ToolHandler::new(|ctx, args| {
            let gctx = super::ctx::GitToolCtx::new(ctx);
            Box::pin(async move {
                git_log_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError)
            })
        }),
    );

    // git_show
    let mut p = common_params();
    p.insert("rev".into(), ToolParam {
        name: "rev".into(), param_type: "string".into(),
        description: Some("Revision to show (commit hash, branch, tag)".into()),
        required: true, properties: None, items: None,
    });
    let schema = ToolSchema { schema_type: "object".into(), properties: Some(p), required: Some(vec!["project_name".into(), "repo_name".into(), "rev".into()]) };
    registry.register(
        ToolDefinition::new("git_show").description("Show detailed commit information including message, author, refs, and diff stats.").parameters(schema),
        ToolHandler::new(|ctx, args| {
            let gctx = super::ctx::GitToolCtx::new(ctx);
            Box::pin(async move {
                git_show_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError)
            })
        }),
    );

    // git_search_commits
    let mut p = common_params();
    p.insert("query".into(), ToolParam {
        name: "query".into(), param_type: "string".into(),
        description: Some("Keyword to search in commit messages".into()),
        required: true, properties: None, items: None,
    });
    p.insert("limit".into(), ToolParam {
        name: "limit".into(), param_type: "integer".into(),
        description: Some("Maximum results to return".into()),
        required: false, properties: None, items: None,
    });
    let schema = ToolSchema { schema_type: "object".into(), properties: Some(p), required: Some(vec!["project_name".into(), "repo_name".into(), "query".into()]) };
    registry.register(
        ToolDefinition::new("git_search_commits").description("Search commit messages for a keyword and return matching commits.").parameters(schema),
        ToolHandler::new(|ctx, args| {
            let gctx = super::ctx::GitToolCtx::new(ctx);
            Box::pin(async move {
                git_search_commits_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError)
            })
        }),
    );

    // git_commit_info
    let mut p = common_params();
    p.insert("rev".into(), ToolParam {
        name: "rev".into(), param_type: "string".into(),
        description: Some("Revision to look up (full or short commit hash, branch, tag)".into()),
        required: true, properties: None, items: None,
    });
    let schema = ToolSchema { schema_type: "object".into(), properties: Some(p), required: Some(vec!["project_name".into(), "repo_name".into(), "rev".into()]) };
    registry.register(
        ToolDefinition::new("git_commit_info").description("Get detailed metadata for a specific commit (author, committer, parents, tree)." ).parameters(schema),
        ToolHandler::new(|ctx, args| {
            let gctx = super::ctx::GitToolCtx::new(ctx);
            Box::pin(async move {
                git_commit_info_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError)
            })
        }),
    );

    // git_graph
    let mut p = common_params();
    p.insert("rev".into(), ToolParam {
        name: "rev".into(), param_type: "string".into(),
        description: Some("Starting revision (default: HEAD)".into()),
        required: false, properties: None, items: None,
    });
    p.insert("limit".into(), ToolParam {
        name: "limit".into(), param_type: "integer".into(),
        description: Some("Maximum number of commits (default: 20)".into()),
        required: false, properties: None, items: None,
    });
    let schema = ToolSchema { schema_type: "object".into(), properties: Some(p), required: Some(vec!["project_name".into(), "repo_name".into()]) };
    registry.register(
        ToolDefinition::new("git_graph").description("Show an ASCII commit graph with branch lanes and refs.").parameters(schema),
        ToolHandler::new(|ctx, args| {
            let gctx = super::ctx::GitToolCtx::new(ctx);
            Box::pin(async move {
                git_graph_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError)
            })
        }),
    );

    // git_reflog
    let mut p = common_params();
    p.insert("ref_name".into(), ToolParam {
        name: "ref_name".into(), param_type: "string".into(),
        description: Some("Reference name (e.g. refs/heads/main). Defaults to all refs.".into()),
        required: false, properties: None, items: None,
    });
    p.insert("limit".into(), ToolParam {
        name: "limit".into(), param_type: "integer".into(),
        description: Some("Maximum number of entries (default: 50)".into()),
        required: false, properties: None, items: None,
    });
    let schema = ToolSchema { schema_type: "object".into(), properties: Some(p), required: Some(vec!["project_name".into(), "repo_name".into()]) };
    registry.register(
        ToolDefinition::new("git_reflog").description("Show the reference log (reflog) recording when branch tips and refs were updated.").parameters(schema),
        ToolHandler::new(|ctx, args| {
            let gctx = super::ctx::GitToolCtx::new(ctx);
            Box::pin(async move {
                git_reflog_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError)
            })
        }),
    );
}
