// SPDX-License-Identifier: MIT
//! In-process mode: load `libgeyserlite.so` via [`libloading`] and call the
//! @CEntryPoint exports declared in `build/overlay/.../GeyserBridge.java`.
//!
//! A native crash inside `libgeyserlite.so` will kill the host process —
//! Rust's [`std::panic::catch_unwind`] cannot save you. Use
//! [`crate::Mode::Subprocess`] if crash isolation matters.
//!
//! ## Cross-thread caveat (v0.2 skeleton)
//!
//! GraalVM `@CEntryPoint` methods take an `IsolateThread*` that's tied to
//! the OS thread that created it. Calling shutdown() from a different
//! thread than run() requires `geyser_attach_thread` / `_detach_thread`
//! @CEntryPoints we haven't added yet. This skeleton therefore ignores
//! [`crate::Server::stop`] in embedded mode for now and exits when
//! `geyser_run` returns naturally. The full graceful-shutdown story
//! lands in v0.7 along with a working libgeyserlite.so.

use std::ffi::CString;
use std::ffi::c_void;
use std::os::raw::c_char;
use std::sync::atomic::Ordering;

use libloading::{Library, Symbol};
use tempfile::TempDir;
use tracing::info;

use crate::config::{render_config, write_floodgate_key, write_permissions_yml};
use crate::error::{Error, Result};
use crate::locate::locate_library;
use crate::server::Server;

pub(crate) struct EmbeddedRunner;

type IsolateThreadPtr = *mut std::ffi::c_void;

/// Sendable wrapper for an IsolateThread pointer. Safe because the pointer
/// is only used as a token by the native side; no data is dereferenced
/// from the Rust side.
#[derive(Copy, Clone)]
struct ThreadHandle(usize);
unsafe impl Send for ThreadHandle {}
impl ThreadHandle {
    fn ptr(self) -> IsolateThreadPtr {
        self.0 as IsolateThreadPtr
    }
}

// graal_create_isolate(params, &isolate, &thread). We pass NULL params
// (defaults baked into the image at build time, e.g. -R:MaxHeapSize).
// The thread arg is what every other geyser_* call takes first.
type CreateIsolateFn =
    unsafe extern "C" fn(*const c_void, *mut *mut c_void, *mut IsolateThreadPtr) -> i32;
type TearDownFn = unsafe extern "C" fn(IsolateThreadPtr) -> i32;
type InitFn = unsafe extern "C" fn(IsolateThreadPtr, *const c_char) -> i32;
type RunFn = unsafe extern "C" fn(IsolateThreadPtr) -> i32;
type ShutdownFn = unsafe extern "C" fn(IsolateThreadPtr) -> i32;
type StatusFn = unsafe extern "C" fn(IsolateThreadPtr) -> i32;

impl EmbeddedRunner {
    pub async fn run(self, srv: &Server) -> Result<()> {
        let libpath = locate_library(&srv.opts).await?;
        info!(path = %libpath.display(), "loading libgeyserlite");

        // SAFETY: dlopen + dlsym; we trust the C ABI matches libgeyserlite.h
        // (auto-generated by GraalVM from the same @CEntryPoint methods).
        // Signature drift is caught by the v0.6 build smoke test.
        let lib = unsafe { Library::new(&libpath)? };
        // graal_create_isolate / graal_tear_down_isolate come from the
        // GraalVM runtime that's linked into every shared library — no
        // project-specific @CEntryPoint needed. The geyser_* methods all
        // run inside the isolate this thread anchors.
        let create_isolate: Symbol<CreateIsolateFn> = unsafe { lib.get(b"graal_create_isolate")? };
        let tear_down: Symbol<TearDownFn> = unsafe { lib.get(b"graal_tear_down_isolate")? };
        let init: Symbol<InitFn> = unsafe { lib.get(b"geyser_init")? };
        let run: Symbol<RunFn> = unsafe { lib.get(b"geyser_run")? };
        let _shutdown: Symbol<ShutdownFn> = unsafe { lib.get(b"geyser_shutdown")? };
        let _status: Symbol<StatusFn> = unsafe { lib.get(b"geyser_status")? };

        let workdir = TempDir::new()?;
        write_floodgate_key(workdir.path(), &srv.opts)?;
        render_config(workdir.path(), &srv.opts)?;
        write_permissions_yml(workdir.path())?;

        // Geyser uses cwd-relative file lookups. Chdir before init.
        let prev_cwd = std::env::current_dir()?;
        std::env::set_current_dir(workdir.path())?;
        let _restore_cwd = CdGuard(Some(prev_cwd));

        let config_path = workdir.path().join("config.yml");
        let cstr = CString::new(config_path.to_string_lossy().as_bytes())
            .map_err(|e| Error::Io(std::io::Error::other(e)))?;

        let create_ptr = *create_isolate;
        let init_ptr = *init;
        let run_ptr = *run;
        let tear_down_ptr = *tear_down;
        let healthy = srv.healthy.clone();

        // Run the entire lifecycle on a dedicated blocking thread so all
        // @CEntryPoint calls land on the same OS thread (a GraalVM
        // requirement — IsolateThread* is thread-affine).
        let join = tokio::task::spawn_blocking(move || -> Result<()> {
            let mut isolate: *mut c_void = std::ptr::null_mut();
            let mut thread: IsolateThreadPtr = std::ptr::null_mut();
            let rc = unsafe { create_ptr(std::ptr::null(), &mut isolate, &mut thread) };
            if rc != 0 {
                return Err(Error::NativeCall {
                    call: "graal_create_isolate",
                    rc,
                });
            }
            let handle = ThreadHandle(thread as usize);

            let rc = unsafe { init_ptr(handle.ptr(), cstr.as_ptr()) };
            if rc != 0 {
                unsafe { tear_down_ptr(handle.ptr()) };
                return Err(Error::NativeCall {
                    call: "geyser_init",
                    rc,
                });
            }

            // Best-effort liveness signal: we know the bedrock listener is
            // bound after init returns. Real status polling lands when we
            // have geyser_attach_thread @CEntryPoint.
            healthy.store(true, Ordering::Relaxed);

            let rc = unsafe { run_ptr(handle.ptr()) };
            healthy.store(false, Ordering::Relaxed);

            unsafe { tear_down_ptr(handle.ptr()) };
            if rc != 0 {
                Err(Error::NativeCall {
                    call: "geyser_run",
                    rc,
                })
            } else {
                Ok(())
            }
        });

        // Wait for either the run thread to return or the user to cancel.
        // In v0.2 we don't actually signal shutdown to the native side
        // (see module-level docstring); we just wait for the thread to
        // notice on its own. Subprocess mode is the right answer for now
        // if you need responsive Stop().
        tokio::select! {
            res = join => res
                .map_err(|e| Error::Io(std::io::Error::other(e)))?,
            _ = srv.cancel.cancelled() => Ok(()),
        }
    }
}

/// Restore cwd when dropped. Avoids pulling in `scopeguard` for one use.
struct CdGuard(Option<std::path::PathBuf>);
impl Drop for CdGuard {
    fn drop(&mut self) {
        if let Some(p) = self.0.take() {
            let _ = std::env::set_current_dir(p);
        }
    }
}
