use super::default_vst3_search_roots;
use super::interfaces::PluginFactory;
use serde::{Deserialize, Serialize};
use std::path::Path;

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Vst3PluginInfo {
    pub id: String,
    pub name: String,
    pub vendor: String,
    pub path: String,
    pub category: String,
    pub version: String,
    pub audio_inputs: usize,
    pub audio_outputs: usize,
    pub has_midi_input: bool,
    pub has_midi_output: bool,
}

pub struct Vst3Host {
    plugins: Vec<Vst3PluginInfo>,
}

impl Default for Vst3Host {
    fn default() -> Self {
        Self::new()
    }
}

impl Vst3Host {
    pub fn new() -> Self {
        Self {
            plugins: Vec::new(),
        }
    }

    pub fn list_plugins(&mut self) -> Vec<Vst3PluginInfo> {
        let blocklist = crate::blocklist::Blocklist::load();
        let mut roots = default_vst3_search_roots();

        if let Ok(extra) = std::env::var("VST3_PATH") {
            for p in std::env::split_paths(&extra) {
                if !p.as_os_str().is_empty() {
                    roots.push(p);
                }
            }
        }

        let mut out = Vec::new();
        for root in roots {
            collect_vst3_plugins(&root, &mut out, &blocklist);
        }

        out.sort_by_key(|a| a.name.to_lowercase());

        out.dedup_by(|a, b| a.path.eq_ignore_ascii_case(&b.path));

        self.plugins = out.clone();
        out
    }

    pub fn get_plugin_info(&self, path: &str) -> Option<&Vst3PluginInfo> {
        self.plugins.iter().find(|p| p.path == path)
    }
}

fn collect_vst3_plugins(
    root: &Path,
    out: &mut Vec<Vst3PluginInfo>,
    blocklist: &crate::blocklist::Blocklist,
) {
    let Ok(entries) = std::fs::read_dir(root) else {
        return;
    };
    for entry in entries.flatten() {
        let path = entry.path();
        let path_str = path.to_string_lossy().to_string();
        let Ok(ft) = entry.file_type() else {
            continue;
        };
        if ft.is_symlink() {
            continue;
        }

        if !ft.is_dir() {
            continue;
        }

        if path
            .extension()
            .is_some_and(|ext| ext.eq_ignore_ascii_case("vst3"))
        {
            if blocklist.is_blocked(&path_str) {
                tracing::debug!("skipping blocklisted VST3 plugin bundle: {path_str}");
                continue;
            }
            if let Some(info) = scan_vst3_bundle(&path) {
                out.push(info);
            }
        } else {
            collect_vst3_plugins(&path, out, blocklist);
        }
    }
}

fn scan_vst3_bundle(bundle_path: &Path) -> Option<Vst3PluginInfo> {
    tracing::debug!("scanning VST3 plugin bundle: {}", bundle_path.display());
    let factory = PluginFactory::from_module(bundle_path).ok()?;
    let class_count = factory.count_classes();
    if class_count == 0 {
        return None;
    }

    let factory_info = factory.get_factory_info();

    let mut audio_module_index = None;
    let mut fallback_index = None;
    for index in 0..class_count {
        let Some(class_info) = factory.get_class_info(index) else {
            continue;
        };
        if fallback_index.is_none() {
            fallback_index = Some(index);
        }
        if class_info.category.contains("Audio Module") {
            audio_module_index = Some(index);
            break;
        }
    }

    let target_index = audio_module_index.or(fallback_index)?;
    let class_info = factory.get_class_info(target_index)?;

    let Ok(mut instance) = factory.create_instance(&class_info.cid) else {
        return Some(class_info_to_plugin_info(
            &class_info,
            bundle_path,
            None,
            factory_info.as_ref(),
        ));
    };
    let capabilities = match instance.initialize(&factory) {
        Ok(()) => {
            let (audio_inputs, audio_outputs) = instance.main_audio_channel_counts();
            let (midi_inputs, midi_outputs) = instance.event_bus_counts();
            let _ = instance.terminate();
            Some((
                audio_inputs,
                audio_outputs,
                midi_inputs > 0,
                midi_outputs > 0,
            ))
        }
        Err(_) => None,
    };
    let info = class_info_to_plugin_info(
        &class_info,
        bundle_path,
        capabilities,
        factory_info.as_ref(),
    );
    tracing::debug!(
        "finished scanning VST3 plugin bundle: {} ({}",
        bundle_path.display(),
        info.name
    );
    Some(info)
}

fn class_info_to_plugin_info(
    class_info: &super::interfaces::ClassInfo,
    bundle_path: &Path,
    capabilities: Option<(usize, usize, bool, bool)>,
    factory_info: Option<&super::interfaces::FactoryInfo>,
) -> Vst3PluginInfo {
    let (audio_inputs, audio_outputs, has_midi_input, has_midi_output) =
        capabilities.unwrap_or((0, 0, false, false));

    Vst3PluginInfo {
        id: tuid_to_string(&class_info.cid),
        name: class_info.name.clone(),
        vendor: factory_info.map(|f| f.vendor.clone()).unwrap_or_default(),
        path: bundle_path.to_string_lossy().to_string(),
        category: class_info.category.clone(),
        version: String::new(),
        audio_inputs,
        audio_outputs,
        has_midi_input,
        has_midi_output,
    }
}

fn tuid_to_string(tuid: &[i8; 16]) -> String {
    tuid.iter()
        .map(|&b| format!("{:02X}", b as u8))
        .collect::<Vec<_>>()
        .join("")
}

pub fn list_plugins() -> Vec<Vst3PluginInfo> {
    let mut host = Vst3Host::new();
    host.list_plugins()
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::vst3::interfaces::ClassInfo;
    use std::path::PathBuf;

    #[test]
    fn class_info_to_plugin_info_uses_capabilities_when_present() {
        let class_info = ClassInfo {
            name: "Synth".to_string(),
            category: "Instrument".to_string(),
            cid: [0x12_i8; 16],
        };

        let info = class_info_to_plugin_info(
            &class_info,
            Path::new("/tmp/Test.vst3"),
            Some((2, 4, true, false)),
            None,
        );

        assert_eq!(info.id, "12121212121212121212121212121212");
        assert_eq!(info.name, "Synth");
        assert_eq!(info.category, "Instrument");
        assert_eq!(info.path, "/tmp/Test.vst3");
        assert_eq!(info.audio_inputs, 2);
        assert_eq!(info.audio_outputs, 4);
        assert!(info.has_midi_input);
        assert!(!info.has_midi_output);
    }

    #[test]
    fn class_info_to_plugin_info_defaults_capabilities_when_missing() {
        let class_info = ClassInfo {
            name: "Fx".to_string(),
            category: "Audio Module".to_string(),
            cid: [0; 16],
        };

        let info = class_info_to_plugin_info(&class_info, Path::new("/tmp/Fx.vst3"), None, None);

        assert_eq!(info.audio_inputs, 0);
        assert_eq!(info.audio_outputs, 0);
        assert!(!info.has_midi_input);
        assert!(!info.has_midi_output);
    }

    #[test]
    fn tuid_to_string_formats_bytes_as_uppercase_hex() {
        let tuid = [
            0x00_i8, 0x01, 0x23, 0x45, 0x67, 0x7F, -0x80, -0x01, 0x10, 0x20, 0x30, 0x40, 0x50,
            0x60, 0x70, 0x7E,
        ];

        assert_eq!(tuid_to_string(&tuid), "00012345677F80FF102030405060707E");
    }

    #[test]
    fn get_plugin_info_returns_cached_plugin_by_exact_path() {
        let mut host = Vst3Host::new();
        host.plugins = vec![
            Vst3PluginInfo {
                id: "id-1".to_string(),
                name: "First".to_string(),
                vendor: String::new(),
                path: "/tmp/First.vst3".to_string(),
                category: "Instrument".to_string(),
                version: String::new(),
                audio_inputs: 2,
                audio_outputs: 2,
                has_midi_input: true,
                has_midi_output: false,
            },
            Vst3PluginInfo {
                id: "id-2".to_string(),
                name: "Second".to_string(),
                vendor: String::new(),
                path: "/tmp/Second.vst3".to_string(),
                category: "Fx".to_string(),
                version: String::new(),
                audio_inputs: 2,
                audio_outputs: 2,
                has_midi_input: false,
                has_midi_output: false,
            },
        ];

        let found = host
            .get_plugin_info("/tmp/Second.vst3")
            .map(|p| p.name.clone());
        let missing = host.get_plugin_info(&PathBuf::from("/tmp/missing.vst3").to_string_lossy());

        assert_eq!(found.as_deref(), Some("Second"));
        assert!(missing.is_none());
    }
}
