//! HTTP API of Obnam server.

use std::{
    path::PathBuf,
    sync::{Arc, Mutex, MutexGuard},
};

use axum::{
    Json, Router,
    body::Bytes,
    debug_handler,
    extract::{FromRequestParts, Path, Query, State},
    http::{StatusCode, header::AUTHORIZATION},
    routing::{get, put},
};
use log::{error, info};
use obnam::blobs::{BlobRepository, BlobsError};
use serde::{Deserialize, Serialize};

use crate::{
    config::ServerConfig,
    repo::{Repo, RepoError},
    token::{ObnamTrustedToken, TokenFactory},
};

pub struct HttpApi {
    listen: String,
    api_state: ApiState,
}

impl HttpApi {
    pub async fn serve(&self) -> Result<(), ApiError> {
        let app = Router::new()
            .route("/validate", get(validate))
            .route(
                "/chunks/{id}",
                put(new_chunk).get(get_chunk).delete(delete_chunk),
            )
            .route("/chunks", get(find_chunks))
            .with_state(self.api_state.clone());
        let listener = tokio::net::TcpListener::bind(&self.listen)
            .await
            .map_err(|err| ApiError::Bind(self.listen.clone(), err))?;
        axum::serve(listener, app).await.map_err(ApiError::Serve)?;
        Ok(())
    }
}

impl TryFrom<&ServerConfig> for HttpApi {
    type Error = ApiError;
    fn try_from(config: &ServerConfig) -> Result<Self, Self::Error> {
        Ok(Self {
            listen: config.listen().to_string(),
            api_state: ApiState::try_from(config)?,
        })
    }
}

#[debug_handler]
async fn validate(
    _: State<ApiState>,
    TokenExtractor(token): TokenExtractor,
) -> Json<ObnamTrustedToken> {
    Json(token)
}

#[debug_handler]
async fn new_chunk(
    Path(id): Path<String>,
    query: Query<LabelQuery>,
    state: State<ApiState>,
    TokenExtractor(token): TokenExtractor,
    data: Bytes,
) -> StatusCode {
    if token.create_chunk_allowed() {
        let mut state = state.lock();
        let id = id.to_string();
        match state.repo.put(&id, &query.label, data) {
            Ok(_) => {
                info!("PUT chunk {id} with label {}", query.label);
                StatusCode::CREATED
            }
            Err(err) => {
                error!("failed to store new chunk in server repository: {err}");
                StatusCode::INTERNAL_SERVER_ERROR
            }
        }
    } else {
        error!("PUT not allowed by token {token}");
        StatusCode::UNAUTHORIZED
    }
}

#[debug_handler]
async fn get_chunk(
    Path(id): Path<String>,
    state: State<ApiState>,
    TokenExtractor(token): TokenExtractor,
) -> Result<Vec<u8>, StatusCode> {
    if token.read_chunk_allowed() {
        let state = state.lock();
        let id = id.to_string();
        let res = state.repo.get(&id);
        match res {
            Ok((_, chunk)) => {
                info!("GET chunk {id}");
                Ok(chunk)
            }
            Err(RepoError::NoSuchChunk(_)) => {
                error!("chunk {id} not found");
                Err(StatusCode::NOT_FOUND)
            }
            Err(err) => {
                error!("failed to get chunk {id}: {err}");
                Err(StatusCode::INTERNAL_SERVER_ERROR)
            }
        }
    } else {
        error!("GET not allowed by token {token}");
        Err(StatusCode::UNAUTHORIZED)
    }
}

#[debug_handler]
async fn delete_chunk(
    Path(id): Path<String>,
    state: State<ApiState>,
    TokenExtractor(token): TokenExtractor,
) -> StatusCode {
    if token.delete_allowed() {
        let mut state = state.lock();
        let repo = &mut state.repo;
        match repo.delete(id) {
            Ok(true) => StatusCode::NO_CONTENT,
            Ok(false) => StatusCode::NOT_FOUND,
            Err(_) => StatusCode::INTERNAL_SERVER_ERROR,
        }
    } else {
        StatusCode::UNAUTHORIZED
    }
}

#[debug_handler]
async fn find_chunks(
    query: Query<LabelQuery>,
    state: State<ApiState>,
    TokenExtractor(token): TokenExtractor,
) -> Result<Json<Ids>, StatusCode> {
    if token.find_chunks_allowed() {
        let state = state.lock();
        match state.repo.find(&query.label) {
            Ok(ids) => {
                let ids = Ids {
                    chunk_ids: ids.iter().map(|id| id.to_string()).collect(),
                };
                Ok(Json(ids))
            }
            Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
        }
    } else {
        Err(StatusCode::UNAUTHORIZED)
    }
}

#[derive(Serialize)]
struct Ids {
    chunk_ids: Vec<String>,
}

#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct LabelQuery {
    label: String,
}

struct TokenExtractor(ObnamTrustedToken);

impl FromRequestParts<ApiState> for TokenExtractor {
    type Rejection = (StatusCode, &'static str);
    async fn from_request_parts(
        parts: &mut axum::http::request::Parts,
        state: &ApiState,
    ) -> Result<Self, Self::Rejection> {
        if let Some(authz) = parts.headers.get(AUTHORIZATION)
            && let Ok(authz) = String::from_utf8(authz.as_bytes().to_vec())
            && let Some(untrusted) = authz.strip_prefix("Bearer ")
        {
            let state = state.repo.lock().expect("lock API state");
            if let Ok(trusted) = state.token_factory.validate(untrusted) {
                Ok(TokenExtractor(trusted))
            } else {
                Err((StatusCode::UNAUTHORIZED, "Unauthorized"))
            }
        } else {
            Err((StatusCode::BAD_REQUEST, "`Authorization` header is missing"))
        }
    }
}

struct UnlockedState {
    token_factory: TokenFactory,
    repo: Repo,
}

#[derive(Clone)]
struct ApiState {
    repo: Arc<Mutex<UnlockedState>>,
}

impl ApiState {
    fn lock<'a>(&'a self) -> MutexGuard<'a, UnlockedState> {
        self.repo.lock().expect("lock ApiState")
    }
}

impl TryFrom<&ServerConfig> for ApiState {
    type Error = ApiError;
    fn try_from(config: &ServerConfig) -> Result<Self, Self::Error> {
        let dirname = config.repository().to_path_buf();
        let repo =
            BlobRepository::open(&dirname).map_err(|err| ApiError::OpenRepo(dirname, err))?;
        Ok(Self {
            repo: Arc::new(Mutex::new(UnlockedState {
                token_factory: TokenFactory::try_from(config).map_err(ApiError::TokenFactory)?,
                repo: Repo::new(repo),
            })),
        })
    }
}

#[derive(Debug, thiserror::Error)]
pub enum ApiError {
    #[error("failed to bind to {0:?}")]
    Bind(String, #[source] std::io::Error),

    #[error("failed to serve HTTP")]
    Serve(#[source] std::io::Error),

    #[error("failed to create a token factory from configuration")]
    TokenFactory(#[source] crate::token::TokenFactoryError),

    #[error("not such chunk")]
    NoSuchChunk,

    #[error("failed to open backup repository {0}")]
    OpenRepo(PathBuf, #[source] BlobsError),
}
