//! PluginRepository — 远程插件仓库客户端
//!
//! 从 HTTP 文件服务器下载插件包 (.tar.gz)，解压安装到 plugin_store/。
//!
//! 远程仓库协议（简单 HTTP 文件服务）：
//! ```text
//! https://plugins.example.com/
//! ├── index.json           # 插件目录
//! ├── custom-sensor/
//! │   ├── latest.json      # {"version": "1.1.0"}
//! │   └── 1.1.0.tar.gz    # 包含 manifest.json + .so
//! ```

use crate::core::plugin_loader::PluginLoader;
use anyhow::{anyhow, Context, Result};
use flate2::read::GzDecoder;
use serde::{Deserialize, Serialize};
use std::io::Read;
use std::path::{Component, Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};

struct TempDirGuard {
    path: PathBuf,
    keep: bool,
}

impl TempDirGuard {
    fn new(path: PathBuf) -> Self {
        Self { path, keep: false }
    }

    fn disarm(&mut self) {
        self.keep = true;
    }
}

impl Drop for TempDirGuard {
    fn drop(&mut self) {
        if !self.keep && self.path.exists() {
            let _ = std::fs::remove_dir_all(&self.path);
        }
    }
}

/// 远程仓库索引
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RepoIndex {
    pub plugins: Vec<RepoPluginEntry>,
}

/// 远程仓库中的插件条目
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RepoPluginEntry {
    pub id: String,
    pub latest_version: String,
    pub description: String,
    #[serde(default)]
    pub versions: Vec<String>,
}

/// 远程仓库最新版本查询响应
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LatestVersion {
    pub version: String,
}

/// 远程插件仓库客户端
pub struct PluginRepository {
    base_url: String,
    loader: PluginLoader,
}

impl PluginRepository {
    pub fn new(base_url: &str, loader: PluginLoader) -> Self {
        let base_url = base_url.trim_end_matches('/').to_string();
        Self { base_url, loader }
    }

    pub fn loader(&self) -> &PluginLoader {
        &self.loader
    }

    /// 获取远程仓库索引
    pub fn fetch_index(&self) -> Result<RepoIndex> {
        let url = format!("{}/index.json", self.base_url);
        let response = ureq::get(&url)
            .call()
            .with_context(|| format!("failed to fetch repo index from {url}"))?;

        let body = response
            .into_string()
            .context("failed to read repo index body")?;

        serde_json::from_str(&body).context("failed to parse repo index")
    }

    /// 检查指定插件是否有新版本
    /// 返回 Some(新版本号) 或 None（已是最新）
    pub fn check_update(&self, plugin_id: &str, current_version: &str) -> Result<Option<String>> {
        let url = format!("{}/{}/latest.json", self.base_url, plugin_id);
        let response = ureq::get(&url)
            .call()
            .with_context(|| format!("failed to check update for '{plugin_id}'"))?;

        let body = response.into_string()?;
        let latest: LatestVersion = serde_json::from_str(&body)?;

        let current = semver::Version::parse(current_version)
            .with_context(|| format!("invalid current version: {current_version}"))?;
        let remote = semver::Version::parse(&latest.version)
            .with_context(|| format!("invalid remote version: {}", latest.version))?;

        if remote > current {
            Ok(Some(latest.version))
        } else {
            Ok(None)
        }
    }

    /// 下载并安装插件到 plugin_store/
    pub fn download_and_install(&self, plugin_id: &str, version: &str) -> Result<()> {
        let url = format!("{}/{}/{}.tar.gz", self.base_url, plugin_id, version);
        println!("[PluginRepo] 下载插件 '{plugin_id}' v{version} 从 {url}");

        let response = ureq::get(&url)
            .call()
            .with_context(|| format!("failed to download {url}"))?;

        // 读取响应体
        let mut body = Vec::new();
        response
            .into_reader()
            .read_to_end(&mut body)
            .context("failed to read download body")?;

        self.install_archive_bytes(plugin_id, version, &body)
    }

    fn install_archive_bytes(&self, plugin_id: &str, version: &str, body: &[u8]) -> Result<()> {
        let plugin_dir = self.loader.store_path().join(plugin_id);

        std::fs::create_dir_all(&plugin_dir)
            .with_context(|| format!("failed to create {}", plugin_dir.display()))?;

        let target_dir = plugin_dir.join(version);

        if target_dir.exists() {
            return Err(anyhow!(
                "version {version} already installed for plugin '{plugin_id}'"
            ));
        }

        let staging_dir = self.staging_dir_path(&plugin_dir, version);
        std::fs::create_dir_all(&staging_dir)
            .with_context(|| format!("failed to create {}", staging_dir.display()))?;
        let mut cleanup = TempDirGuard::new(staging_dir.clone());

        self.extract_archive_securely(body, &staging_dir)?;

        // 验证 manifest.json 存在
        let manifest_path = staging_dir.join("manifest.json");
        if !manifest_path.is_file() {
            return Err(anyhow!(
                "downloaded archive for '{plugin_id}' v{version} missing manifest.json"
            ));
        }

        std::fs::rename(&staging_dir, &target_dir).with_context(|| {
            format!(
                "failed to atomically move extracted plugin from {} to {}",
                staging_dir.display(),
                target_dir.display()
            )
        })?;
        cleanup.disarm();

        println!(
            "[PluginRepo] 插件 '{plugin_id}' v{version} 安装成功到 {}",
            target_dir.display()
        );
        Ok(())
    }

    fn staging_dir_path(&self, plugin_dir: &Path, version: &str) -> PathBuf {
        let nanos = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap_or_default()
            .as_nanos();
        plugin_dir.join(format!(".{version}.tmp-{}-{nanos}", std::process::id()))
    }

    fn extract_archive_securely(&self, body: &[u8], target_dir: &Path) -> Result<()> {
        let gz = GzDecoder::new(body);
        let mut archive = tar::Archive::new(gz);
        let canonical_target = std::fs::canonicalize(target_dir)
            .with_context(|| format!("failed to canonicalize {}", target_dir.display()))?;

        for entry in archive
            .entries()
            .context("failed to read archive entries")?
        {
            let mut entry = entry.context("failed to read archive entry")?;
            let entry_path = entry.path().context("failed to read archive entry path")?;
            let entry_path = entry_path.into_owned();

            Self::validate_archive_path(&entry_path)?;

            let entry_type = entry.header().entry_type();
            if entry_type.is_symlink() || entry_type.is_hard_link() {
                return Err(anyhow!(
                    "archive entry '{}' uses unsupported link type",
                    entry_path.display()
                ));
            }

            let destination = target_dir.join(&entry_path);

            if entry_type.is_dir() {
                std::fs::create_dir_all(&destination).with_context(|| {
                    format!("failed to create directory {}", destination.display())
                })?;
                Self::ensure_path_within_root(&canonical_target, &destination)?;
                continue;
            }

            if !entry_type.is_file() {
                return Err(anyhow!(
                    "archive entry '{}' has unsupported type {:?}",
                    entry_path.display(),
                    entry_type
                ));
            }

            let parent = destination.parent().ok_or_else(|| {
                anyhow!(
                    "archive entry '{}' has no valid parent directory",
                    entry_path.display()
                )
            })?;
            std::fs::create_dir_all(parent)
                .with_context(|| format!("failed to create {}", parent.display()))?;
            Self::ensure_path_within_root(&canonical_target, parent)?;

            entry.unpack(&destination).with_context(|| {
                format!(
                    "failed to unpack archive entry to {}",
                    destination.display()
                )
            })?;
        }

        Ok(())
    }

    fn validate_archive_path(path: &Path) -> Result<()> {
        if path.as_os_str().is_empty() {
            return Err(anyhow!("archive contains empty path"));
        }

        for component in path.components() {
            match component {
                Component::Normal(_) | Component::CurDir => {}
                Component::ParentDir => {
                    return Err(anyhow!(
                        "archive entry '{}' attempts path traversal",
                        path.display()
                    ));
                }
                Component::RootDir | Component::Prefix(_) => {
                    return Err(anyhow!(
                        "archive entry '{}' uses absolute path",
                        path.display()
                    ));
                }
            }
        }

        Ok(())
    }

    fn ensure_path_within_root(root: &Path, path: &Path) -> Result<()> {
        let canonical_path = std::fs::canonicalize(path)
            .with_context(|| format!("failed to canonicalize {}", path.display()))?;

        if canonical_path.starts_with(root) {
            Ok(())
        } else {
            Err(anyhow!(
                "archive entry resolves outside extraction root: {}",
                canonical_path.display()
            ))
        }
    }

    /// 批量检查所有已安装插件的更新
    pub fn check_all_updates(&self) -> Result<Vec<(String, String, String)>> {
        // (plugin_id, current_version, new_version)
        let registry = self.loader.load_registry()?;
        let mut updates = Vec::new();

        for (plugin_id, entry) in &registry.plugins {
            match self.check_update(plugin_id, &entry.active_version) {
                Ok(Some(new_version)) => {
                    updates.push((plugin_id.clone(), entry.active_version.clone(), new_version));
                }
                Ok(None) => {}
                Err(e) => {
                    eprintln!("[PluginRepo] 检查 '{plugin_id}' 更新失败: {e}");
                }
            }
        }

        Ok(updates)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use flate2::write::GzEncoder;
    use flate2::Compression;
    use std::fs;
    use tar::{Builder, EntryType, Header};

    fn unique_test_dir(name: &str) -> PathBuf {
        let nanos = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .expect("system time should be after unix epoch")
            .as_nanos();
        std::env::temp_dir().join(format!(
            "showen_plugin_repo_{name}_{}_{}",
            std::process::id(),
            nanos
        ))
    }

    fn gzip_bytes(bytes: &[u8]) -> Vec<u8> {
        let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
        std::io::Write::write_all(&mut encoder, bytes).expect("gzip encoder should accept bytes");
        encoder.finish().expect("gzip encoder should finish")
    }

    fn build_plain_tar(entries: Vec<(&str, &[u8])>) -> Vec<u8> {
        let mut builder = Builder::new(Vec::new());

        for (path, contents) in entries {
            let mut header = Header::new_gnu();
            header.set_entry_type(EntryType::Regular);
            header.set_mode(0o644);
            header.set_size(contents.len() as u64);
            header.set_cksum();
            builder
                .append_data(&mut header, path, contents)
                .expect("archive entry should be appended");
        }

        builder.finish().expect("archive builder should finish");
        builder.into_inner().expect("tar bytes should be returned")
    }

    fn build_archive(entries: Vec<(&str, &[u8])>) -> Vec<u8> {
        let tar_bytes = build_plain_tar(entries);
        gzip_bytes(&tar_bytes)
    }

    fn build_symlink_archive(path: &str, target: &str) -> Vec<u8> {
        let mut builder = Builder::new(Vec::new());
        let mut header = Header::new_gnu();
        header.set_entry_type(EntryType::Symlink);
        header.set_mode(0o777);
        header.set_size(0);
        header.set_cksum();
        builder
            .append_link(&mut header, path, target)
            .expect("symlink entry should be appended");
        builder.finish().expect("archive builder should finish");
        let tar_bytes = builder.into_inner().expect("tar bytes should be returned");
        gzip_bytes(&tar_bytes)
    }

    fn build_traversal_archive() -> Vec<u8> {
        fn write_octal(field: &mut [u8], value: u64) {
            field.fill(0);
            let octal = format!("{:0width$o}\0", value, width = field.len() - 1);
            field[..octal.len()].copy_from_slice(octal.as_bytes());
        }

        let contents = br#"{}"#;
        let mut tar_bytes = vec![0u8; 2048];
        let (header, rest) = tar_bytes.split_at_mut(512);
        let path = b"../manifest.json";
        header[..path.len()].copy_from_slice(path);
        write_octal(&mut header[100..108], 0o644);
        write_octal(&mut header[108..116], 0);
        write_octal(&mut header[116..124], 0);
        write_octal(&mut header[124..136], contents.len() as u64);
        write_octal(&mut header[136..148], 0);
        header[148..156].fill(b' ');
        header[156] = b'0';
        header[257..263].copy_from_slice(b"ustar\0");
        header[263..265].copy_from_slice(b"00");

        {
            let data = &mut rest[..512];
            data[..contents.len()].copy_from_slice(contents);
        }

        let checksum = header.iter().map(|byte| *byte as u32).sum::<u32>();
        let checksum_field = format!("{:06o}\0 ", checksum);
        header[148..156].copy_from_slice(checksum_field.as_bytes());

        gzip_bytes(&tar_bytes)
    }

    #[test]
    fn install_archive_rejects_path_traversal_and_cleans_staging_dir() {
        let store = unique_test_dir("traversal");
        fs::create_dir_all(&store).expect("store dir should be created");

        let loader = PluginLoader::new(&store);
        let repo = PluginRepository::new("https://plugins.example.com", loader);
        let archive = build_traversal_archive();

        let err = repo
            .install_archive_bytes("sensor", "1.0.0", &archive)
            .expect_err("path traversal archive should be rejected");
        assert!(err.to_string().contains("path traversal"));

        let plugin_dir = store.join("sensor");
        let entries = fs::read_dir(&plugin_dir)
            .expect("plugin dir should exist")
            .collect::<std::result::Result<Vec<_>, _>>()
            .expect("plugin dir entries should be readable");
        assert!(entries.is_empty());
        assert!(!plugin_dir.join("1.0.0").exists());

        let _ = fs::remove_dir_all(&store);
    }

    #[test]
    fn install_archive_rejects_symlinks() {
        let store = unique_test_dir("symlink");
        fs::create_dir_all(&store).expect("store dir should be created");

        let loader = PluginLoader::new(&store);
        let repo = PluginRepository::new("https://plugins.example.com", loader);
        let archive = build_symlink_archive("manifest.json", "/etc/passwd");

        let err = repo
            .install_archive_bytes("sensor", "1.0.0", &archive)
            .expect_err("symlink archive should be rejected");
        assert!(err.to_string().contains("unsupported link type"));
        assert!(!store.join("sensor").join("1.0.0").exists());

        let _ = fs::remove_dir_all(&store);
    }

    #[test]
    fn install_archive_extracts_to_staging_then_moves_into_place() {
        let store = unique_test_dir("valid");
        fs::create_dir_all(&store).expect("store dir should be created");

        let loader = PluginLoader::new(&store);
        let repo = PluginRepository::new("https://plugins.example.com", loader);
        let archive = build_archive(vec![
            (
                "manifest.json",
                br#"{"id":"sensor","version":"1.0.0","sdk_version":"0.2.0","so_filename":"libsensor.so"}"#,
            ),
            ("libsensor.so", b"fake so bytes"),
        ]);

        repo.install_archive_bytes("sensor", "1.0.0", &archive)
            .expect("valid archive should install");

        let target_dir = store.join("sensor").join("1.0.0");
        assert!(target_dir.join("manifest.json").is_file());
        assert!(target_dir.join("libsensor.so").is_file());

        let leftover_tmp = fs::read_dir(store.join("sensor"))
            .expect("plugin dir should be readable")
            .collect::<std::result::Result<Vec<_>, _>>()
            .expect("plugin dir entries should be readable")
            .into_iter()
            .any(|entry| entry.file_name().to_string_lossy().contains(".tmp-"));
        assert!(!leftover_tmp);

        let _ = fs::remove_dir_all(&store);
    }
}
