// Copyright © 2019 Intel Corporation
//
// SPDX-License-Identifier: Apache-2.0
//

use std::collections::BTreeMap;
use std::error::Error;
use std::fs::{File, OpenOptions};
use std::os::unix::io::{IntoRawFd, RawFd};
use std::os::unix::net::UnixListener;
use std::panic::AssertUnwindSafe;
use std::path::{Path, PathBuf};
use std::sync::LazyLock;
use std::sync::mpsc::Sender;
use std::{fs, panic, result, thread};

use block::fcntl::{LockError, LockGranularity, LockType, try_acquire_lock};
use log::{error, info};
use micro_http::{
    Body, HttpServer, MediaType, Method, Request, Response, ServerError, StatusCode, Version,
};
use seccompiler::{SeccompAction, apply_filter};
use serde_json::Error as SerdeError;
use thiserror::Error;
use vmm_sys_util::eventfd::EventFd;

use self::http_endpoint::{VmActionHandler, VmCreate, VmInfo, VmmPing, VmmShutdown};
#[cfg(all(target_arch = "x86_64", feature = "guest_debug"))]
use crate::api::VmCoredump;
use crate::api::{
    AddDisk, ApiError, ApiRequest, VmAddDevice, VmAddFs, VmAddGenericVhostUser, VmAddNet,
    VmAddPmem, VmAddUserDevice, VmAddVdpa, VmAddVsock, VmBoot, VmCounters, VmDelete, VmNmi,
    VmPause, VmPowerButton, VmReboot, VmReceiveMigration, VmRemoveDevice, VmResize, VmResizeDisk,
    VmResizeZone, VmRestore, VmResume, VmSendMigration, VmShutdown, VmSnapshot,
};
use crate::landlock::Landlock;
use crate::seccomp_filters::{Thread, get_seccomp_filter};
use crate::util::{error_chain_messages, flatten_error_chain_to_string};
use crate::vm::Error as VmError;
use crate::{Error as VmmError, Result};

pub mod http_endpoint;

pub type HttpApiHandle = (thread::JoinHandle<Result<()>>, EventFd);

/// Errors associated with VMM management
#[derive(Error, Debug)]
pub enum HttpError {
    /// API request receive error
    #[error("Failed to deserialize JSON")]
    SerdeJsonDeserialize(#[from] SerdeError),

    /// Attempt to access unsupported HTTP method
    #[error("Bad Request")]
    BadRequest,

    /// Undefined endpoints
    #[error("Not Found")]
    NotFound,

    /// Too many requests
    #[error("Too Many Requests")]
    TooManyRequests,

    /// Internal Server Error
    #[error("Internal Server Error")]
    InternalServerError,

    /// Error from internal API
    #[error("Error from API")]
    ApiError(#[source] ApiError),
}

impl HttpError {
    /// Returns the HTTP status code that best matches this error.
    fn status_code(&self) -> StatusCode {
        match self {
            HttpError::SerdeJsonDeserialize(_) | HttpError::BadRequest => StatusCode::BadRequest,
            HttpError::NotFound => StatusCode::NotFound,
            HttpError::TooManyRequests => StatusCode::TooManyRequests,
            HttpError::InternalServerError => StatusCode::InternalServerError,
            HttpError::ApiError(e) => api_error_status_code(e),
        }
    }
}

/// Maps an [`ApiError`] to an HTTP [`StatusCode`].
fn api_error_status_code(error: &ApiError) -> StatusCode {
    match error.source().and_then(|e| e.downcast_ref::<VmError>()) {
        Some(VmError::VmNotCreated | VmError::VmMissingConfig) => StatusCode::NotFound,
        _ => StatusCode::InternalServerError,
    }
}

const HTTP_ROOT: &str = "/api/v1";

/// Creates the error response's JSON body meant to be sent back to an API client.
///
/// The error message contained in the response is supposed to be user-facing,
/// thus insightful and helpful while balancing technical accuracy and
/// simplicity.
#[expect(clippy::needless_pass_by_value)]
pub fn error_response(error: HttpError) -> Response {
    let mut response = Response::new(Version::Http11, error.status_code());

    info!(
        "HTTP API error response: {}",
        flatten_error_chain_to_string(&error)
    );

    // TODO: Move `api` module from `vmm` to dedicated crate and use a common type definition
    let json = serde_json::to_string(&error_chain_messages(&error)).unwrap();

    let body = Body::new(json);
    response.set_body(body);

    response
}

/// An HTTP endpoint handler interface
pub trait EndpointHandler {
    /// Handles an HTTP request.
    /// After parsing the request, the handler could decide to send an
    /// associated API request down to the VMM API server to e.g. create
    /// or start a VM. The request will block waiting for an answer from the
    /// API server and translate that into an HTTP response.
    fn handle_request(
        &self,
        req: &Request,
        api_notifier: EventFd,
        api_sender: Sender<ApiRequest>,
    ) -> Response {
        // Cloning the files here is very important as it dup() the file
        // descriptors, leaving open the one that was received. This way,
        // rebooting the VM will work since the VM will be created from the
        // original file descriptors.
        let files = req.files.iter().map(|f| f.try_clone().unwrap()).collect();
        let res = match req.method() {
            Method::Put => self.put_handler(api_notifier, api_sender, &req.body, files),
            Method::Get => self.get_handler(api_notifier, api_sender, &req.body),
            _ => return Response::new(Version::Http11, StatusCode::BadRequest),
        };

        match res {
            Ok(response_body) => {
                if let Some(body) = response_body {
                    let mut response = Response::new(Version::Http11, StatusCode::OK);
                    response.set_body(body);
                    response
                } else {
                    Response::new(Version::Http11, StatusCode::NoContent)
                }
            }
            Err(e) => error_response(e),
        }
    }

    fn put_handler(
        &self,
        _api_notifier: EventFd,
        _api_sender: Sender<ApiRequest>,
        _body: &Option<Body>,
        _files: Vec<File>,
    ) -> result::Result<Option<Body>, HttpError> {
        Err(HttpError::BadRequest)
    }

    fn get_handler(
        &self,
        _api_notifier: EventFd,
        _api_sender: Sender<ApiRequest>,
        _body: &Option<Body>,
    ) -> result::Result<Option<Body>, HttpError> {
        Err(HttpError::BadRequest)
    }
}

/// An HTTP routes structure.
pub struct HttpRoutes {
    /// routes is a hash table mapping endpoint URIs to their endpoint handlers.
    pub routes: BTreeMap<String, Box<dyn EndpointHandler + Sync + Send>>,
}

macro_rules! endpoint {
    ($path:expr) => {
        format!("{}{}", HTTP_ROOT, $path)
    };
}

/// HTTP_ROUTES contain all the cloud-hypervisor HTTP routes.
pub static HTTP_ROUTES: LazyLock<HttpRoutes> = LazyLock::new(|| {
    let mut r = HttpRoutes {
        routes: BTreeMap::new(),
    };

    r.routes.insert(
        endpoint!("/vm.add-device"),
        Box::new(VmActionHandler::new(&VmAddDevice)),
    );
    r.routes.insert(
        endpoint!("/vm.add-user-device"),
        Box::new(VmActionHandler::new(&VmAddUserDevice)),
    );
    r.routes.insert(
        endpoint!("/vm.add-disk"),
        Box::new(VmActionHandler::new(&AddDisk)),
    );
    r.routes.insert(
        endpoint!("/vm.add-fs"),
        Box::new(VmActionHandler::new(&VmAddFs)),
    );
    r.routes.insert(
        endpoint!("/vm.add-generic-vhost-user"),
        Box::new(VmActionHandler::new(&VmAddGenericVhostUser)),
    );
    r.routes.insert(
        endpoint!("/vm.add-net"),
        Box::new(VmActionHandler::new(&VmAddNet)),
    );
    r.routes.insert(
        endpoint!("/vm.add-pmem"),
        Box::new(VmActionHandler::new(&VmAddPmem)),
    );
    r.routes.insert(
        endpoint!("/vm.add-vdpa"),
        Box::new(VmActionHandler::new(&VmAddVdpa)),
    );
    r.routes.insert(
        endpoint!("/vm.add-vsock"),
        Box::new(VmActionHandler::new(&VmAddVsock)),
    );
    r.routes.insert(
        endpoint!("/vm.boot"),
        Box::new(VmActionHandler::new(&VmBoot)),
    );
    r.routes.insert(
        endpoint!("/vm.counters"),
        Box::new(VmActionHandler::new(&VmCounters)),
    );
    r.routes
        .insert(endpoint!("/vm.create"), Box::new(VmCreate {}));
    r.routes.insert(
        endpoint!("/vm.delete"),
        Box::new(VmActionHandler::new(&VmDelete)),
    );
    r.routes.insert(endpoint!("/vm.info"), Box::new(VmInfo {}));
    r.routes.insert(
        endpoint!("/vm.pause"),
        Box::new(VmActionHandler::new(&VmPause)),
    );
    r.routes.insert(
        endpoint!("/vm.power-button"),
        Box::new(VmActionHandler::new(&VmPowerButton)),
    );
    r.routes.insert(
        endpoint!("/vm.reboot"),
        Box::new(VmActionHandler::new(&VmReboot)),
    );
    r.routes.insert(
        endpoint!("/vm.receive-migration"),
        Box::new(VmActionHandler::new(&VmReceiveMigration)),
    );
    r.routes.insert(
        endpoint!("/vm.remove-device"),
        Box::new(VmActionHandler::new(&VmRemoveDevice)),
    );
    r.routes.insert(
        endpoint!("/vm.resize"),
        Box::new(VmActionHandler::new(&VmResize)),
    );
    r.routes.insert(
        endpoint!("/vm.resize-disk"),
        Box::new(VmActionHandler::new(&VmResizeDisk)),
    );
    r.routes.insert(
        endpoint!("/vm.resize-zone"),
        Box::new(VmActionHandler::new(&VmResizeZone)),
    );
    r.routes.insert(
        endpoint!("/vm.restore"),
        Box::new(VmActionHandler::new(&VmRestore)),
    );
    r.routes.insert(
        endpoint!("/vm.resume"),
        Box::new(VmActionHandler::new(&VmResume)),
    );
    r.routes.insert(
        endpoint!("/vm.send-migration"),
        Box::new(VmActionHandler::new(&VmSendMigration)),
    );
    r.routes.insert(
        endpoint!("/vm.shutdown"),
        Box::new(VmActionHandler::new(&VmShutdown)),
    );
    r.routes.insert(
        endpoint!("/vm.snapshot"),
        Box::new(VmActionHandler::new(&VmSnapshot)),
    );
    #[cfg(all(target_arch = "x86_64", feature = "guest_debug"))]
    r.routes.insert(
        endpoint!("/vm.coredump"),
        Box::new(VmActionHandler::new(&VmCoredump)),
    );
    r.routes
        .insert(endpoint!("/vmm.ping"), Box::new(VmmPing {}));
    r.routes
        .insert(endpoint!("/vmm.shutdown"), Box::new(VmmShutdown {}));
    r.routes
        .insert(endpoint!("/vm.nmi"), Box::new(VmActionHandler::new(&VmNmi)));

    r
});

fn handle_http_request(
    request: &Request,
    api_notifier: &EventFd,
    api_sender: &Sender<ApiRequest>,
) -> Response {
    let path = request.uri().get_abs_path().to_string();
    let mut response = match HTTP_ROUTES.routes.get(&path) {
        Some(route) => match api_notifier.try_clone() {
            Ok(notifier) => route.handle_request(request, notifier, api_sender.clone()),
            Err(_) => error_response(HttpError::InternalServerError),
        },
        None => error_response(HttpError::NotFound),
    };

    response.set_server("Cloud Hypervisor API");
    response.set_content_type(MediaType::ApplicationJson);
    response
}

fn start_http_thread(
    mut server: HttpServer,
    api_notifier: EventFd,
    api_sender: Sender<ApiRequest>,
    seccomp_action: &SeccompAction,
    exit_evt: EventFd,
    landlock_enable: bool,
    api_socket_lock: Option<File>,
) -> Result<HttpApiHandle> {
    // Retrieve seccomp filter for API thread
    let api_seccomp_filter = get_seccomp_filter(seccomp_action, Thread::HttpApi, None)
        .map_err(VmmError::CreateSeccompFilter)?;

    let api_shutdown_fd = EventFd::new(libc::EFD_NONBLOCK).map_err(VmmError::EventFdCreate)?;
    let api_shutdown_fd_clone = api_shutdown_fd.try_clone().unwrap();

    server
        .add_kill_switch(api_shutdown_fd_clone)
        .map_err(VmmError::CreateApiServer)?;

    let thread = thread::Builder::new()
        .name("http-server".to_string())
        .spawn(move || {
            // Keep the API socket lock (if any) alive for the server thread.
            let _api_socket_lock = api_socket_lock;

            // Apply seccomp filter for API thread.
            if !api_seccomp_filter.is_empty() {
                apply_filter(&api_seccomp_filter)
                    .map_err(VmmError::ApplySeccompFilter)
                    .map_err(|e| {
                        error!("Error applying seccomp filter: {e:?}");
                        exit_evt.write(1).ok();
                        e
                    })?;
            }

            if landlock_enable {
                Landlock::new()
                    .map_err(VmmError::CreateLandlock)?
                    .restrict_self()
                    .map_err(VmmError::ApplyLandlock)
                    .map_err(|e| {
                        error!("Error applying landlock to http-server thread: {e:?}");
                        exit_evt.write(1).ok();
                        e
                    })?;
            }

            panic::catch_unwind(AssertUnwindSafe(move || {
                server.start_server().unwrap();
                loop {
                    match server.requests() {
                        Ok(request_vec) => {
                            for server_request in request_vec {
                                if let Err(e) = server.respond(server_request.process(|request| {
                                    handle_http_request(request, &api_notifier, &api_sender)
                                })) {
                                    error!("HTTP server error on response: {e}");
                                }
                            }
                        }
                        Err(ServerError::ShutdownEvent) => {
                            server.flush_outgoing_writes();
                            return;
                        }
                        Err(e) => {
                            error!("HTTP server error on retrieving incoming request. Error: {e}");
                        }
                    }
                }
            }))
            .map_err(|_| {
                error!("http-server thread panicked");
                exit_evt.write(1).ok()
            })
            .ok();

            Ok(())
        })
        .map_err(VmmError::HttpThreadSpawn)?;

    Ok((thread, api_shutdown_fd))
}

/// Acquires an exclusive lock for the socket path.
///
/// This prevents opening (and potentially deleting) a socket that is in active
/// use by another Cloud Hypervisor instance.
fn acquire_api_socket_lock(socket_path: &Path) -> Result<File> {
    let mut lock_path = socket_path.to_path_buf().into_os_string();
    lock_path.push(".lock");

    let lock = OpenOptions::new()
        .create(true)
        .truncate(false)
        .write(true)
        .open(&lock_path)
        .map_err(VmmError::CreateApiServerSocket)?;

    match try_acquire_lock(&lock, LockType::Write, LockGranularity::WholeFile) {
        Ok(()) => Ok(lock),
        Err(LockError::AlreadyLocked) => Err(VmmError::ApiSocketInUse(socket_path.to_path_buf())),
        Err(LockError::Io(e)) => Err(VmmError::CreateApiServerSocket(e)),
    }
}

pub fn start_http_path_thread(
    path: &str,
    api_notifier: EventFd,
    api_sender: Sender<ApiRequest>,
    seccomp_action: &SeccompAction,
    exit_evt: EventFd,
    landlock_enable: bool,
) -> Result<HttpApiHandle> {
    let socket_path = PathBuf::from(path);

    let lock = acquire_api_socket_lock(&socket_path)?;
    // We hold the lock, so any socket at this path is stale from a crashed
    // run: remove it before bind. Ignore errors (it is usually not present).
    let _ = fs::remove_file(&socket_path);

    let socket_fd = UnixListener::bind(socket_path).map_err(VmmError::CreateApiServerSocket)?;
    // SAFETY: Valid FD just opened
    let server = unsafe { HttpServer::new_from_fd(socket_fd.into_raw_fd()) }
        .map_err(VmmError::CreateApiServer)?;

    start_http_thread(
        server,
        api_notifier,
        api_sender,
        seccomp_action,
        exit_evt,
        landlock_enable,
        Some(lock),
    )
}

pub fn start_http_fd_thread(
    fd: RawFd,
    api_notifier: EventFd,
    api_sender: Sender<ApiRequest>,
    seccomp_action: &SeccompAction,
    exit_evt: EventFd,
    landlock_enable: bool,
) -> Result<HttpApiHandle> {
    // SAFETY: Valid FD
    let server = unsafe { HttpServer::new_from_fd(fd) }.map_err(VmmError::CreateApiServer)?;
    start_http_thread(
        server,
        api_notifier,
        api_sender,
        seccomp_action,
        exit_evt,
        landlock_enable,
        None,
    )
}

pub fn http_api_graceful_shutdown(http_handle: HttpApiHandle) -> Result<()> {
    let (api_thread, api_shutdown_fd) = http_handle;

    api_shutdown_fd.write(1).unwrap();
    api_thread.join().map_err(VmmError::ThreadCleanup)?
}
