//! Blame/annotate view for files.
//!
//! Covers one route: `/{repo}/commits/{sha}/blame/{*path}`.  The handler
//! resolves the file at `{sha}:{path}` and runs `gix`'s blame algorithm to
//! attribute each line to the commit that last modified it.  Results are
//! displayed in a three-column table: commit SHA, line number, and code
//! content (cgit style).

use askama::Template;
use askama_web::WebTemplate;
use axum::extract::{Path, State};
use axum::response::IntoResponse;
use serde::Deserialize;

use crate::error::{Error, Result};
use crate::routes::{AppState, RepoName};

/// Path parameters for the blame route.
#[derive(Deserialize)]
pub(super) struct BlamePath {
    sha: String,
    path: String,
}

/// A single annotated line in the blame view.
struct BlameLine {
    /// Line number (1-based).
    num: usize,
    /// Full commit SHA.
    sha: String,
    /// Short SHA (first 8 characters).
    short_sha: String,
    /// Color variable for the SHA column.
    color: String,
    /// The line content.
    content: String,
}

/// A breadcrumb segment for the navigation bar.
struct Breadcrumb {
    label: String,
    url: String,
}

/// Template data for the blame page.
#[derive(Template, WebTemplate)]
#[template(path = "blame.html")]
struct BlameView {
    repo: String,
    sha: String,
    path: String,
    tree_url: String,
    breadcrumbs: Vec<Breadcrumb>,
    lines: Vec<BlameLine>,
}

/// Color palette for commit highlighting (matches commit.rs graph colors).
const COLORS: &[&str] = &[
    "var(--blue)",
    "var(--red)",
    "var(--green)",
    "var(--yellow)",
    "var(--violet)",
    "var(--magenta)",
    "var(--cyan)",
    "var(--orange)",
];

fn commit_color(sha: &str) -> &'static str {
    let hash: u64 = sha.bytes().fold(0u64, |acc, b| acc.wrapping_mul(31).wrapping_add(b as u64));
    COLORS[hash as usize % COLORS.len()]
}

/// Renders the blame view for a file.
pub async fn handler(
    State(state): State<AppState>,
    RepoName(name): RepoName,
    Path(params): Path<BlamePath>,
) -> Result<impl IntoResponse> {
    if std::path::Path::new(&params.path)
        .components()
        .any(|c| !matches!(c, std::path::Component::Normal(_)))
    {
        return Err(Error::BadRequest("invalid path".to_string()));
    }

    let git_repo =
        gix::open(state.root.join(&name)).map_err(|_| Error::RepoNotFound(name.clone()))?;

    let oid = gix::ObjectId::from_hex(params.sha.as_bytes())
        .map_err(|_| Error::BadRequest(format!("invalid sha: {}", params.sha)))?;

    // Verify the commit exists.
    let _commit = git_repo
        .find_object(oid)
        .map_err(|_| Error::NotFound(params.sha.clone()))?
        .try_into_commit()
        .map_err(|_| Error::BadRequest(format!("{} is not a commit", params.sha)))?;

    let file_path = gix::bstr::BStr::new(params.path.as_bytes());

    let opts = gix::repository::blame_file::Options::default();
    let outcome = git_repo
        .blame_file(file_path, oid, opts)
        .map_err(|e| Error::GitCorrupt(e.to_string()))?;

    // Split the blob into lines.
    let content = String::from_utf8_lossy(&outcome.blob).into_owned();
    let file_lines: Vec<&str> = content.split('\n').collect();

    // Build per-line blame entries.
    let mut lines: Vec<BlameLine> = Vec::new();
    for entry in &outcome.entries {
        let sha = entry.commit_id.to_string();
        let color = commit_color(&sha);
        let short_sha = sha[..8].to_string();

        for i in 0..entry.len.get() {
            let line_idx = (entry.start_in_blamed_file + i) as usize;
            let content = file_lines.get(line_idx).unwrap_or(&"").to_string();
            lines.push(BlameLine {
                num: line_idx + 1,
                sha: sha.clone(),
                short_sha: short_sha.clone(),
                color: color.to_string(),
                content,
            });
        }
    }

    let b = crate::base_url();
    let tree_url = format!("{b}/{name}/commits/{}/tree/{}", params.sha, params.path);

    let mut breadcrumbs = Vec::new();
    breadcrumbs.push(Breadcrumb {
        label: "tree".to_string(),
        url: format!("{b}/{name}/commits/{}/tree", params.sha),
    });
    if !params.path.is_empty() {
        let mut accumulated = String::new();
        for segment in params.path.split('/') {
            if !accumulated.is_empty() {
                accumulated.push('/');
            }
            accumulated.push_str(segment);
            breadcrumbs.push(Breadcrumb {
                label: segment.to_string(),
                url: format!("{b}/{name}/commits/{}/tree/{accumulated}", params.sha),
            });
        }
    }

    Ok(BlameView {
        repo: name,
        sha: params.sha,
        path: params.path,
        tree_url,
        breadcrumbs,
        lines,
    })
}
