//! `diffsift calibrate` — reads reactions on DiffSift PR comments and updates
//! `.diffsift/calibration.json` with per-rule feedback weights.

use std::collections::HashMap;
use std::path::PathBuf;

use clap::Args;
use regex::Regex;

use diffsift_core::calibration::{CalibrationData, RuleWeight};

#[derive(Args)]
pub struct CalibrateArgs {
    /// Path to the git repository
    #[arg(long, default_value = ".")]
    pub repo: PathBuf,

    /// GitHub repository (owner/name). Auto-detected from git remote if not set.
    #[arg(long)]
    pub github_repo: Option<String>,

    /// GitHub API token. Uses GITHUB_TOKEN env var if not set.
    #[arg(long)]
    pub token: Option<String>,

    /// Maximum number of recent PRs to scan for comments (default 50)
    #[arg(long, default_value = "50")]
    pub max_prs: usize,
}

pub async fn run(args: CalibrateArgs) -> miette::Result<()> {
    let token = args
        .token
        .or_else(|| std::env::var("GITHUB_TOKEN").ok())
        .ok_or_else(|| miette::miette!("GITHUB_TOKEN required — set via --token or env var"))?;

    let repo = args
        .github_repo
        .or_else(|| detect_github_repo(&args.repo))
        .ok_or_else(|| {
            miette::miette!("Could not detect GitHub repo — use --github-repo owner/name")
        })?;

    let parts: Vec<&str> = repo.split('/').collect();
    if parts.len() != 2 {
        return Err(miette::miette!(
            "Invalid repo format '{}' — expected owner/name",
            repo
        ));
    }
    let (owner, name) = (parts[0], parts[1]);

    println!("Scanning {owner}/{name} for DiffSift comment reactions...");

    let client = reqwest::Client::new();
    let marker_re = Regex::new(r"<!-- diffsift:v1:([^:]+):([^:]+) -->").expect("valid regex");

    // Fetch recent PRs
    let prs_url = format!(
        "https://api.github.com/repos/{owner}/{name}/pulls?state=closed&per_page={}&sort=updated&direction=desc",
        args.max_prs
    );
    let prs: Vec<serde_json::Value> = github_get(&client, &prs_url, &token).await?;

    let mut rule_reactions: HashMap<String, (u32, u32)> = HashMap::new(); // rule_id → (thumbs_up, thumbs_down)
    let mut total_comments = 0u32;

    for pr in &prs {
        let pr_number = pr["number"].as_u64().unwrap_or(0);
        if pr_number == 0 {
            continue;
        }

        // Fetch PR review comments
        let comments_url = format!(
            "https://api.github.com/repos/{owner}/{name}/pulls/{pr_number}/comments?per_page=100"
        );
        let comments: Vec<serde_json::Value> =
            match github_get(&client, &comments_url, &token).await {
                Ok(c) => c,
                Err(_) => continue,
            };

        // Also fetch issue comments (summary comments)
        let issue_comments_url = format!(
            "https://api.github.com/repos/{owner}/{name}/issues/{pr_number}/comments?per_page=100"
        );
        let issue_comments: Vec<serde_json::Value> =
            github_get(&client, &issue_comments_url, &token)
                .await
                .unwrap_or_default();

        for comment in comments.iter().chain(issue_comments.iter()) {
            let body = comment["body"].as_str().unwrap_or("");
            // Check for DiffSift marker
            if let Some(caps) = marker_re.captures(body) {
                let pattern = caps.get(2).map(|m| m.as_str()).unwrap_or("unknown");

                // Fetch reactions for this comment
                let comment_id = comment["id"].as_u64().unwrap_or(0);
                if comment_id == 0 {
                    continue;
                }

                // Determine if it's a review comment or issue comment
                let reactions_url = if comment.get("pull_request_review_id").is_some() {
                    format!(
                        "https://api.github.com/repos/{owner}/{name}/pulls/comments/{comment_id}/reactions"
                    )
                } else {
                    format!(
                        "https://api.github.com/repos/{owner}/{name}/issues/comments/{comment_id}/reactions"
                    )
                };

                let reactions: Vec<serde_json::Value> =
                    match github_get(&client, &reactions_url, &token).await {
                        Ok(r) => r,
                        Err(_) => continue,
                    };

                let thumbs_up = reactions
                    .iter()
                    .filter(|r| r["content"].as_str() == Some("+1"))
                    .count() as u32;
                let thumbs_down = reactions
                    .iter()
                    .filter(|r| r["content"].as_str() == Some("-1"))
                    .count() as u32;

                let entry = rule_reactions.entry(pattern.to_string()).or_default();
                entry.0 += thumbs_up;
                entry.1 += thumbs_down;
                total_comments += 1;
            }
        }
    }

    if total_comments == 0 {
        println!(
            "No DiffSift comments with reactions found in {} PRs.",
            prs.len()
        );
        println!("Tip: reactions (👍/👎) on DiffSift PR comments are used for calibration.");
        return Ok(());
    }

    // Build calibration data
    let mut cal = CalibrationData {
        version: 1,
        generated_at: chrono_now(),
        rule_weights: HashMap::new(),
    };

    for (rule_id, (up, down)) in &rule_reactions {
        let total = up + down;
        if total == 0 {
            continue;
        }
        cal.rule_weights.insert(
            rule_id.clone(),
            RuleWeight {
                reactions: total,
                positive_rate: *up as f64 / total as f64,
            },
        );
    }

    let cal_path = args.repo.join(".diffsift").join("calibration.json");
    cal.save(&cal_path)
        .map_err(|e| miette::miette!("Failed to save calibration: {e}"))?;

    println!(
        "\nCalibration saved to {}\n  {} rules, {} total reactions across {} comments from {} PRs",
        cal_path.display(),
        cal.rule_weights.len(),
        rule_reactions.values().map(|(u, d)| u + d).sum::<u32>(),
        total_comments,
        prs.len()
    );

    // Print summary
    let mut sorted: Vec<_> = cal.rule_weights.iter().collect();
    sorted.sort_by(|a, b| {
        b.1.reactions
            .cmp(&a.1.reactions)
            .then(b.1.positive_rate.partial_cmp(&a.1.positive_rate).unwrap())
    });
    println!("\n  Rule       Reactions  Positive Rate");
    println!("  ---------  ---------  -------------");
    for (id, w) in sorted.iter().take(20) {
        println!(
            "  {:<9}  {:>9}  {:>12.0}%",
            id,
            w.reactions,
            w.positive_rate * 100.0
        );
    }

    Ok(())
}

async fn github_get(
    client: &reqwest::Client,
    url: &str,
    token: &str,
) -> miette::Result<Vec<serde_json::Value>> {
    let response = client
        .get(url)
        .header("Authorization", format!("Bearer {token}"))
        .header("Accept", "application/vnd.github+json")
        .header("X-GitHub-Api-Version", "2022-11-28")
        .header("User-Agent", "diffsift")
        .send()
        .await
        .map_err(|e| miette::miette!("GitHub API request failed: {e}"))?;

    if !response.status().is_success() {
        let status = response.status().as_u16();
        return Err(miette::miette!("GitHub API returned {status}"));
    }

    response
        .json()
        .await
        .map_err(|e| miette::miette!("Failed to parse GitHub response: {e}"))
}

fn detect_github_repo(repo_path: &std::path::Path) -> Option<String> {
    let output = std::process::Command::new("git")
        .args(["remote", "get-url", "origin"])
        .current_dir(repo_path)
        .output()
        .ok()?;
    let url = String::from_utf8(output.stdout).ok()?.trim().to_string();
    // Parse github.com/owner/name from HTTPS or SSH URLs
    if let Some(rest) = url
        .strip_prefix("https://github.com/")
        .or_else(|| url.strip_prefix("git@github.com:"))
    {
        let repo = rest.trim_end_matches(".git").trim_end_matches('/');
        if repo.contains('/') {
            return Some(repo.to_string());
        }
    }
    None
}

fn chrono_now() -> String {
    // Simple ISO 8601 timestamp without chrono dependency
    let duration = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default();
    format!("{}s-since-epoch", duration.as_secs())
}
