//! Application error types and HTTP response rendering.
//!
//! Defines the unified [`Error`] enum used across all handlers and the
//! [`Result`] type alias.  `Error` implements [`IntoResponse`] via askama's
//! `error.html` template.

use std::sync::{Arc, OnceLock};

use askama::Template;
use axum::http::{StatusCode, header};
use axum::response::{IntoResponse, Response};
use thiserror::Error;

use crate::config::SiteConfig;

/// Unified error type for the application.
///
/// Each variant maps to an HTTP status code and a rendered error page.
#[derive(Debug, Error)]
pub enum Error {
    #[error(transparent)]
    Io(#[from] std::io::Error),
    #[error("repository not found: {0}")]
    RepoNotFound(String),
    #[error("not found: {0}")]
    NotFound(String),
    #[error("bad request: {0}")]
    BadRequest(String),
    #[error("corrupt object: {0}")]
    GitCorrupt(String),
}

/// Convenience alias for `std::result::Result<T, Error>`.
pub type Result<T> = std::result::Result<T, Error>;

/// Module-level [`SiteConfig`] used only by the error template renderer.
///
/// Set once during startup in [`init_site`].  This exists because
/// [`Error::into_response`] does not have access to [`axum::extract::State`]
/// — it runs after the handler has returned.  All other code paths read from
/// [`AppState`](crate::routes::AppState).
static SITE: OnceLock<Arc<SiteConfig>> = OnceLock::new();

/// Stores the [`SiteConfig`] for use by the error template.
///
/// Called once from `main` before the server starts.
pub fn init_site(site: Arc<SiteConfig>) {
    let _ = SITE.set(site);
}

fn site() -> Arc<SiteConfig> {
    SITE.get()
        .cloned()
        .unwrap_or_else(|| {
            Arc::new(SiteConfig {
                site_name: crate::PKG_NAME.to_owned(),
                hostname: String::new(),
                ssh_prefix: String::new(),
                base_url: String::new(),
                favicon: Vec::new(),
                favicon_mime: "image/svg+xml",
            })
        })
}

#[derive(Template)]
#[template(path = "error.html")]
struct ErrorTemplate {
    status: u16,
    kind: &'static str,
    message: String,
    site: Arc<SiteConfig>,
}

impl IntoResponse for Error {
    fn into_response(self) -> Response {
        let is_5xx: bool;
        let (status, kind) = match &self {
            Self::RepoNotFound(_) | Self::NotFound(_) => {
                is_5xx = false;
                (StatusCode::NOT_FOUND, "Not Found")
            }
            Self::BadRequest(_) => {
                is_5xx = false;
                (StatusCode::BAD_REQUEST, "Bad Request")
            }
            Self::Io(_) | Self::GitCorrupt(_) => {
                is_5xx = true;
                tracing::error!(error = %self, "internal error");
                (StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error")
            }
        };

        let message = if is_5xx {
            "An internal error occurred.".into()
        } else {
            self.to_string()
        };

        let tpl = ErrorTemplate {
            status: status.as_u16(),
            kind,
            message,
            site: site(),
        };

        tpl.render().map_or_else(
            |_| {
                let fallback = if is_5xx {
                    "Internal Server Error".into()
                } else {
                    self.to_string()
                };
                (status, fallback).into_response()
            },
            |html| {
                (
                    status,
                    [(header::CONTENT_TYPE, "text/html; charset=utf-8")],
                    html,
                )
                    .into_response()
            },
        )
    }
}
