//! Dynamic plugin loading and ABI host callback bridge.

use std::path::{Path, PathBuf};

use libloading::Library;

use crate::{
    ErrorCategory, ErrorCode, FilterDescriptor, FilterRegistry, LogLevel, Logger, MetadataKind,
    PIXELFLOW_ABI_VERSION, PIXELFLOW_PLUGIN_ENTRY_SYMBOL, PixelFlowError,
    PixelflowFilterDescriptorV1, PixelflowHostApiV1, PixelflowMetadataKind, PixelflowPluginApiV1,
    PixelflowPluginEntryV1, PixelflowRegistrar, PixelflowStatus, PixelflowStringView, Result,
};

/// Describes one loaded plugin.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LoadedPlugin {
    name: String,
    path: PathBuf,
    abi_version: u32,
}

impl LoadedPlugin {
    /// Returns plugin name reported by ABI table.
    #[must_use]
    pub fn name(&self) -> &str {
        &self.name
    }

    /// Returns dynamic library path.
    #[must_use]
    pub fn path(&self) -> &Path {
        &self.path
    }

    /// Returns plugin ABI version reported by callback table.
    #[must_use]
    pub const fn abi_version(&self) -> u32 {
        self.abi_version
    }
}

struct RegistrarBridge {
    registry: *mut FilterRegistry,
    logger: Logger,
}

/// Returns conventional platform plugin directories.
#[must_use]
pub fn platform_plugin_directories() -> Vec<PathBuf> {
    let mut directories = Vec::new();

    #[cfg(target_os = "linux")]
    {
        directories.push(PathBuf::from("/usr/lib/pixelflow/plugins"));
        if let Some(home) = std::env::var_os("HOME") {
            directories.push(PathBuf::from(home).join(".local/lib/pixelflow/plugins"));
        }
    }

    #[cfg(target_os = "macos")]
    {
        directories.push(PathBuf::from(
            "/Library/Application Support/pixelflow/plugins",
        ));
        if let Some(home) = std::env::var_os("HOME") {
            directories
                .push(PathBuf::from(home).join("Library/Application Support/pixelflow/plugins"));
        }
    }

    #[cfg(target_os = "windows")]
    {
        if let Some(appdata) = std::env::var_os("APPDATA") {
            directories.push(PathBuf::from(appdata).join("pixelflow/plugins"));
        }
        if let Some(programdata) = std::env::var_os("PROGRAMDATA") {
            directories.push(PathBuf::from(programdata).join("pixelflow/plugins"));
        }
    }

    directories
}

/// Returns true when path has current platform dynamic library extension.
#[must_use]
pub fn is_dynamic_library(path: &Path) -> bool {
    let Some(extension) = path.extension().and_then(|value| value.to_str()) else {
        return false;
    };

    #[cfg(target_os = "linux")]
    {
        extension == "so"
    }

    #[cfg(target_os = "macos")]
    {
        extension == "dylib"
    }

    #[cfg(target_os = "windows")]
    {
        extension == "dll"
    }

    #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
    {
        false
    }
}

/// Loads all plugins from configured directories, warning and skipping failures.
pub fn load_plugins_from_directories(
    directories: &[PathBuf],
    registry: &mut FilterRegistry,
    logger: &Logger,
) -> Vec<LoadedPlugin> {
    let mut loaded = Vec::new();

    for directory in directories {
        let Ok(entries) = std::fs::read_dir(directory) else {
            continue;
        };

        for entry in entries.flatten() {
            let path = entry.path();
            if !is_dynamic_library(&path) {
                continue;
            }

            match load_plugin(&path, registry, logger) {
                Ok(plugin) => loaded.push(plugin),
                Err(error) => logger.log(
                    LogLevel::Warn,
                    "pixelflow_core::plugin_host",
                    format!("skipping plugin '{}': {error}", path.display()),
                ),
            }
        }
    }

    loaded
}

fn load_plugin(
    path: &Path,
    registry: &mut FilterRegistry,
    logger: &Logger,
) -> Result<LoadedPlugin> {
    // SAFETY: Host intentionally loads operator-selected plugin path and keeps handle alive after
    // successful registration so any resolved symbols do not outlive library storage.
    let library = unsafe { Library::new(path) }.map_err(|error| {
        PixelFlowError::new(
            ErrorCategory::Plugin,
            ErrorCode::new("plugin.load_failed"),
            format!(
                "failed to load plugin library '{}': {error}",
                path.display()
            ),
        )
    })?;

    // SAFETY: Symbol lookup happens against still-live library handle and returned function pointer
    // remains valid because loaded library is leaked after successful registration.
    let entry = unsafe { library.get::<PixelflowPluginEntryV1>(PIXELFLOW_PLUGIN_ENTRY_SYMBOL) }
        .map_err(|error| {
            PixelFlowError::new(
                ErrorCategory::Plugin,
                ErrorCode::new("plugin.entry_symbol_missing"),
                format!(
                    "failed to load plugin entry symbol from '{}': {error}",
                    path.display()
                ),
            )
        })?;

    let host_api = PixelflowHostApiV1 {
        size: std::mem::size_of::<PixelflowHostApiV1>() as u32,
        version: PIXELFLOW_ABI_VERSION,
        register_filter: register_filter_callback,
        register_metadata_key: register_metadata_key_callback,
        log: log_callback,
        reserved: [0; 4],
    };
    let mut plugin_api = PixelflowPluginApiV1 {
        size: 0,
        version: 0,
        plugin_name: placeholder_plugin_name,
        register: placeholder_register,
        reserved: [0; 5],
    };

    // SAFETY: `entry` came from plugin's ABI symbol, and both API structs are valid writable stack
    // allocations for call duration.
    status_to_result(unsafe { entry(&host_api, &mut plugin_api) })?;
    validate_plugin_api(&plugin_api)?;

    // SAFETY: `validate_plugin_api` ensures callback table is initialized before invoking plugin
    // name accessor, and returned view is copied immediately into owned `String`.
    let name = string_view_to_string(unsafe { (plugin_api.plugin_name)() })?;
    let mut bridge = RegistrarBridge {
        registry: registry as *mut FilterRegistry,
        logger: logger.clone(),
    };

    // SAFETY: `validate_plugin_api` ensured register callback exists; host API and registrar bridge
    // pointers reference live stack data for full callback duration.
    status_to_result(unsafe {
        (plugin_api.register)(&host_api, (&mut bridge as *mut RegistrarBridge).cast())
    })?;

    let _library = Box::leak(Box::new(library));

    Ok(LoadedPlugin {
        name,
        path: path.to_path_buf(),
        abi_version: plugin_api.version,
    })
}

const unsafe extern "C" fn placeholder_plugin_name() -> PixelflowStringView {
    PixelflowStringView::from_rust_str("")
}

const unsafe extern "C" fn placeholder_register(
    _host: *const PixelflowHostApiV1,
    _registrar: *mut PixelflowRegistrar,
) -> PixelflowStatus {
    PixelflowStatus::plugin_error(
        "plugin.uninitialized_api",
        "plugin entry did not initialize registration callback",
    )
}

unsafe extern "C" fn register_filter_callback(
    registrar: *mut PixelflowRegistrar,
    descriptor: *const PixelflowFilterDescriptorV1,
) -> PixelflowStatus {
    if registrar.is_null() || descriptor.is_null() {
        return PixelflowStatus::plugin_error(
            "plugin.null_pointer",
            "plugin passed null registration pointer",
        );
    }

    // SAFETY: Both pointers were checked non-null above and originate from host/plugin ABI call for
    // this callback invocation.
    let bridge = unsafe { &mut *registrar.cast::<RegistrarBridge>() };
    // SAFETY: Descriptor pointer was checked non-null above and remains valid for callback duration.
    let descriptor = unsafe { &*descriptor };

    if validate_filter_descriptor(descriptor).is_err() {
        return PixelflowStatus::plugin_error(
            "plugin.invalid_descriptor",
            "plugin provided incompatible filter descriptor",
        );
    }

    let Ok(name) = string_view_to_string(descriptor.name) else {
        return PixelflowStatus::plugin_error(
            "plugin.invalid_filter",
            "plugin returned invalid filter name",
        );
    };
    let Ok(publisher) = string_view_to_string(descriptor.publisher) else {
        return PixelflowStatus::plugin_error(
            "plugin.invalid_filter",
            "plugin returned invalid publisher name",
        );
    };
    let Ok(plugin) = string_view_to_string(descriptor.plugin) else {
        return PixelflowStatus::plugin_error(
            "plugin.invalid_filter",
            "plugin returned invalid plugin namespace",
        );
    };

    // SAFETY: Bridge stores original exclusive `FilterRegistry` pointer from `load_plugin`; host
    // only uses it synchronously during registration callback.
    match unsafe { &mut *bridge.registry }
        .register_filter(FilterDescriptor::new(name, publisher, plugin))
    {
        Ok(()) => PixelflowStatus::ok(),
        Err(_) => PixelflowStatus::plugin_error(
            "plugin.registration_failed",
            "host rejected filter registration",
        ),
    }
}

unsafe extern "C" fn register_metadata_key_callback(
    registrar: *mut PixelflowRegistrar,
    key: PixelflowStringView,
    kind: PixelflowMetadataKind,
) -> PixelflowStatus {
    if registrar.is_null() {
        return PixelflowStatus::plugin_error(
            "plugin.null_pointer",
            "plugin passed null registrar pointer",
        );
    }

    // SAFETY: Pointer was checked non-null above and comes from host-owned bridge for this call.
    let bridge = unsafe { &mut *registrar.cast::<RegistrarBridge>() };
    let Ok(key) = string_view_to_string(key) else {
        return PixelflowStatus::plugin_error(
            "plugin.invalid_metadata_key",
            "plugin returned invalid metadata key",
        );
    };

    let kind = match kind {
        PixelflowMetadataKind::Bool => MetadataKind::Bool,
        PixelflowMetadataKind::Int => MetadataKind::Int,
        PixelflowMetadataKind::Float => MetadataKind::Float,
        PixelflowMetadataKind::String => MetadataKind::String,
        PixelflowMetadataKind::Array => MetadataKind::Array,
        PixelflowMetadataKind::Rational => MetadataKind::Rational,
        PixelflowMetadataKind::Blob => MetadataKind::Blob,
    };

    // SAFETY: Bridge stores original exclusive `FilterRegistry` pointer and callback uses it only
    // for synchronous metadata registration.
    match unsafe { &mut *bridge.registry }.register_metadata_key(&key, kind) {
        Ok(()) => PixelflowStatus::ok(),
        Err(_) => PixelflowStatus::plugin_error(
            "plugin.registration_failed",
            "host rejected metadata registration",
        ),
    }
}

unsafe extern "C" fn log_callback(
    registrar: *mut PixelflowRegistrar,
    level: u32,
    message: PixelflowStringView,
) {
    if registrar.is_null() {
        return;
    }

    // SAFETY: Pointer was checked non-null above and points at host-owned bridge for this call.
    let bridge = unsafe { &mut *registrar.cast::<RegistrarBridge>() };
    let Ok(message) = string_view_to_string(message) else {
        return;
    };

    let level = match level {
        0 => LogLevel::Trace,
        1 => LogLevel::Debug,
        2 => LogLevel::Info,
        3 => LogLevel::Warn,
        _ => LogLevel::Error,
    };
    bridge
        .logger
        .log(level, "pixelflow_core::plugin_host::plugin", message);
}

fn string_view_to_string(view: PixelflowStringView) -> Result<String> {
    if view.ptr.is_null() && view.len == 0 {
        return Ok(String::new());
    }
    if view.ptr.is_null() {
        return Err(PixelFlowError::new(
            ErrorCategory::Plugin,
            ErrorCode::new("plugin.invalid_string"),
            "plugin returned null string pointer with non-zero length",
        ));
    }

    // SAFETY: Null-with-length case rejected above; plugin ABI requires non-null pointer reference
    // `view.len` readable bytes, which are copied/validated immediately.
    let bytes = unsafe { std::slice::from_raw_parts(view.ptr, view.len) };
    std::str::from_utf8(bytes).map(str::to_owned).map_err(|_| {
        PixelFlowError::new(
            ErrorCategory::Plugin,
            ErrorCode::new("plugin.invalid_utf8"),
            "plugin returned invalid UTF-8 string",
        )
    })
}

fn validate_plugin_api(api: &PixelflowPluginApiV1) -> Result<()> {
    if api.size != std::mem::size_of::<PixelflowPluginApiV1>() as u32 {
        return Err(PixelFlowError::new(
            ErrorCategory::Plugin,
            ErrorCode::new("plugin.invalid_descriptor"),
            format!(
                "plugin api size {} does not match expected {}",
                api.size,
                std::mem::size_of::<PixelflowPluginApiV1>()
            ),
        ));
    }
    if api.version != PIXELFLOW_ABI_VERSION {
        return Err(PixelFlowError::new(
            ErrorCategory::Plugin,
            ErrorCode::new("plugin.abi_version_mismatch"),
            format!(
                "plugin api version {} does not match host version {}",
                api.version, PIXELFLOW_ABI_VERSION
            ),
        ));
    }

    Ok(())
}

fn validate_filter_descriptor(descriptor: &PixelflowFilterDescriptorV1) -> Result<()> {
    if descriptor.size != std::mem::size_of::<PixelflowFilterDescriptorV1>() as u32 {
        return Err(PixelFlowError::new(
            ErrorCategory::Plugin,
            ErrorCode::new("plugin.invalid_descriptor"),
            format!(
                "filter descriptor size {} does not match expected {}",
                descriptor.size,
                std::mem::size_of::<PixelflowFilterDescriptorV1>()
            ),
        ));
    }
    if descriptor.version != PIXELFLOW_ABI_VERSION {
        return Err(PixelFlowError::new(
            ErrorCategory::Plugin,
            ErrorCode::new("plugin.abi_version_mismatch"),
            format!(
                "filter descriptor version {} does not match host version {}",
                descriptor.version, PIXELFLOW_ABI_VERSION
            ),
        ));
    }

    Ok(())
}

fn status_to_result(status: PixelflowStatus) -> Result<()> {
    if status.is_ok() {
        return Ok(());
    }

    let code = string_view_to_string(status.error_code)
        .unwrap_or_else(|_| "plugin.callback_failed".to_owned());
    let message = string_view_to_string(status.message)
        .unwrap_or_else(|_| "plugin returned invalid status message".to_owned());
    Err(PixelFlowError::new(
        ErrorCategory::Plugin,
        ErrorCode::new("plugin.callback_failed"),
        format!("foreign code {code}: {message}"),
    ))
}

#[cfg(test)]
mod tests {
    #![expect(clippy::indexing_slicing, reason = "allow in tests")]

    use std::path::Path;
    use std::sync::{Arc, Mutex};

    use tempfile::tempdir;

    use crate::{FilterRegistry, LogRecord, LogSink, Logger};

    use super::{is_dynamic_library, load_plugins_from_directories, platform_plugin_directories};
    use super::{status_to_result, validate_filter_descriptor, validate_plugin_api};
    use crate::{
        ErrorCode, PIXELFLOW_ABI_VERSION, PixelflowErrorCategory, PixelflowFilterDescriptorV1,
        PixelflowHostApiV1, PixelflowPluginApiV1, PixelflowRegistrar, PixelflowStatus,
        PixelflowStringView,
    };

    #[test]
    fn dynamic_library_filter_matches_current_platform_extension() {
        #[cfg(target_os = "linux")]
        assert!(is_dynamic_library(Path::new("libsample.so")));
        #[cfg(target_os = "macos")]
        assert!(is_dynamic_library(Path::new("libsample.dylib")));
        #[cfg(target_os = "windows")]
        assert!(is_dynamic_library(Path::new("sample.dll")));
        assert!(!is_dynamic_library(Path::new("sample.txt")));
    }

    #[test]
    fn platform_plugin_directories_include_conventional_paths() {
        let dirs = platform_plugin_directories();
        assert!(
            dirs.iter()
                .any(|path| path.to_string_lossy().contains("pixelflow"))
        );
    }

    #[derive(Default)]
    struct RecordingSink {
        records: Mutex<Vec<LogRecord>>,
    }

    impl LogSink for RecordingSink {
        fn log(&self, record: &LogRecord) {
            self.records
                .lock()
                .expect("record lock poisoned")
                .push(record.clone());
        }
    }

    #[test]
    fn load_plugins_warns_and_skips_invalid_dynamic_library_files() {
        let tempdir = tempdir().expect("tempdir should exist");
        let invalid_path = tempdir.path().join(if cfg!(target_os = "macos") {
            "libinvalid.dylib"
        } else if cfg!(target_os = "windows") {
            "invalid.dll"
        } else {
            "libinvalid.so"
        });
        std::fs::write(&invalid_path, b"not a shared library")
            .expect("invalid test plugin file should be written");
        std::fs::write(tempdir.path().join("notes.txt"), b"ignore me")
            .expect("non-library marker should be written");

        let sink = Arc::new(RecordingSink::default());
        let logger = Logger::new(sink.clone());
        let mut registry = FilterRegistry::new();

        let loaded =
            load_plugins_from_directories(&[tempdir.path().to_path_buf()], &mut registry, &logger);

        assert!(loaded.is_empty());
        assert!(registry.filter_names().is_empty());

        let records = sink.records.lock().expect("record lock poisoned");
        assert_eq!(records.len(), 1);
        assert!(records[0].message().contains("skipping plugin"));
    }

    #[test]
    fn validate_plugin_api_rejects_wrong_version() {
        let api = PixelflowPluginApiV1 {
            size: std::mem::size_of::<PixelflowPluginApiV1>() as u32,
            version: PIXELFLOW_ABI_VERSION + 1,
            plugin_name: placeholder_plugin_name,
            register: placeholder_register,
            reserved: [0; 5],
        };

        let error = validate_plugin_api(&api)
            .expect_err("plugin api with wrong version should fail validation");

        assert_eq!(error.code(), ErrorCode::new("plugin.abi_version_mismatch"));
    }

    #[test]
    fn validate_filter_descriptor_rejects_wrong_size() {
        let descriptor = PixelflowFilterDescriptorV1 {
            size: 1,
            version: PIXELFLOW_ABI_VERSION,
            name: PixelflowStringView::from_rust_str("sample.identity"),
            publisher: PixelflowStringView::from_rust_str("pixelflow"),
            plugin: PixelflowStringView::from_rust_str("sample"),
            reserved: [0; 4],
        };

        let status = validate_filter_descriptor(&descriptor)
            .expect_err("descriptor with wrong size should fail validation");

        assert_eq!(status.code(), ErrorCode::new("plugin.invalid_descriptor"));
    }

    #[test]
    fn status_to_result_uses_static_host_error_code() {
        let status = PixelflowStatus {
            size: std::mem::size_of::<PixelflowStatus>() as u32,
            version: PIXELFLOW_ABI_VERSION,
            status_code: 1,
            category: PixelflowErrorCategory::Plugin,
            error_code: PixelflowStringView::from_rust_str("plugin.dynamic_code"),
            message: PixelflowStringView::from_rust_str("dynamic message"),
            reserved: [0; 4],
        };

        let error = status_to_result(status).expect_err("non-ok status should fail");

        assert_eq!(error.code(), ErrorCode::new("plugin.callback_failed"));
        assert!(error.message().contains("plugin.dynamic_code"));
        assert!(error.message().contains("dynamic message"));
    }

    unsafe extern "C" fn placeholder_plugin_name() -> PixelflowStringView {
        PixelflowStringView::from_rust_str("placeholder")
    }

    unsafe extern "C" fn placeholder_register(
        _host: *const PixelflowHostApiV1,
        _registrar: *mut PixelflowRegistrar,
    ) -> PixelflowStatus {
        PixelflowStatus::ok()
    }
}
