//! Git blame ownership analysis — bus factor, ownership concentration per file

use super::git_utils;
use super::is_source_file;
use crate::output::OutputFormatter;
use glob::Pattern;
use normalize_analyze::ranked::{
    Column, DiffableRankEntry, RankEntry, format_delta, format_ranked_table,
};
use rayon::prelude::*;
use serde::Serialize;
use std::collections::HashMap;
use std::path::Path;

/// Ownership data for a file
#[derive(Debug, Clone, Serialize, schemars::JsonSchema)]
pub struct FileOwnership {
    pub path: String,
    pub total_lines: usize,
    /// Number of distinct authors
    pub authors: usize,
    /// Top author and their percentage of lines
    pub top_author: String,
    pub top_author_pct: f64,
    /// Bus factor: number of authors needed to cover >50% of lines
    pub bus_factor: usize,
    /// Delta vs baseline (set by `--diff`).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub delta: Option<f64>,
}

impl RankEntry for FileOwnership {
    fn columns() -> Vec<Column> {
        vec![
            Column::left("File"),
            Column::right("Lines"),
            Column::right("Auth"),
            Column::right("BF"),
            Column::left("Top Author"),
        ]
    }

    fn values(&self) -> Vec<String> {
        let top = format!("{} ({:.0}%)", self.top_author, self.top_author_pct * 100.0);
        let top_display = if top.len() > 28 {
            format!("{}...", &top[..25])
        } else {
            top
        };
        let bf_str = match self.delta {
            Some(d) => format!("{} ({})", self.bus_factor, format_delta(d, false)),
            None => self.bus_factor.to_string(),
        };
        vec![
            self.path.clone(),
            self.total_lines.to_string(),
            self.authors.to_string(),
            bf_str,
            top_display,
        ]
    }
}

impl DiffableRankEntry for FileOwnership {
    fn diff_key(&self) -> &str {
        &self.path
    }
    fn diff_score(&self) -> f64 {
        self.bus_factor as f64
    }
    fn set_delta(&mut self, delta: Option<f64>) {
        self.delta = delta;
    }
    fn delta(&self) -> Option<f64> {
        self.delta
    }
}

/// Per-repo ownership entry for multi-repo runs
#[derive(Debug, Serialize, schemars::JsonSchema)]
pub struct OwnershipRepoEntry {
    pub name: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
    pub files: Vec<FileOwnership>,
}

/// Ownership analysis report
#[derive(Debug, Serialize, schemars::JsonSchema)]
pub struct OwnershipReport {
    pub files: Vec<FileOwnership>,
    /// Per-repo results when run with --repos
    #[serde(skip_serializing_if = "Option::is_none")]
    pub repos: Option<Vec<OwnershipRepoEntry>>,
    /// Set when `--diff` is used.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub diff_ref: Option<String>,
}

fn format_ownership_data(files: &[FileOwnership]) -> String {
    let mut out = format_ranked_table(
        "# File Ownership (git blame)",
        files,
        Some("No ownership data found"),
    );
    out.push_str("\n\nBF = Bus Factor (authors needed for >50% ownership)\nLow bus factor (1) means single-author risk.");
    out
}

impl OutputFormatter for OwnershipReport {
    fn format_text(&self) -> String {
        if let Some(ref repos) = self.repos {
            let mut parts = Vec::new();
            for entry in repos {
                parts.push(format!("=== {} ===", entry.name));
                if let Some(ref err) = entry.error {
                    parts.push(format!("Error: {}", err));
                } else if entry.files.is_empty() {
                    parts.push("No ownership data found".to_string());
                } else {
                    parts.push(format_ownership_data(&entry.files));
                }
                parts.push(String::new());
            }
            return parts.join("\n");
        }

        if self.files.is_empty() {
            return "No ownership data found".to_string();
        }
        format_ownership_data(&self.files)
    }
}

/// Get ownership data for a single file via gix blame.
fn blame_file(root: &Path, path: &str) -> Option<FileOwnership> {
    let repo = git_utils::open_repo(root)?;
    let head_id = repo.head_id().ok()?;
    let path_bstr: &gix::bstr::BStr = path.as_bytes().into();
    let outcome = repo
        .blame_file(
            path_bstr,
            head_id.detach(),
            gix::repository::blame_file::Options::default(),
        )
        .ok()?;

    // Accumulate per-author line counts by resolving each commit's author.
    let mut author_lines: HashMap<String, usize> = HashMap::new();
    for entry in &outcome.entries {
        let lines = entry.len.get() as usize;
        let author_name = repo
            .find_object(entry.commit_id)
            .ok()
            .and_then(|obj| {
                obj.into_commit()
                    .author()
                    .ok()
                    .map(|a| String::from_utf8_lossy(a.name).into_owned())
            })
            .unwrap_or_else(|| "Unknown".to_string());
        *author_lines.entry(author_name).or_default() += lines;
    }

    if author_lines.is_empty() {
        return None;
    }

    let total_lines: usize = author_lines.values().sum();
    if total_lines == 0 {
        return None;
    }

    // Sort authors by line count descending
    let mut sorted: Vec<(String, usize)> = author_lines.into_iter().collect();
    sorted.sort_by(|a, b| b.1.cmp(&a.1));

    let top_author = sorted[0].0.clone();
    let top_author_pct = sorted[0].1 as f64 / total_lines as f64;
    let authors = sorted.len();

    // Bus factor: minimum authors to cover >50%
    let half = total_lines / 2;
    let mut cumulative = 0;
    let mut bus_factor = 0;
    for (_, count) in &sorted {
        cumulative += count;
        bus_factor += 1;
        if cumulative > half {
            break;
        }
    }

    Some(FileOwnership {
        path: path.to_string(),
        total_lines,
        authors,
        top_author,
        top_author_pct,
        bus_factor,
        delta: None,
    })
}

/// Collect source files from the git index (tracked files).
fn git_tracked_files(root: &Path) -> Vec<String> {
    git_utils::git_ls_files(root)
}

/// Analyze file ownership via git blame, returning the report.
pub fn analyze_ownership(
    root: &Path,
    limit: usize,
    exclude_patterns: &[String],
) -> Result<OwnershipReport, String> {
    let excludes: Vec<Pattern> = exclude_patterns
        .iter()
        .filter_map(|p| Pattern::new(p).ok())
        .collect();

    let git_dir = root.join(".git");
    if !git_dir.exists() {
        return Err("Not a git repository".to_string());
    }

    let tracked = git_tracked_files(root);
    let source_files: Vec<String> = tracked
        .into_iter()
        .filter(|path| {
            let p = Path::new(path.as_str());
            is_source_file(p) && !excludes.iter().any(|pat| pat.matches(path))
        })
        .collect();

    // Run git blame in parallel
    let mut files: Vec<FileOwnership> = source_files
        .par_iter()
        .filter_map(|path| blame_file(root, path))
        .collect();

    // Sort by bus factor ascending (riskiest first), then by top_author_pct descending
    normalize_analyze::ranked::rank_and_truncate(
        &mut files,
        limit,
        |a, b| {
            a.bus_factor
                .cmp(&b.bus_factor)
                .then_with(|| {
                    b.top_author_pct
                        .partial_cmp(&a.top_author_pct)
                        .unwrap_or(std::cmp::Ordering::Equal)
                })
                .then_with(|| b.total_lines.cmp(&a.total_lines))
        },
        |f| f.bus_factor as f64,
    );

    Ok(OwnershipReport {
        files,
        repos: None,
        diff_ref: None,
    })
}
