//! Route handlers for tag pages.
//!
//! Covers two routes: the tag list (`/{repo}/tags`) and the tag detail page
//! (`/{repo}/tags/{*tag}`).  The detail handler peels the tag ref to its
//! commit and presents a release-oriented view with the annotation message,
//! commit info, and archive download links.

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

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

#[derive(Deserialize)]
pub(super) struct TagPath {
    tag: String,
}

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

/// Template context for the tag list page.
#[derive(Template, WebTemplate)]
#[template(path = "tag_list.html")]
struct TagList {
    repo: String,
    tags: Vec<TagSummary>,
}

/// Template context for the tag detail page.
#[derive(Template, WebTemplate)]
#[template(path = "tag_detail.html")]
struct TagDetail {
    repo: String,
    tag: String,
    /// Tagger name and date for annotated tags; `None` for lightweight tags.
    tagger: Option<String>,
    tag_date: Option<String>,
    /// Annotation message for annotated tags; `None` for lightweight tags.
    message: Option<String>,
    commit_sha: String,
    commit_author: String,
    commit_date: String,
}

/// Renders the full list of tags for a repository, sorted newest first.
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()))?;

    // `.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> = git_repo
        .references()
        .map_err(|e| Error::GitCorrupt(e.to_string()))?
        .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));

    Ok(TagList { repo, tags })
}

/// Renders the detail page for a single tag.
pub async fn detail(
    State(state): State<AppState>,
    RepoName(repo): RepoName,
    Path(params): Path<TagPath>,
) -> Result<impl IntoResponse> {
    let tag_name = params.tag;

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

    let ref_name = format!("refs/tags/{tag_name}");
    let mut reference = git_repo
        .find_reference(&ref_name)
        .map_err(|_| Error::NotFound(format!("tag {tag_name}")))?;

    // Extract annotation info if this is an annotated tag.
    let (tagger, tag_date, message) = match reference.peel_to_tag().ok() {
        None => (None, None, None),
        Some(t) => match t.decode() {
            Err(_) => (None, None, None),
            Ok(decoded) => {
                let sig = decoded.tagger().ok().flatten();
                let tagger = sig.as_ref().map(|s| s.name.to_str_lossy().into_owned());
                let tag_date = sig.as_ref().and_then(|s| {
                    // Parse the raw "seconds offset" time string.
                    let mut parts = s.time.splitn(2, ' ');
                    let seconds: i64 = parts.next()?.parse().ok()?;
                    let offset_str = parts.next().unwrap_or("+0000");
                    let sign = if offset_str.starts_with('-') {
                        -1i32
                    } else {
                        1
                    };
                    let hhmm = offset_str.trim_start_matches(['+', '-']);
                    let h: i32 = hhmm.get(..2)?.parse().ok()?;
                    let m: i32 = hhmm.get(2..4)?.parse().ok()?;
                    let offset_secs = sign * (h * 3600 + m * 60);
                    let zone = jiff::tz::TimeZone::fixed(
                        jiff::tz::Offset::from_seconds(offset_secs).ok()?,
                    );
                    jiff::Timestamp::from_second(seconds)
                        .map(|ts| ts.to_zoned(zone).date().to_string())
                        .ok()
                });
                let message = decoded.message.to_str_lossy().trim().to_string();
                let message = if message.is_empty() {
                    None
                } else {
                    Some(message)
                };
                (tagger, tag_date, message)
            }
        },
    };

    let commit = reference
        .peel_to_commit()
        .map_err(|e| Error::GitCorrupt(e.to_string()))?;

    let commit_sha = commit.id().to_string();
    let commit_author = commit
        .author()
        .map(|a| a.name.to_string())
        .unwrap_or_default();
    let commit_date = commit_date(&commit).unwrap_or_default();

    Ok(TagDetail {
        repo,
        tag: tag_name,
        tagger,
        tag_date,
        message,
        commit_sha,
        commit_author,
        commit_date,
    })
}
