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

use clap::Args;

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

    /// Start date (e.g., "2024-01-01")
    #[arg(long)]
    pub since: Option<String>,

    /// End date (default: HEAD)
    #[arg(long)]
    pub until: Option<String>,

    /// Maximum commits to analyze
    #[arg(long, default_value = "100")]
    pub max_commits: usize,

    /// Output format
    #[arg(long, default_value = "summary")]
    pub format: ScanOutputFormat,

    /// Only show hunks at this priority or above
    #[arg(long, default_value = "moderate")]
    pub min_priority: MinPriority,

    /// Only analyze files matching this glob
    #[arg(long)]
    pub path_filter: Option<String>,

    /// Generate suggested .diffsift/rules.toml entries based on hotspot files
    #[arg(long)]
    pub suggest_rules: bool,
}

#[derive(Clone, Debug, clap::ValueEnum)]
pub enum ScanOutputFormat {
    Summary,
    Json,
}

#[derive(Clone, Debug, clap::ValueEnum)]
pub enum MinPriority {
    High,
    Moderate,
    Low,
    Filtered,
}

impl MinPriority {
    fn to_priority(&self) -> diffsift_core::Priority {
        match self {
            MinPriority::High => diffsift_core::Priority::High,
            MinPriority::Moderate => diffsift_core::Priority::Moderate,
            MinPriority::Low => diffsift_core::Priority::Low,
            MinPriority::Filtered => diffsift_core::Priority::Filtered,
        }
    }
}

#[derive(serde::Serialize)]
struct ScanResult {
    commits_scanned: usize,
    date_range: DateRange,
    rule_frequency: Vec<RuleFrequency>,
    priority_distribution: PriorityDist,
    reverted_commits: usize,
    hotspot_files: Vec<HotspotFile>,
    category_distribution: Vec<CategoryDist>,
}

#[derive(serde::Serialize)]
struct DateRange {
    since: Option<String>,
    until: String,
}

#[derive(serde::Serialize)]
struct RuleFrequency {
    rule_id: String,
    rule_name: String,
    count: usize,
}

#[derive(serde::Serialize)]
struct PriorityDist {
    high: usize,
    moderate: usize,
    low: usize,
    filtered: usize,
}

#[derive(serde::Serialize)]
struct HotspotFile {
    path: String,
    high_count: usize,
    commit_count: usize,
}

#[derive(serde::Serialize)]
struct CategoryDist {
    category: String,
    count: usize,
}

pub fn run(args: ScanArgs) -> miette::Result<()> {
    let repo = gix::open(&args.repo).map_err(|e| miette::miette!("Failed to open repo: {e}"))?;

    // Walk commit log
    let commits = collect_commits(&repo, &args)?;

    if commits.is_empty() {
        eprintln!("No commits found in the specified range.");
        return Ok(());
    }

    // Classify each commit's diff
    let mut rule_counts: HashMap<(String, String), usize> = HashMap::new();
    let mut priority_dist = PriorityDist {
        high: 0,
        moderate: 0,
        low: 0,
        filtered: 0,
    };
    let mut category_counts: HashMap<String, usize> = HashMap::new();
    let mut file_high_counts: HashMap<String, (usize, std::collections::HashSet<usize>)> =
        HashMap::new();
    let min_priority = args.min_priority.to_priority();
    let mut commits_scanned = 0;
    let mut reverted_commits = 0;

    // Detect reverts: commits whose message starts with "Revert "
    for (commit_id, _) in &commits {
        let msg = get_commit_message(&repo, commit_id);
        if msg.starts_with("Revert ") || msg.starts_with("revert ") {
            reverted_commits += 1;
        }
    }

    for (idx, (commit_id, parent_id)) in commits.iter().enumerate() {
        let result = diffsift_core::classify_diff(&args.repo, parent_id, commit_id);
        let classified = match result {
            Ok(c) => c,
            Err(_) => continue, // Skip commits that fail to classify (e.g., merge commits)
        };

        commits_scanned += 1;

        for file in &classified.files {
            // Apply path filter if specified
            if let Some(ref filter) = args.path_filter {
                if !file.path.contains(filter.as_str()) {
                    continue;
                }
            }

            for hunk in &file.hunks {
                // Count all hunks for distribution
                match hunk.priority {
                    diffsift_core::Priority::High => priority_dist.high += 1,
                    diffsift_core::Priority::Moderate => priority_dist.moderate += 1,
                    diffsift_core::Priority::Low => priority_dist.low += 1,
                    diffsift_core::Priority::Filtered => priority_dist.filtered += 1,
                }

                if hunk.priority < min_priority {
                    continue;
                }

                // Count categories
                let cat_name = format!("{:?}", hunk.category).to_lowercase();
                *category_counts.entry(cat_name).or_default() += 1;

                // Count rules
                for pattern in &hunk.patterns {
                    let key = (pattern.pattern_id.clone(), pattern.pattern_name.clone());
                    *rule_counts.entry(key).or_default() += 1;
                }

                // Track hotspot files
                if hunk.priority == diffsift_core::Priority::High {
                    let entry = file_high_counts
                        .entry(file.path.clone())
                        .or_insert_with(|| (0, std::collections::HashSet::new()));
                    entry.0 += 1;
                    entry.1.insert(idx);
                }
            }
        }
    }

    // Build result
    let mut rule_frequency: Vec<RuleFrequency> = rule_counts
        .into_iter()
        .map(|((id, name), count)| RuleFrequency {
            rule_id: id,
            rule_name: name,
            count,
        })
        .collect();
    rule_frequency.sort_by(|a, b| b.count.cmp(&a.count));

    let mut hotspot_files: Vec<HotspotFile> = file_high_counts
        .into_iter()
        .map(|(path, (high_count, commits))| HotspotFile {
            path,
            high_count,
            commit_count: commits.len(),
        })
        .collect();
    hotspot_files.sort_by(|a, b| b.high_count.cmp(&a.high_count));
    hotspot_files.truncate(10);

    let mut category_distribution: Vec<CategoryDist> = category_counts
        .into_iter()
        .map(|(category, count)| CategoryDist { category, count })
        .collect();
    category_distribution.sort_by(|a, b| b.count.cmp(&a.count));

    let scan_result = ScanResult {
        commits_scanned,
        date_range: DateRange {
            since: args.since.clone(),
            until: args.until.clone().unwrap_or_else(|| "HEAD".to_string()),
        },
        rule_frequency,
        priority_distribution: priority_dist,
        reverted_commits,
        hotspot_files,
        category_distribution,
    };

    match args.format {
        ScanOutputFormat::Json => {
            let json = serde_json::to_string_pretty(&scan_result)
                .map_err(|e| miette::miette!("JSON serialization failed: {e}"))?;
            println!("{json}");
        }
        ScanOutputFormat::Summary => {
            print_summary(&scan_result);
        }
    }

    // Suggest rules.toml entries for hotspot files
    if args.suggest_rules && !scan_result.hotspot_files.is_empty() {
        println!("\n# Suggested .diffsift/rules.toml entries based on hotspots:\n");
        for file in &scan_result.hotspot_files {
            if file.high_count >= 3 {
                // Suggest high priority for directories with many high findings
                let dir = std::path::Path::new(&file.path)
                    .parent()
                    .map(|p| format!("{}/**", p.display()))
                    .unwrap_or_else(|| file.path.clone());
                println!("[[rules]]");
                println!("paths = [\"{}\"]", dir);
                println!(
                    "min_priority = \"high\"  # {} high findings across {} commits",
                    file.high_count, file.commit_count
                );
                println!();
            }
        }
    }

    Ok(())
}

fn get_commit_message(repo: &gix::Repository, commit_id: &str) -> String {
    repo.rev_parse_single(commit_id)
        .ok()
        .and_then(|r| r.object().ok())
        .and_then(|o| o.peel_to_commit().ok())
        .and_then(|c| c.message().ok().map(|m| m.title.to_string()))
        .unwrap_or_default()
}

fn collect_commits(
    repo: &gix::Repository,
    args: &ScanArgs,
) -> miette::Result<Vec<(String, String)>> {
    // Start from HEAD or --until
    let start_ref = args.until.as_deref().unwrap_or("HEAD");
    let start = repo
        .rev_parse_single(start_ref)
        .map_err(|e| miette::miette!("Failed to resolve {start_ref}: {e}"))?
        .object()
        .map_err(|e| miette::miette!("Failed to get object: {e}"))?
        .peel_to_commit()
        .map_err(|e| miette::miette!("Failed to peel to commit: {e}"))?;

    let mut commits = Vec::new();
    let mut current = start;
    let mut count = 0;

    loop {
        if count >= args.max_commits {
            break;
        }

        // Get parent
        let parents: Vec<_> = current.parent_ids().map(|id| id.detach()).collect();

        if parents.is_empty() {
            break; // Root commit
        }

        let commit_id = current.id().to_string();
        let parent_id = parents[0].to_string(); // Use first parent (skip merge second parents)

        // Check --since filter
        if let Some(ref since) = args.since {
            let time = current
                .time()
                .map_err(|e| miette::miette!("Failed to get commit time: {e}"))?;
            let commit_date = format_gix_time(time);
            if commit_date < *since {
                break;
            }
        }

        commits.push((commit_id, parent_id));
        count += 1;

        // Move to parent
        current = repo
            .find_object(parents[0])
            .map_err(|e| miette::miette!("Failed to find parent: {e}"))?
            .peel_to_commit()
            .map_err(|e| miette::miette!("Failed to peel parent: {e}"))?;
    }

    Ok(commits)
}

fn format_gix_time(time: gix::date::Time) -> String {
    chrono_lite_from_epoch(time.seconds)
}

/// Minimal epoch-to-date without pulling in chrono. Produces "YYYY-MM-DD".
fn chrono_lite_from_epoch(epoch_secs: i64) -> String {
    // Days since 1970-01-01
    let days = epoch_secs / 86400;
    let (year, month, day) = civil_from_days(days);
    format!("{year:04}-{month:02}-{day:02}")
}

/// Convert days since 1970-01-01 to (year, month, day).
/// Algorithm from Howard Hinnant's date algorithms.
fn civil_from_days(days: i64) -> (i64, u32, u32) {
    let z = days + 719468;
    let era = if z >= 0 { z } else { z - 146096 } / 146097;
    let doe = (z - era * 146097) as u32;
    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
    let y = yoe as i64 + era * 400;
    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
    let mp = (5 * doy + 2) / 153;
    let d = doy - (153 * mp + 2) / 5 + 1;
    let m = if mp < 10 { mp + 3 } else { mp - 9 };
    let y = if m <= 2 { y + 1 } else { y };
    (y, m, d)
}

fn print_summary(result: &ScanResult) {
    println!(
        "Scanned {} commits ({})",
        result.commits_scanned,
        match &result.date_range.since {
            Some(since) => format!("{} to {}", since, result.date_range.until),
            None => format!("up to {}", result.date_range.until),
        }
    );
    if result.reverted_commits > 0 {
        println!(
            "  ⚠ {} reverted commits detected (indicates risky changes)",
            result.reverted_commits
        );
    }
    println!();

    if !result.rule_frequency.is_empty() {
        let total: usize = result.rule_frequency.iter().map(|r| r.count).sum();
        println!("Rule Frequency:");
        for rule in &result.rule_frequency {
            let pct = if total > 0 {
                (rule.count as f64 / total as f64 * 100.0) as u32
            } else {
                0
            };
            println!(
                "  {:<6} {:<35} {} hits ({}%)",
                rule.rule_id, rule.rule_name, rule.count, pct
            );
        }
        println!();
    }

    let total_hunks = result.priority_distribution.high
        + result.priority_distribution.moderate
        + result.priority_distribution.low
        + result.priority_distribution.filtered;

    println!("Priority Distribution:");
    if total_hunks > 0 {
        let pct = |n: usize| (n as f64 / total_hunks as f64 * 100.0) as u32;
        println!(
            "  HIGH      {:>4} hunks ({}%)",
            result.priority_distribution.high,
            pct(result.priority_distribution.high)
        );
        println!(
            "  MODERATE  {:>4} hunks ({}%)",
            result.priority_distribution.moderate,
            pct(result.priority_distribution.moderate)
        );
        println!(
            "  LOW       {:>4} hunks ({}%)",
            result.priority_distribution.low,
            pct(result.priority_distribution.low)
        );
        println!(
            "  FILTERED  {:>4} hunks ({}%)",
            result.priority_distribution.filtered,
            pct(result.priority_distribution.filtered)
        );
    } else {
        println!("  (no hunks classified)");
    }
    println!();

    if !result.hotspot_files.is_empty() {
        println!("Hotspot Files (most HIGH-priority changes):");
        for file in &result.hotspot_files {
            println!(
                "  {:<40} {} HIGH hunks across {} commits",
                file.path, file.high_count, file.commit_count
            );
        }
        println!();
    }

    if !result.category_distribution.is_empty() {
        let total: usize = result.category_distribution.iter().map(|c| c.count).sum();
        println!("Category Distribution:");
        for cat in &result.category_distribution {
            let pct = if total > 0 {
                (cat.count as f64 / total as f64 * 100.0) as u32
            } else {
                0
            };
            println!("  {:<20} {:>4} ({}%)", cat.category, cat.count, pct);
        }
    }
}
