//! Route handlers, shared types, and router construction.
//!
//! This module defines the [`AppState`] injected into every handler, the
//! [`RepoName`] path extractor (which validates against directory traversal),
//! the [`Breadcrumb`] type used by tree/blame pages, and the [`router`]
//! function that wires all 15 routes together.

mod archive;
mod blame;
mod branch;
mod commit;
mod raw;
mod repo;
mod tag;
mod tree;

use std::collections::HashMap;
use std::path::{Component, PathBuf};

use crate::error::{Error, Result};
use axum::Router;
use axum::extract::{FromRequestParts, Path, State};
use axum::http::header;
use axum::http::request::Parts;
use axum::response::IntoResponse;
use axum::routing::get;
use serde::Serialize;

/// A breadcrumb segment for the navigation bar.
#[derive(Serialize)]
pub struct Breadcrumb {
    pub label: String,
    pub url: String,
}

/// Builds breadcrumbs for a tree path.
///
/// The root breadcrumb is always `"tree"`.  Each path segment adds a breadcrumb
/// linking to its corresponding subdirectory.
pub fn build_breadcrumbs(base: &str, repo: &str, sha: &str, path: &str) -> Vec<Breadcrumb> {
    let mut crumbs = Vec::new();
    crumbs.push(Breadcrumb {
        label: "tree".to_string(),
        url: format!("{base}/{repo}/commits/{sha}/tree"),
    });
    if !path.is_empty() {
        let mut accumulated = String::new();
        for segment in path.split('/') {
            if !accumulated.is_empty() {
                accumulated.push('/');
            }
            accumulated.push_str(segment);
            crumbs.push(Breadcrumb {
                label: segment.to_string(),
                url: format!("{base}/{repo}/commits/{sha}/tree/{accumulated}"),
            });
        }
    }
    crumbs
}

/// Axum extractor that pulls the `{repo}` path parameter and validates it
/// contains no path traversal components before passing it to handlers.
pub struct RepoName(pub String);

impl<S: Send + Sync> FromRequestParts<S> for RepoName {
    type Rejection = Error;

    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self> {
        let Path(params) = Path::<HashMap<String, String>>::from_request_parts(parts, state)
            .await
            .map_err(|e| Error::BadRequest(e.to_string()))?;

        let name = params
            .get("repo")
            .ok_or_else(|| Error::BadRequest("missing repo parameter".to_string()))?
            .clone();

        // Reject names containing non-normal components (e.g. `..`) to prevent
        // directory traversal.
        if std::path::Path::new(&name)
            .components()
            .any(|c| !matches!(c, Component::Normal(_)))
        {
            return Err(Error::BadRequest("invalid repository name".to_string()));
        }

        Ok(Self(name))
    }
}

/// Shared application state passed to all route handlers.
#[derive(Clone)]
pub struct AppState {
    /// Path to the directory containing bare git repositories.
    pub root: PathBuf,
    /// Concatenated CSS served at `/style.css`.
    pub css: String,
}

/// Serves the concatenated stylesheet.
async fn style(State(state): State<AppState>) -> impl IntoResponse {
    (
        [(header::CONTENT_TYPE, "text/css; charset=utf-8")],
        state.css,
    )
}

/// Serves the favicon (SVG or ICO bytes loaded at startup).
async fn favicon() -> impl IntoResponse {
    (
        [(header::CONTENT_TYPE, "image/svg+xml")],
        crate::config::favicon(),
    )
}

/// Builds the application router with all routes and shared state attached.
pub fn router(state: AppState) -> Router {
    Router::new()
        .route("/favicon.ico", get(favicon))
        .route("/style.css", get(style))
        .route("/", get(repo::list))
        .route("/{repo}", get(repo::detail))
        .route("/{repo}/tags", get(tag::list))
        .route("/{repo}/tags/{*tag}", get(tag::detail))
        .route("/{repo}/branches", get(branch::list))
        .route("/{repo}/branches/{*branch}", get(branch::detail))
        .route("/{repo}/commits", get(commit::list))
        .route("/{repo}/commits/{sha}", get(commit::detail))
        .route("/{repo}/commits/{sha}/tree", get(tree::handler))
        .route("/{repo}/commits/{sha}/tree/{*path}", get(tree::handler))
        .route("/{repo}/commits/{sha}/blame/{*path}", get(blame::handler))
        .route("/{repo}/commits/{sha}/raw/{*path}", get(raw::handler))
        .route("/{repo}/archive/{format}/{*ref}", get(archive::handler))
        .with_state(state)
}
