//! Route handlers for branch pages.
//!
//! Covers two routes: the branch list (`/{repo}/branches`) and the branch
//! detail page (`/{repo}/branches/{*branch}`).  The detail handler resolves
//! the branch ref to its tip commit and walks ancestors to produce a log view.

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

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

const PAGE_SIZE: usize = 100;

#[derive(Deserialize)]
pub(super) struct BranchPath {
    branch: String,
}

#[derive(Deserialize)]
pub(super) struct Pagination {
    #[serde(default)]
    page: usize,
}

/// Template context for a single branch in the list.
struct BranchSummary {
    name: String,
    date: String,
    timestamp: i64,
    is_default: bool,
}

/// Template context for the branch list page.
#[derive(Template, WebTemplate)]
#[template(path = "branch_list.html")]
struct BranchList {
    repo: String,
    branches: Vec<BranchSummary>,
}

/// Template context for a single commit row in the log.
struct CommitSummary {
    sha: String,
    message: String,
    author: String,
    date: String,
}

/// Template context for the branch detail (log) page.
#[derive(Template, WebTemplate)]
#[template(path = "branch_detail.html")]
struct BranchDetail {
    repo: String,
    branch: String,
    tip_sha: String,
    commits: Vec<CommitSummary>,
    page: usize,
    has_prev: bool,
    has_next: bool,
}

/// Renders the full list of branches for a repository.
pub async fn list(
    State(state): State<AppState>,
    RepoName(repo): RepoName,
) -> Result<impl IntoResponse> {
    let git_repo =
        gix::open(state.root.join(&repo)).map_err(|_| Error::RepoNotFound(repo.clone()))?;

    let head_name = git_repo.head_name().ok().flatten();

    // `.local_branches()` returns a `Result<impl Iterator<Item = Result<Reference>>>`.
    // The first `.flatten()` unwraps the outer `Result`, the second unwraps
    // each `Result<Reference>`, silently discarding any errors.
    let mut branches: Vec<BranchSummary> = git_repo
        .references()
        .map_err(|e| Error::GitCorrupt(e.to_string()))?
        .local_branches()
        .into_iter()
        .flatten()
        .flatten()
        .filter_map(|mut branch| {
            let commit = branch.peel_to_commit().ok()?;
            let date = commit_date(&commit)?;
            let timestamp = commit_timestamp(&commit)?;
            let name = branch.name().shorten().to_string();
            let is_default = head_name
                .as_ref()
                .is_some_and(|h| h.as_ref() == branch.name());

            Some(BranchSummary {
                name,
                date,
                timestamp,
                is_default,
            })
        })
        .collect();

    branches.sort_by(|a, b| {
        b.is_default
            .cmp(&a.is_default)
            .then(b.timestamp.cmp(&a.timestamp))
    });

    Ok(BranchList { repo, branches })
}

/// Renders a paginated log of commits reachable from the tip of the given branch.
pub async fn detail(
    State(state): State<AppState>,
    RepoName(repo): RepoName,
    Path(params): Path<BranchPath>,
    Query(pagination): Query<Pagination>,
) -> Result<impl IntoResponse> {
    let branch = params.branch;
    let page = pagination.page;

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

    let ref_name = format!("refs/heads/{branch}");
    let tip = git_repo
        .find_reference(&ref_name)
        .map_err(|_| Error::NotFound(format!("branch {branch}")))?
        .peel_to_commit()
        .map_err(|e| Error::GitCorrupt(e.to_string()))?;

    let tip_sha = tip.id().to_string();

    // Fetch one extra commit to determine if a next page exists.
    let mut commits: Vec<CommitSummary> = tip
        .id()
        .ancestors()
        .all()
        .map_err(|e| Error::GitCorrupt(e.to_string()))?
        .filter_map(|info| {
            let info = info.ok()?;
            let commit = info.id().object().ok()?.try_into_commit().ok()?;

            let sha = info.id().to_string();
            let message = commit.message().ok()?.summary().to_string();
            let author = commit.author().ok()?.name.to_string();
            let date = commit_date(&commit)?;

            Some(CommitSummary {
                sha,
                message,
                author,
                date,
            })
        })
        .skip(page * PAGE_SIZE)
        .take(PAGE_SIZE + 1)
        .collect();

    // The extra commit was only fetched to detect whether a next page exists.
    // Discard it before rendering.
    let has_next = commits.len() > PAGE_SIZE;
    if has_next {
        commits.pop();
    }

    Ok(BranchDetail {
        repo,
        branch,
        tip_sha,
        commits,
        page,
        has_prev: page > 0,
        has_next,
    })
}
