//! Filesystem plugin stores and external MCP supervisor.

use std::collections::{BTreeMap, HashMap, HashSet};
use std::fs;
use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};
use std::process::Stdio;
use std::sync::{Arc, Mutex, MutexGuard};
use std::time::{SystemTime, UNIX_EPOCH};

use async_trait::async_trait;
use domain::ports::{
    LocalPath, PluginManifestBytes, PluginMcpError, PluginMcpSupervisor, PluginPackageStore,
    PluginRegistryError, PluginRegistryStore, PluginStorageError, PluginStorageStore,
    PluginStoreError,
};
use domain::{
    ContentHash, PluginBundleUrl, PluginId, PluginInstallSource, PluginMcpServerSpec,
    PluginMcpStatus, PluginMcpStatusSet, PluginPackageRef, PluginRegistry, RelativePath,
    RemovalOutcome, StagedPluginPackage,
};
use sha2::{Digest, Sha256};
use tokio::process::Child;

const REGISTRY_FILE: &str = "registry.json";
const MANIFEST_FILE: &str = "idea-plugin.json";
const STORAGE_ENTRIES_DIR: &str = "entries";

#[derive(Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
struct PluginStorageEntry {
    key: String,
    value: serde_json::Value,
}

/// Filesystem package store under app-data `plugins/`.
#[derive(Debug, Clone)]
pub struct FsPluginPackageStore {
    root: PathBuf,
}

impl FsPluginPackageStore {
    /// Builds the store.
    #[must_use]
    pub fn new(app_data_dir: impl Into<PathBuf>) -> Self {
        Self {
            root: app_data_dir.into().join("plugins"),
        }
    }

    fn installed_dir(&self) -> PathBuf {
        self.root.join("installed")
    }

    /// Returns the global plugin store root.
    #[must_use]
    pub fn plugins_root(&self) -> PathBuf {
        self.root.clone()
    }

    /// Returns the managed directory for one installed plugin id.
    #[must_use]
    pub fn installed_plugin_dir(&self, plugin_id: &PluginId) -> PathBuf {
        self.installed_dir().join(plugin_id.as_str())
    }

    fn staging_dir(&self) -> PathBuf {
        self.root.join("_staging")
    }

    fn trash_dir(&self) -> PathBuf {
        self.root.join("_trash")
    }

    fn package_root(&self, package: &PluginPackageRef) -> PathBuf {
        match &package.plugin_id {
            Some(id) => self.installed_dir().join(id.as_str()),
            None => PathBuf::from(&package.root),
        }
    }

    /// Resolves an asset path if the request is root-confined.
    pub fn resolve_asset_path(
        &self,
        plugin_id: &PluginId,
        path: &RelativePath,
    ) -> Result<PathBuf, PluginStoreError> {
        let root = self.installed_dir().join(plugin_id.as_str());
        let candidate = root.join(path.as_str());
        let canonical_root = root
            .canonicalize()
            .map_err(|e| PluginStoreError::Io(e.to_string()))?;
        let canonical = candidate
            .canonicalize()
            .map_err(|e| PluginStoreError::Io(e.to_string()))?;
        if canonical.starts_with(canonical_root) {
            Ok(canonical)
        } else {
            Err(PluginStoreError::Invalid(
                "plugin asset escapes package root".to_owned(),
            ))
        }
    }

    fn stage_root(&self) -> Result<PathBuf, PluginStoreError> {
        let ts = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .map_err(|e| PluginStoreError::Io(e.to_string()))?
            .as_nanos();
        let path = self.staging_dir().join(format!("stage-{ts}"));
        fs::create_dir_all(&path).map_err(|e| PluginStoreError::Io(e.to_string()))?;
        Ok(path)
    }
}

#[async_trait]
impl PluginPackageStore for FsPluginPackageStore {
    async fn list_installed(&self) -> Result<Vec<PluginPackageRef>, PluginStoreError> {
        let dir = self.installed_dir();
        if !dir.exists() {
            return Ok(Vec::new());
        }
        let mut out = Vec::new();
        for entry in fs::read_dir(dir).map_err(|e| PluginStoreError::Io(e.to_string()))? {
            let entry = entry.map_err(|e| PluginStoreError::Io(e.to_string()))?;
            if !entry
                .file_type()
                .map_err(|e| PluginStoreError::Io(e.to_string()))?
                .is_dir()
            {
                continue;
            }
            let name = entry.file_name().to_string_lossy().into_owned();
            if let Ok(id) = PluginId::new(name) {
                out.push(PluginPackageRef {
                    plugin_id: Some(id),
                    root: entry.path().to_string_lossy().into_owned(),
                });
            }
        }
        Ok(out)
    }

    async fn read_manifest(
        &self,
        package: &PluginPackageRef,
    ) -> Result<PluginManifestBytes, PluginStoreError> {
        let bytes = fs::read(self.package_root(package).join(MANIFEST_FILE)).map_err(|e| {
            if e.kind() == std::io::ErrorKind::NotFound {
                PluginStoreError::NotFound
            } else {
                PluginStoreError::Io(e.to_string())
            }
        })?;
        Ok(PluginManifestBytes { bytes })
    }

    async fn install_from_archive(
        &self,
        archive: &LocalPath,
    ) -> Result<StagedPluginPackage, PluginStoreError> {
        let stage = self.stage_root()?;
        extract_archive_confined(Path::new(archive.as_str()), &stage)?;
        ensure_manifest(&stage)?;
        let content_hash = hash_dir(&stage)?;
        Ok(StagedPluginPackage {
            root: stage.to_string_lossy().into_owned(),
            source: PluginInstallSource::Archive {
                path_label: archive.as_str().to_owned(),
            },
            content_hash,
        })
    }

    async fn install_from_directory(
        &self,
        dir: &LocalPath,
    ) -> Result<StagedPluginPackage, PluginStoreError> {
        let source = PathBuf::from(dir.as_str());
        if !source.is_dir() {
            return Err(PluginStoreError::NotFound);
        }
        let stage = self.stage_root()?;
        copy_dir_all(&source, &stage)?;
        ensure_manifest(&stage)?;
        let content_hash = hash_dir(&stage)?;
        Ok(StagedPluginPackage {
            root: stage.to_string_lossy().into_owned(),
            source: PluginInstallSource::Directory {
                path_label: dir.as_str().to_owned(),
            },
            content_hash,
        })
    }

    async fn commit_install(
        &self,
        staged: StagedPluginPackage,
        plugin_id: &PluginId,
    ) -> Result<PluginPackageRef, PluginStoreError> {
        fs::create_dir_all(self.installed_dir())
            .map_err(|e| PluginStoreError::Io(e.to_string()))?;
        let target = self.installed_dir().join(plugin_id.as_str());
        let tmp = self
            .installed_dir()
            .join(format!(".{}-new", plugin_id.as_str()));
        if tmp.exists() {
            fs::remove_dir_all(&tmp).map_err(|e| PluginStoreError::Io(e.to_string()))?;
        }
        fs::rename(&staged.root, &tmp).or_else(|_| {
            copy_dir_all(Path::new(&staged.root), &tmp)?;
            fs::remove_dir_all(&staged.root).map_err(|e| PluginStoreError::Io(e.to_string()))
        })?;
        if target.exists() {
            fs::remove_dir_all(&target).map_err(|e| PluginStoreError::Io(e.to_string()))?;
        }
        fs::rename(&tmp, &target).map_err(|e| PluginStoreError::Io(e.to_string()))?;
        Ok(PluginPackageRef {
            plugin_id: Some(plugin_id.clone()),
            root: target.to_string_lossy().into_owned(),
        })
    }

    async fn remove_package(
        &self,
        plugin_id: &PluginId,
    ) -> Result<RemovalOutcome, PluginStoreError> {
        let target = self.installed_dir().join(plugin_id.as_str());
        if !target.exists() {
            return Ok(RemovalOutcome::NotFound);
        }
        match fs::remove_dir_all(&target) {
            Ok(()) => Ok(RemovalOutcome::Removed),
            Err(_) => {
                fs::create_dir_all(self.trash_dir())
                    .map_err(|e| PluginStoreError::Io(e.to_string()))?;
                let ts = SystemTime::now()
                    .duration_since(UNIX_EPOCH)
                    .map_err(|e| PluginStoreError::Io(e.to_string()))?
                    .as_secs();
                let tombstone = self
                    .trash_dir()
                    .join(format!("{}-{ts}", plugin_id.as_str()));
                fs::rename(&target, tombstone).map_err(|e| PluginStoreError::Io(e.to_string()))?;
                Ok(RemovalOutcome::Tombstoned)
            }
        }
    }

    fn bundle_url(
        &self,
        plugin_id: &PluginId,
        entry: &RelativePath,
        hash: &ContentHash,
    ) -> Result<PluginBundleUrl, PluginStoreError> {
        self.resolve_asset_path(plugin_id, entry)?;
        Ok(PluginBundleUrl::new(format!(
            "idea-plugin://{}/current/{}{}{}",
            plugin_id.as_str(),
            hash.as_str(),
            "/",
            entry.as_str()
        )))
    }

    fn app_data_dir_label(&self) -> Option<String> {
        self.root.parent().map(|p| p.to_string_lossy().into_owned())
    }
}

fn ensure_manifest(root: &Path) -> Result<(), PluginStoreError> {
    if root.join(MANIFEST_FILE).is_file() {
        Ok(())
    } else {
        Err(PluginStoreError::NotFound)
    }
}

fn extract_archive_confined(archive: &Path, stage: &Path) -> Result<(), PluginStoreError> {
    let file = fs::File::open(archive).map_err(|e| PluginStoreError::Io(e.to_string()))?;
    let mut archive = zip::ZipArchive::new(file)
        .map_err(|e| PluginStoreError::Format(format!("invalid zip archive: {e}")))?;
    for index in 0..archive.len() {
        let mut entry = archive
            .by_index(index)
            .map_err(|e| PluginStoreError::Format(format!("invalid zip entry: {e}")))?;
        let entry_name = entry.name().to_owned();
        let enclosed = entry.enclosed_name().ok_or_else(|| {
            PluginStoreError::Invalid(format!("archive entry escapes plugin root: {entry_name}"))
        })?;
        if entry
            .unix_mode()
            .is_some_and(|mode| mode & 0o170000 == 0o120000)
        {
            return Err(PluginStoreError::Invalid(format!(
                "archive entry symlinks are not allowed: {entry_name}"
            )));
        }
        let destination = stage.join(&enclosed);
        if entry.is_dir() {
            fs::create_dir_all(&destination).map_err(|e| PluginStoreError::Io(e.to_string()))?;
            continue;
        }
        if let Some(parent) = destination.parent() {
            fs::create_dir_all(parent).map_err(|e| PluginStoreError::Io(e.to_string()))?;
        }
        let mut out =
            fs::File::create(&destination).map_err(|e| PluginStoreError::Io(e.to_string()))?;
        io::copy(&mut entry, &mut out).map_err(|e| PluginStoreError::Io(e.to_string()))?;
        out.flush()
            .map_err(|e| PluginStoreError::Io(e.to_string()))?;
    }
    Ok(())
}

fn copy_dir_all(source: &Path, target: &Path) -> Result<(), PluginStoreError> {
    fs::create_dir_all(target).map_err(|e| PluginStoreError::Io(e.to_string()))?;
    for entry in fs::read_dir(source).map_err(|e| PluginStoreError::Io(e.to_string()))? {
        let entry = entry.map_err(|e| PluginStoreError::Io(e.to_string()))?;
        let ty = entry
            .file_type()
            .map_err(|e| PluginStoreError::Io(e.to_string()))?;
        let dest = target.join(entry.file_name());
        if ty.is_dir() {
            copy_dir_all(&entry.path(), &dest)?;
        } else if ty.is_file() {
            fs::copy(entry.path(), dest).map_err(|e| PluginStoreError::Io(e.to_string()))?;
        } else if ty.is_symlink() {
            return Err(PluginStoreError::Invalid(format!(
                "plugin source contains symlink: {}",
                entry.path().display()
            )));
        } else {
            return Err(PluginStoreError::Invalid(format!(
                "plugin source contains unsupported entry: {}",
                entry.path().display()
            )));
        }
    }
    Ok(())
}

fn hash_dir(root: &Path) -> Result<ContentHash, PluginStoreError> {
    let mut files = Vec::new();
    collect_files(root, &mut files)?;
    files.sort();
    let mut hasher = Sha256::new();
    for path in files {
        let rel = path
            .strip_prefix(root)
            .map_err(|e| PluginStoreError::Io(e.to_string()))?
            .to_string_lossy()
            .replace('\\', "/");
        hasher.update(rel.as_bytes());
        hasher.update([0]);
        let mut file = fs::File::open(&path).map_err(|e| PluginStoreError::Io(e.to_string()))?;
        let mut buf = Vec::new();
        file.read_to_end(&mut buf)
            .map_err(|e| PluginStoreError::Io(e.to_string()))?;
        hasher.update(buf);
        hasher.update([0]);
    }
    ContentHash::new(hex::encode(hasher.finalize()))
        .map_err(|e| PluginStoreError::Invalid(e.to_string()))
}

fn collect_files(root: &Path, files: &mut Vec<PathBuf>) -> Result<(), PluginStoreError> {
    for entry in fs::read_dir(root).map_err(|e| PluginStoreError::Io(e.to_string()))? {
        let entry = entry.map_err(|e| PluginStoreError::Io(e.to_string()))?;
        let ty = entry
            .file_type()
            .map_err(|e| PluginStoreError::Io(e.to_string()))?;
        if ty.is_dir() {
            collect_files(&entry.path(), files)?;
        } else if ty.is_file() {
            files.push(entry.path());
        } else if ty.is_symlink() {
            return Err(PluginStoreError::Invalid(format!(
                "plugin package contains symlink: {}",
                entry.path().display()
            )));
        } else {
            return Err(PluginStoreError::Invalid(format!(
                "plugin package contains unsupported entry: {}",
                entry.path().display()
            )));
        }
    }
    Ok(())
}

/// Filesystem registry store.
#[derive(Debug, Clone)]
pub struct FsPluginRegistryStore {
    root: PathBuf,
}

impl FsPluginRegistryStore {
    /// Builds the store.
    #[must_use]
    pub fn new(app_data_dir: impl Into<PathBuf>) -> Self {
        Self {
            root: app_data_dir.into().join("plugins"),
        }
    }

    fn path(&self) -> PathBuf {
        self.root.join(REGISTRY_FILE)
    }
}

/// Filesystem plugin-owned storage under app-data `plugins/data/<pluginId>/`.
#[derive(Debug, Clone)]
pub struct FsPluginStorageStore {
    root: PathBuf,
}

impl FsPluginStorageStore {
    /// Builds the store.
    #[must_use]
    pub fn new(app_data_dir: impl Into<PathBuf>) -> Self {
        Self {
            root: app_data_dir.into().join("plugins").join("data"),
        }
    }

    fn plugin_dir(&self, plugin_id: &PluginId) -> PathBuf {
        self.root.join(plugin_id.as_str())
    }

    fn entry_path(&self, plugin_id: &PluginId, key: &str) -> PathBuf {
        let digest = Sha256::digest(key.as_bytes());
        self.plugin_dir(plugin_id)
            .join(STORAGE_ENTRIES_DIR)
            .join(format!("{}.json", hex::encode(digest)))
    }
}

#[async_trait]
impl PluginStorageStore for FsPluginStorageStore {
    async fn get(
        &self,
        plugin_id: &PluginId,
        key: &str,
    ) -> Result<Option<serde_json::Value>, PluginStorageError> {
        let path = self.entry_path(plugin_id, key);
        if !path.exists() {
            return Ok(None);
        }
        let bytes = fs::read(&path).map_err(|e| PluginStorageError::Io(e.to_string()))?;
        let entry: PluginStorageEntry = serde_json::from_slice(&bytes)
            .map_err(|e| PluginStorageError::Serialization(e.to_string()))?;
        if entry.key == key {
            Ok(Some(entry.value))
        } else {
            Err(PluginStorageError::Invalid(
                "plugin storage key hash collision".to_owned(),
            ))
        }
    }

    async fn set(
        &self,
        plugin_id: &PluginId,
        key: &str,
        value: serde_json::Value,
    ) -> Result<(), PluginStorageError> {
        let path = self.entry_path(plugin_id, key);
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent).map_err(|e| PluginStorageError::Io(e.to_string()))?;
        }
        let entry = PluginStorageEntry {
            key: key.to_owned(),
            value,
        };
        let bytes = serde_json::to_vec_pretty(&entry)
            .map_err(|e| PluginStorageError::Serialization(e.to_string()))?;
        fs::write(path, bytes).map_err(|e| PluginStorageError::Io(e.to_string()))
    }

    async fn delete(&self, plugin_id: &PluginId, key: &str) -> Result<bool, PluginStorageError> {
        let path = self.entry_path(plugin_id, key);
        match fs::remove_file(path) {
            Ok(()) => Ok(true),
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
            Err(e) => Err(PluginStorageError::Io(e.to_string())),
        }
    }

    async fn purge_plugin(
        &self,
        plugin_id: &PluginId,
    ) -> Result<RemovalOutcome, PluginStorageError> {
        let dir = self.plugin_dir(plugin_id);
        match fs::remove_dir_all(dir) {
            Ok(()) => Ok(RemovalOutcome::Removed),
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(RemovalOutcome::NotFound),
            Err(e) => Err(PluginStorageError::Io(e.to_string())),
        }
    }
}

#[async_trait]
impl PluginRegistryStore for FsPluginRegistryStore {
    async fn load_registry(&self) -> Result<PluginRegistry, PluginRegistryError> {
        match fs::read(self.path()) {
            Ok(bytes) => serde_json::from_slice(&bytes)
                .map_err(|e| PluginRegistryError::Serialization(e.to_string())),
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(PluginRegistry::default()),
            Err(e) => Err(PluginRegistryError::Io(e.to_string())),
        }
    }

    async fn save_registry(&self, registry: &PluginRegistry) -> Result<(), PluginRegistryError> {
        fs::create_dir_all(&self.root).map_err(|e| PluginRegistryError::Io(e.to_string()))?;
        let bytes = serde_json::to_vec_pretty(registry)
            .map_err(|e| PluginRegistryError::Serialization(e.to_string()))?;
        fs::write(self.path(), bytes).map_err(|e| PluginRegistryError::Io(e.to_string()))
    }
}

#[async_trait]
trait ExternalMcpServerHandle: Send {
    async fn stop(&mut self) -> Result<(), PluginMcpError>;
}

#[async_trait]
trait ExternalMcpServerBridge: Send + Sync {
    async fn start(
        &self,
        spec: &PluginMcpServerSpec,
    ) -> Result<Box<dyn ExternalMcpServerHandle>, PluginMcpError>;
}

type ExternalMcpChildren = HashMap<String, Box<dyn ExternalMcpServerHandle>>;

struct ProcessMcpServerHandle {
    child: Child,
}

#[async_trait]
impl ExternalMcpServerHandle for ProcessMcpServerHandle {
    async fn stop(&mut self) -> Result<(), PluginMcpError> {
        self.child
            .kill()
            .await
            .map_err(|e| PluginMcpError::Process(e.to_string()))
    }
}

#[derive(Debug, Default)]
struct StdioExternalMcpServerBridge;

#[async_trait]
impl ExternalMcpServerBridge for StdioExternalMcpServerBridge {
    async fn start(
        &self,
        spec: &PluginMcpServerSpec,
    ) -> Result<Box<dyn ExternalMcpServerHandle>, PluginMcpError> {
        if spec.transport != "stdio" {
            return Err(PluginMcpError::Process(format!(
                "unsupported plugin MCP transport: {}",
                spec.transport
            )));
        }
        let mut cmd = tokio::process::Command::new(&spec.command);
        cmd.args(&spec.args)
            .current_dir(&spec.cwd)
            .stdin(Stdio::piped())
            .stdout(Stdio::piped())
            .stderr(Stdio::piped());
        let env: BTreeMap<_, _> = spec.env.iter().cloned().collect();
        cmd.envs(env);
        let child = cmd
            .spawn()
            .map_err(|e| PluginMcpError::Process(e.to_string()))?;
        Ok(Box::new(ProcessMcpServerHandle { child }))
    }
}

/// External process supervisor for plugin MCP servers.
pub struct ExternalMcpPluginSupervisor {
    bridge: Arc<dyn ExternalMcpServerBridge>,
    children: Mutex<ExternalMcpChildren>,
}

impl ExternalMcpPluginSupervisor {
    /// Builds the supervisor.
    #[must_use]
    pub fn new() -> Self {
        Self {
            bridge: Arc::new(StdioExternalMcpServerBridge),
            children: Mutex::new(HashMap::new()),
        }
    }

    #[cfg(test)]
    fn with_bridge(bridge: Arc<dyn ExternalMcpServerBridge>) -> Self {
        Self {
            bridge,
            children: Mutex::new(HashMap::new()),
        }
    }

    fn children(&self) -> Result<MutexGuard<'_, ExternalMcpChildren>, PluginMcpError> {
        self.children
            .lock()
            .map_err(|_| PluginMcpError::Process("plugin mcp supervisor mutex poisoned".to_owned()))
    }
}

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

#[async_trait]
impl PluginMcpSupervisor for ExternalMcpPluginSupervisor {
    async fn reconcile(
        &self,
        active_servers: Vec<PluginMcpServerSpec>,
    ) -> Result<PluginMcpStatusSet, PluginMcpError> {
        let desired: HashSet<String> = active_servers.iter().map(|s| s.identity.clone()).collect();
        let to_stop = {
            let children = self.children()?;
            children
                .keys()
                .filter(|id| !desired.contains(*id))
                .cloned()
                .collect::<Vec<_>>()
        };
        for id in to_stop {
            let child = self.children()?.remove(&id);
            if let Some(mut child) = child {
                let _ = child.stop().await;
            }
        }
        let mut statuses = Vec::new();
        for spec in active_servers {
            let already = self.children()?.contains_key(&spec.identity);
            if already {
                statuses.push(PluginMcpStatus {
                    identity: spec.identity,
                    running: true,
                    error: None,
                });
                continue;
            }
            match self.bridge.start(&spec).await {
                Ok(handle) => {
                    match self.children() {
                        Ok(mut children) => {
                            children.insert(spec.identity.clone(), handle);
                        }
                        Err(err) => return Err(err),
                    }
                    statuses.push(PluginMcpStatus {
                        identity: spec.identity,
                        running: true,
                        error: None,
                    });
                }
                Err(e) => statuses.push(PluginMcpStatus {
                    identity: spec.identity,
                    running: false,
                    error: Some(e.to_string()),
                }),
            }
        }
        Ok(PluginMcpStatusSet { servers: statuses })
    }

    async fn stop_plugin(&self, plugin_id: &PluginId) -> Result<(), PluginMcpError> {
        let prefix = format!("plugin:{}:", plugin_id.as_str());
        let ids = {
            let children = self.children()?;
            children
                .keys()
                .filter(|id| id.starts_with(&prefix))
                .cloned()
                .collect::<Vec<_>>()
        };
        for id in ids {
            let child = self.children()?.remove(&id);
            if let Some(mut child) = child {
                child.stop().await?;
            }
        }
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use domain::ports::PluginPackageStore;
    use std::sync::atomic::{AtomicU64, Ordering};

    static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);

    fn temp_dir(label: &str) -> PathBuf {
        let n = TEMP_COUNTER.fetch_add(1, Ordering::SeqCst);
        let path = std::env::temp_dir().join(format!(
            "idea-plugin-test-{label}-{}-{n}",
            std::process::id()
        ));
        let _ = fs::remove_dir_all(&path);
        fs::create_dir_all(&path).unwrap();
        path
    }

    fn write_plugin(root: &Path, main_body: &str) {
        fs::create_dir_all(root.join("dist")).unwrap();
        fs::write(root.join("dist/index.js"), main_body).unwrap();
        fs::write(
            root.join(MANIFEST_FILE),
            r#"{"ideaPluginManifestVersion":1,"id":"dev.acme.test","displayName":"Test","version":"1.0.0","main":"dist/index.js","trustLevel":"full","contributes":{}}"#,
        )
        .unwrap();
    }

    struct ZipEntrySpec<'a> {
        name: &'a str,
        contents: &'a [u8],
        unix_mode: Option<u32>,
    }

    fn write_u16(out: &mut Vec<u8>, value: u16) {
        out.extend_from_slice(&value.to_le_bytes());
    }

    fn write_u32(out: &mut Vec<u8>, value: u32) {
        out.extend_from_slice(&value.to_le_bytes());
    }

    fn crc32(bytes: &[u8]) -> u32 {
        let mut crc = 0xffff_ffffu32;
        for &byte in bytes {
            crc ^= u32::from(byte);
            for _ in 0..8 {
                let mask = (crc & 1).wrapping_neg();
                crc = (crc >> 1) ^ (0xedb8_8320 & mask);
            }
        }
        !crc
    }

    fn write_zip(path: &Path, entries: &[ZipEntrySpec<'_>]) {
        let mut file = fs::File::create(path).unwrap();
        let mut central = Vec::new();
        let mut offset = 0u32;
        for entry in entries {
            let name = entry.name.as_bytes();
            let data = entry.contents;
            let crc = crc32(data);
            let local_size = 30u32 + name.len() as u32 + data.len() as u32;

            let mut local = Vec::new();
            write_u32(&mut local, 0x0403_4b50);
            write_u16(&mut local, 20);
            write_u16(&mut local, 0);
            write_u16(&mut local, 0);
            write_u16(&mut local, 0);
            write_u16(&mut local, 0);
            write_u32(&mut local, crc);
            write_u32(&mut local, data.len() as u32);
            write_u32(&mut local, data.len() as u32);
            write_u16(&mut local, name.len() as u16);
            write_u16(&mut local, 0);
            local.extend_from_slice(name);
            local.extend_from_slice(data);
            file.write_all(&local).unwrap();

            let mut header = Vec::new();
            let version_made_by = if entry.unix_mode.is_some() {
                (3u16 << 8) | 20
            } else {
                20
            };
            let external_attributes = entry.unix_mode.unwrap_or(0) << 16;
            write_u32(&mut header, 0x0201_4b50);
            write_u16(&mut header, version_made_by);
            write_u16(&mut header, 20);
            write_u16(&mut header, 0);
            write_u16(&mut header, 0);
            write_u16(&mut header, 0);
            write_u16(&mut header, 0);
            write_u32(&mut header, crc);
            write_u32(&mut header, data.len() as u32);
            write_u32(&mut header, data.len() as u32);
            write_u16(&mut header, name.len() as u16);
            write_u16(&mut header, 0);
            write_u16(&mut header, 0);
            write_u16(&mut header, 0);
            write_u16(&mut header, 0);
            write_u32(&mut header, external_attributes);
            write_u32(&mut header, offset);
            header.extend_from_slice(name);
            central.extend_from_slice(&header);
            offset += local_size;
        }
        file.write_all(&central).unwrap();
        let mut eocd = Vec::new();
        write_u32(&mut eocd, 0x0605_4b50);
        write_u16(&mut eocd, 0);
        write_u16(&mut eocd, 0);
        write_u16(&mut eocd, entries.len() as u16);
        write_u16(&mut eocd, entries.len() as u16);
        write_u32(&mut eocd, central.len() as u32);
        write_u32(&mut eocd, offset);
        write_u16(&mut eocd, 0);
        file.write_all(&eocd).unwrap();
    }

    #[derive(Default)]
    struct RecordingBridge {
        started: Mutex<Vec<PluginMcpServerSpec>>,
        stopped: Arc<Mutex<Vec<String>>>,
    }

    struct RecordingHandle {
        identity: String,
        stopped: Arc<Mutex<Vec<String>>>,
    }

    #[async_trait]
    impl ExternalMcpServerHandle for RecordingHandle {
        async fn stop(&mut self) -> Result<(), PluginMcpError> {
            self.stopped.lock().unwrap().push(self.identity.clone());
            Ok(())
        }
    }

    #[async_trait]
    impl ExternalMcpServerBridge for RecordingBridge {
        async fn start(
            &self,
            spec: &PluginMcpServerSpec,
        ) -> Result<Box<dyn ExternalMcpServerHandle>, PluginMcpError> {
            assert_eq!(spec.transport, "stdio");
            self.started.lock().unwrap().push(spec.clone());
            Ok(Box::new(RecordingHandle {
                identity: spec.identity.clone(),
                stopped: Arc::clone(&self.stopped),
            }))
        }
    }

    fn mcp_spec(plugin_id: &str, server_id: &str) -> PluginMcpServerSpec {
        let plugin_id = PluginId::new(plugin_id).unwrap();
        let server_id = domain::PluginMcpServerId::new(server_id).unwrap();
        PluginMcpServerSpec {
            identity: format!("plugin:{}:{}", plugin_id.as_str(), server_id.as_str()),
            plugin_id,
            server_id,
            display_name: "Plugin Tools".to_owned(),
            command: "/plugin/servers/tool".to_owned(),
            args: vec!["--stdio".to_owned()],
            env: vec![("PLUGIN_ROOT".to_owned(), "/plugin".to_owned())],
            cwd: "/plugin".to_owned(),
            transport: "stdio".to_owned(),
        }
    }

    #[tokio::test]
    async fn installs_directory_snapshot_and_uninstalls_cleanly() {
        let app = temp_dir("app");
        let source = temp_dir("source");
        write_plugin(&source, "one");
        let store = FsPluginPackageStore::new(&app);
        let staged = store
            .install_from_directory(&LocalPath::new(source.to_string_lossy()))
            .await
            .unwrap();
        assert_ne!(staged.root, source.to_string_lossy());
        let id = PluginId::new("dev.acme.test").unwrap();
        let package = store.commit_install(staged, &id).await.unwrap();
        assert!(PathBuf::from(package.root).join(MANIFEST_FILE).exists());
        assert_eq!(
            store.remove_package(&id).await.unwrap(),
            RemovalOutcome::Removed
        );
        assert!(!app.join("plugins/installed/dev.acme.test").exists());
        let _ = fs::remove_dir_all(app);
        let _ = fs::remove_dir_all(source);
    }

    #[tokio::test]
    async fn content_hash_changes_when_bundle_changes() {
        let app = temp_dir("hash-app");
        let source = temp_dir("hash-source");
        write_plugin(&source, "one");
        let store = FsPluginPackageStore::new(&app);
        let first = store
            .install_from_directory(&LocalPath::new(source.to_string_lossy()))
            .await
            .unwrap()
            .content_hash;
        fs::write(source.join("dist/index.js"), "two").unwrap();
        let second = store
            .install_from_directory(&LocalPath::new(source.to_string_lossy()))
            .await
            .unwrap()
            .content_hash;
        assert_ne!(first, second);
        let _ = fs::remove_dir_all(app);
        let _ = fs::remove_dir_all(source);
    }

    #[tokio::test]
    async fn extracts_archive_without_path_escape() {
        let app = temp_dir("archive-app");
        let archive_path = app.join("plugin.ideaplug");
        write_zip(
            &archive_path,
            &[
                ZipEntrySpec {
                    name: "idea-plugin.json",
                    contents: br#"{"ideaPluginManifestVersion":1,"id":"dev.acme.test","displayName":"Test","version":"1.0.0","main":"dist/index.js","trustLevel":"full","contributes":{}}"#,
                    unix_mode: Some(0o100644),
                },
                ZipEntrySpec {
                    name: "dist/index.js",
                    contents: b"bundle",
                    unix_mode: Some(0o100644),
                },
            ],
        );
        let store = FsPluginPackageStore::new(app.join("data"));
        let staged = store
            .install_from_archive(&LocalPath::new(archive_path.to_string_lossy()))
            .await
            .unwrap();
        assert!(PathBuf::from(staged.root).join(MANIFEST_FILE).exists());
        let _ = fs::remove_dir_all(app);
    }

    #[tokio::test]
    async fn install_from_directory_rejects_source_symlink() {
        let app = temp_dir("symlink-app");
        let source = temp_dir("symlink-source");
        write_plugin(&source, "bundle");
        let outside = app.join("outside.txt");
        fs::write(&outside, "secret").unwrap();
        #[cfg(unix)]
        std::os::unix::fs::symlink(&outside, source.join("dist/escape.txt")).unwrap();
        #[cfg(windows)]
        std::os::windows::fs::symlink_file(&outside, source.join("dist/escape.txt")).unwrap();
        let store = FsPluginPackageStore::new(&app);

        let err = store
            .install_from_directory(&LocalPath::new(source.to_string_lossy()))
            .await
            .unwrap_err();

        assert!(matches!(err, PluginStoreError::Invalid(_)));
        assert!(err.to_string().contains("symlink"));
        assert!(!app.join("plugins/installed/dev.acme.test").exists());
        let _ = fs::remove_dir_all(app);
        let _ = fs::remove_dir_all(source);
    }

    #[tokio::test]
    async fn install_from_archive_rejects_parent_traversal_without_writing_outside_stage() {
        let app = temp_dir("traversal-app");
        let archive_path = app.join("plugin.ideaplug");
        write_zip(
            &archive_path,
            &[
                ZipEntrySpec {
                    name: "../../../../outside.txt",
                    contents: b"pwned",
                    unix_mode: Some(0o100644),
                },
                ZipEntrySpec {
                    name: "idea-plugin.json",
                    contents: br#"{"ideaPluginManifestVersion":1}"#,
                    unix_mode: Some(0o100644),
                },
            ],
        );
        let store = FsPluginPackageStore::new(&app);

        let err = store
            .install_from_archive(&LocalPath::new(archive_path.to_string_lossy()))
            .await
            .unwrap_err();

        assert!(matches!(err, PluginStoreError::Invalid(_)));
        assert!(err.to_string().contains("escapes plugin root"));
        assert!(!app.join("outside.txt").exists());
        let _ = fs::remove_dir_all(app);
    }

    #[tokio::test]
    async fn install_from_archive_rejects_symlink_entries() {
        let app = temp_dir("archive-symlink-app");
        let archive_path = app.join("plugin.ideaplug");
        write_zip(
            &archive_path,
            &[
                ZipEntrySpec {
                    name: "idea-plugin.json",
                    contents: br#"{"ideaPluginManifestVersion":1,"id":"dev.acme.test","displayName":"Test","version":"1.0.0","main":"dist/index.js","trustLevel":"full","contributes":{}}"#,
                    unix_mode: Some(0o100644),
                },
                ZipEntrySpec {
                    name: "dist/link.js",
                    contents: b"/tmp/outside.js",
                    unix_mode: Some(0o120777),
                },
            ],
        );
        let store = FsPluginPackageStore::new(&app);

        let err = store
            .install_from_archive(&LocalPath::new(archive_path.to_string_lossy()))
            .await
            .unwrap_err();

        assert!(matches!(err, PluginStoreError::Invalid(_)));
        assert!(err.to_string().contains("symlinks are not allowed"));
        assert!(!app.join("plugins/installed/dev.acme.test").exists());
        let _ = fs::remove_dir_all(app);
    }

    #[tokio::test]
    async fn plugin_storage_store_round_trips_deletes_and_purges_plugin_data() {
        let app = temp_dir("storage-app");
        let store = FsPluginStorageStore::new(&app);
        let plugin_id = PluginId::new("dev.acme.test").unwrap();

        assert_eq!(
            store.get(&plugin_id, "helloPlugin.launches").await.unwrap(),
            None
        );
        store
            .set(
                &plugin_id,
                "helloPlugin.launches",
                serde_json::json!({"count": 1}),
            )
            .await
            .unwrap();
        assert_eq!(
            store.get(&plugin_id, "helloPlugin.launches").await.unwrap(),
            Some(serde_json::json!({"count": 1}))
        );
        assert!(app.join("plugins/data/dev.acme.test/entries").is_dir());

        assert!(store
            .delete(&plugin_id, "helloPlugin.launches")
            .await
            .unwrap());
        assert_eq!(
            store.get(&plugin_id, "helloPlugin.launches").await.unwrap(),
            None
        );
        store
            .set(&plugin_id, "helloPlugin.enabled", serde_json::json!(true))
            .await
            .unwrap();
        assert_eq!(
            store.purge_plugin(&plugin_id).await.unwrap(),
            RemovalOutcome::Removed
        );
        assert!(!app.join("plugins/data/dev.acme.test").exists());

        let _ = fs::remove_dir_all(app);
    }

    #[tokio::test]
    async fn supervisor_delegates_stdio_servers_to_external_mcp_bridge() {
        let bridge = Arc::new(RecordingBridge::default());
        let supervisor = ExternalMcpPluginSupervisor::with_bridge(bridge.clone());
        let spec = mcp_spec("dev.acme.gitgraph", "dev.acme.gitgraph.mcp");

        let statuses = supervisor.reconcile(vec![spec.clone()]).await.unwrap();

        assert_eq!(statuses.servers.len(), 1);
        assert_eq!(statuses.servers[0].identity, spec.identity);
        assert!(statuses.servers[0].running);
        assert_eq!(&*bridge.started.lock().unwrap(), &[spec]);
    }

    #[tokio::test]
    async fn supervisor_stop_plugin_stops_only_matching_plugin_servers() {
        let bridge = Arc::new(RecordingBridge::default());
        let supervisor = ExternalMcpPluginSupervisor::with_bridge(bridge.clone());
        let target = mcp_spec("dev.acme.gitgraph", "dev.acme.gitgraph.mcp");
        let other = mcp_spec("dev.other.tools", "dev.other.tools.mcp");
        supervisor
            .reconcile(vec![target.clone(), other.clone()])
            .await
            .unwrap();

        supervisor.stop_plugin(&target.plugin_id).await.unwrap();

        assert_eq!(&*bridge.stopped.lock().unwrap(), &[target.identity.clone()]);
        let statuses = supervisor.reconcile(vec![other.clone()]).await.unwrap();
        assert_eq!(statuses.servers.len(), 1);
        assert_eq!(statuses.servers[0].identity, other.identity);
        assert_eq!(bridge.started.lock().unwrap().len(), 2);
    }

    #[tokio::test]
    async fn supervisor_poisoned_registry_returns_typed_error() {
        let supervisor = ExternalMcpPluginSupervisor::new();
        let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
            let _guard = supervisor.children.lock().unwrap();
            panic!("poison plugin mcp supervisor registry");
        }));

        let err = supervisor.reconcile(Vec::new()).await.unwrap_err();

        assert_eq!(
            err,
            PluginMcpError::Process("plugin mcp supervisor mutex poisoned".to_owned())
        );
    }
}
