//! Git repository helpers used across route handlers.
//!
//! Provides convenience functions for common `gix` operations such as opening a
//! repository, formatting commit dates, validating paths, and iterating
//! references.  The [`GitResultExt`] extension trait lets handlers convert gix
//! errors into [`Error::GitCorrupt`] with a single `.corrupt()` call.

use std::path::Path;

use jiff::Timestamp;
use jiff::tz::{Offset, TimeZone};

use crate::error::{Error, Result as CrateResult};

/// Opens a repository at `root/{name}` or returns [`Error::RepoNotFound`].
pub fn open_repo(root: &Path, name: &str) -> CrateResult<gix::Repository> {
    gix::open(root.join(name)).map_err(|_| Error::RepoNotFound(name.to_owned()))
}

/// Rejects paths containing non-normal [`Component`]s (e.g. `..`) to prevent
/// directory traversal.
pub fn validate_path(path: &str) -> CrateResult<()> {
    if std::path::Path::new(path)
        .components()
        .any(|c| !matches!(c, std::path::Component::Normal(_)))
    {
        return Err(Error::BadRequest("invalid path".to_string()));
    }
    Ok(())
}

/// Returns the commit's author timestamp as Unix seconds.
pub fn commit_timestamp(commit: &gix::Commit) -> Option<i64> {
    commit.time().ok().map(|t| t.seconds)
}

/// Formats a commit's author date as a `YYYY-MM-DD` string in the commit's
/// local timezone, returning `None` if the time cannot be parsed.
pub fn commit_date(commit: &gix::Commit) -> Option<String> {
    let time = commit.time().ok()?;
    format_time(time.seconds, time.offset)
}

/// Formats a commit's committer date as a `YYYY-MM-DD` string in the
/// commit's local timezone, returning `None` if the time cannot be parsed.
pub fn committer_date(commit: &gix::Commit) -> Option<String> {
    let sig = commit.committer().ok()?;
    let time = sig.time().ok()?;
    format_time(time.seconds, time.offset)
}

/// Converts a Unix timestamp + timezone offset into a `YYYY-MM-DD` string.
fn format_time(seconds: i64, offset: i32) -> Option<String> {
    let zone = TimeZone::fixed(Offset::from_seconds(offset).ok()?);
    Timestamp::from_second(seconds)
        .map(|ts| ts.to_zoned(zone).date().to_string())
        .ok()
}

/// Extension trait adding `.corrupt()` to `Result<T, E>` where `E: Display`.
///
/// Converts an error into [`Error::GitCorrupt`] by calling `.to_string()` on
/// the inner error value.  This removes the need for repetitive
/// `.map_err(|e| Error::GitCorrupt(e.to_string()))` chains.
///
/// # Usage
///
/// ```ignore
/// use crate::git::GitResultExt as _;
/// let repo = git_repo.references().corrupt()?;
/// ```
pub trait GitResultExt<T> {
    fn corrupt(self) -> CrateResult<T>;
}

impl<T, E: std::fmt::Display> GitResultExt<T> for std::result::Result<T, E> {
    fn corrupt(self) -> CrateResult<T> {
        self.map_err(|e| Error::GitCorrupt(e.to_string()))
    }
}

/// Silently flattens a `Result<impl IntoIterator<Item = Result<T, E2>>, E1>` into
/// an `Iterator<Item = T>`.
///
/// Both the outer and inner `Result` layers are consumed via `.into_iter()` and
/// `.flatten()`, so errors at either level are silently skipped.  This matches
/// the common git-ref iteration pattern where a single corrupt ref should not
/// crash the page.
pub fn flatten_refs<T, E1, E2, I>(result: std::result::Result<I, E1>) -> impl Iterator<Item = T>
where
    I: IntoIterator<Item = std::result::Result<T, E2>>,
{
    result.into_iter().flatten().flatten()
}
