//! Route handlers for repository pages.
//!
//! Covers two routes: the repository list (`/`) and the repository detail
//! page (`/{repo}`).  The detail handler opens the repository with `gix`,
//! collects branch and tag summaries by peeling each ref to its commit, and
//! reads the description from `.git/description`.  Refs that cannot be read
//! are silently skipped rather than treated as fatal errors.

use askama::Template;
use askama_web::WebTemplate;
use axum::extract::State;
use axum::response::IntoResponse;

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

/// Reads the repository description from `.git/description`, returning `None`
/// if the file is missing, empty, or contains the default git placeholder text.
fn repo_description(git_repo: &gix::Repository) -> Option<String> {
    std::fs::read_to_string(git_repo.path().join("description"))
        .ok()
        .map(|s| s.trim().to_string())
        .filter(|s| !s.is_empty() && !s.starts_with("Unnamed repository"))
}

/// Returns `true` if `url` is a relative path that should be rewritten to
/// point at the raw file serving route.
fn is_relative_url(url: &str) -> bool {
    !url.starts_with("http://")
        && !url.starts_with("https://")
        && !url.starts_with('#')
        && !url.starts_with("mailto:")
        && !url.starts_with("data:")
}

/// Template context for a single branch.
struct BranchSummary {
    name: String,
    date: String,
    timestamp: i64,
    /// Whether this branch is the repository's HEAD.
    is_default: bool,
}

/// Template context for a single tag.
struct TagSummary {
    name: String,
    date: String,
    timestamp: i64,
}

/// Template context for the repository detail page.
#[derive(Template, WebTemplate)]
#[template(path = "repo.html")]
struct Repo {
    name: String,
    description: Option<String>,
    branches: Vec<BranchSummary>,
    tags: Vec<TagSummary>,
    commit_count: usize,
    head_sha: Option<String>,
    default_branch: Option<String>,
    /// README rendered to HTML, or `None` if no README was found.
    readme: Option<String>,
}

/// Renders the detail page for a single repository, including its branches,
/// tags, and description.  Returns [`Error::RepoNotFound`] if no repository
/// exists at the given name, or [`Error::GitCorrupt`] if the ref database
/// cannot be read.
pub async fn detail(
    State(state): State<AppState>,
    RepoName(name): RepoName,
) -> Result<impl IntoResponse> {
    let path = state.root.join(&name);
    let git_repo = gix::open(path).map_err(|_| Error::RepoNotFound(name.clone()))?;

    let platform = git_repo
        .references()
        .map_err(|e| Error::GitCorrupt(e.to_string()))?;

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

    // `.tags()` 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 tags: Vec<TagSummary> = platform
        .tags()
        .into_iter()
        .flatten()
        .flatten()
        .filter_map(|mut tag| {
            let commit = tag.peel_to_commit().ok()?;
            let date = commit_date(&commit)?;
            let timestamp = commit_timestamp(&commit)?;
            let name = tag.name().shorten().to_string();

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

    tags.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));

    // Same double-flatten pattern as tags above.
    let mut branches: Vec<BranchSummary> = platform
        .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))
    });

    let description = repo_description(&git_repo);
    let head_id = git_repo.head_id().ok();
    let commit_count = head_id
        .as_ref()
        .and_then(|id| id.ancestors().all().ok())
        .map(|walk| walk.count())
        .unwrap_or(0);

    // Resolve HEAD to a SHA so we can build stable raw URLs for images.
    let head_sha = head_id.map(|id| id.to_string());

    // Try each candidate filename in order, stopping at the first one found.
    let readme = ["HEAD:README.md", "HEAD:README", "HEAD:readme.md"]
        .iter()
        .find_map(|&spec| {
            // Resolve the rev-spec to a blob, discarding any that don't exist
            // or aren't blobs (e.g. submodules).
            let blob = git_repo
                .rev_parse_single(spec)
                .ok()
                .and_then(|id| id.object().ok())
                .and_then(|obj| obj.try_into_blob().ok())?;

            // Silently skip non-UTF-8 content.
            let text = std::str::from_utf8(&blob.data).ok()?;

            use pulldown_cmark::{Event, Options, Parser, Tag, html};

            // Rewrite relative image URLs to the raw file serving route so
            // that images committed alongside the README render correctly.
            let mut rendered = String::new();
            let events = Parser::new_ext(text, Options::all()).map(|event| match event {
                Event::Start(Tag::Image {
                    link_type,
                    dest_url,
                    title,
                    id,
                }) => {
                    let dest_url = match &head_sha {
                        Some(sha) if is_relative_url(&dest_url) => {
                            format!("/{}/commits/{}/raw/{}", name, sha, dest_url).into()
                        }
                        _ => dest_url,
                    };
                    Event::Start(Tag::Image {
                        link_type,
                        dest_url,
                        title,
                        id,
                    })
                }
                _ => event,
            });
            html::push_html(&mut rendered, events);

            Some(rendered)
        });

    let default_branch = head_name.map(|h| h.shorten().to_string());

    Ok(Repo {
        name,
        description,
        branches,
        tags,
        commit_count,
        head_sha,
        default_branch,
        readme,
    })
}

/// Template context for a single repository in the list.
struct RepoSummary {
    name: String,
    description: Option<String>,
    commit_count: usize,
    last_commit: Option<String>,
}

/// Template context for the repository list page.
#[derive(Template, WebTemplate)]
#[template(path = "repo_list.html")]
struct RepoList {
    repos: Vec<RepoSummary>,
}

/// Renders the repository list page by scanning the root directory for git
/// repositories.  Entries that cannot be opened as a git repository are
/// skipped.  The list is sorted by latest commit first.
pub async fn list(State(state): State<AppState>) -> Result<impl IntoResponse> {
    let mut repos: Vec<RepoSummary> = std::fs::read_dir(&state.root)?
        .filter_map(|entry| {
            let name = entry.ok()?.file_name().to_string_lossy().into_owned();
            let git_repo = gix::open(state.root.join(&name)).ok()?;
            let description = repo_description(&git_repo);

            let head_id = git_repo.head_id().ok();

            let commit_count = head_id
                .as_ref()
                .and_then(|id| id.ancestors().all().ok())
                .map(|walk| walk.count())
                .unwrap_or(0);

            let last_commit = head_id
                .and_then(|id| id.object().ok())
                .and_then(|obj| obj.try_into_commit().ok())
                .and_then(|commit| commit_date(&commit));

            Some(RepoSummary {
                name,
                description,
                commit_count,
                last_commit,
            })
        })
        .collect();

    repos.sort_by(|a, b| b.last_commit.cmp(&a.last_commit));

    Ok(RepoList { repos })
}
