//! Registry catalog: loads manifests from disk, provides list/search/resolve operations.

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

use crate::registry::embedded;
use crate::registry::manifest::{BundleDefinition, BundlesFile, ExtensionManifest, ManifestKind};

/// Error type for registry operations.
#[derive(Debug, thiserror::Error)]
pub enum RegistryError {
    #[error("Registry directory not found: {0}")]
    DirectoryNotFound(PathBuf),

    #[error("Failed to read manifest {path}: {reason}")]
    ManifestRead { path: PathBuf, reason: String },

    #[error("Failed to parse manifest {path}: {reason}")]
    ManifestParse { path: PathBuf, reason: String },

    #[error("Extension not found: {0}")]
    ExtensionNotFound(String),

    #[error("'{name}' already installed at {path}. Use --force to overwrite.")]
    AlreadyInstalled {
        name: String,
        path: std::path::PathBuf,
    },

    // `url` is stored for programmatic access (logs, retries) but intentionally
    // omitted from the Display message to avoid leaking internal artifact URLs
    // to end users.
    #[error("Artifact download failed: {reason}")]
    DownloadFailed { url: String, reason: String },

    #[error("Invalid extension manifest for '{name}' field '{field}': {reason}")]
    InvalidManifest {
        name: String,
        field: &'static str,
        reason: String,
    },

    #[error("Checksum verification failed: expected {expected_sha256}, got {actual_sha256}")]
    ChecksumMismatch {
        url: String,
        expected_sha256: String,
        actual_sha256: String,
    },

    #[error("Missing SHA256 checksum for '{name}' artifact. Use --build to build from source.")]
    MissingChecksum { name: String },

    #[error(
        "Source fallback unavailable for '{name}' after artifact install failed. Retry artifact download or run from a repository checkout."
    )]
    SourceFallbackUnavailable {
        name: String,
        source_dir: PathBuf,
        artifact_error: Box<RegistryError>,
    },

    #[error("Artifact install and source fallback both failed for '{name}'.")]
    InstallFallbackFailed {
        name: String,
        artifact_error: Box<RegistryError>,
        source_error: Box<RegistryError>,
    },

    #[error(
        "Ambiguous name '{name}': exists as both {kind_a} and {kind_b}. Use '{prefix_a}/{name}' or '{prefix_b}/{name}'."
    )]
    AmbiguousName {
        name: String,
        kind_a: &'static str,
        prefix_a: &'static str,
        kind_b: &'static str,
        prefix_b: &'static str,
    },

    #[error("Bundle not found: {0}")]
    BundleNotFound(String),

    #[error("Failed to read bundles file: {0}")]
    BundlesRead(String),

    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
}

/// Central catalog loaded from the `registry/` directory.
#[derive(Debug, Clone)]
pub struct RegistryCatalog {
    /// All loaded manifests, keyed by "<kind>/<name>" (e.g. "tools/github").
    manifests: HashMap<String, ExtensionManifest>,

    /// Bundle definitions from `_bundles.json`.
    bundles: HashMap<String, BundleDefinition>,

    /// Root directory of the registry.
    root: PathBuf,
}

impl RegistryCatalog {
    /// Find the `registry/` directory by searching relative to cwd, the executable,
    /// and `CARGO_MANIFEST_DIR`. Returns `None` if the directory cannot be found
    /// (non-fatal at startup).
    pub fn find_dir() -> Option<PathBuf> {
        // Try relative to current directory (for dev usage)
        if let Ok(cwd) = std::env::current_dir() {
            let candidate = cwd.join("registry");
            if candidate.is_dir() {
                return Some(candidate);
            }
        }

        // Try relative to executable (covers installed binary, target/debug/, target/release/)
        if let Ok(exe) = std::env::current_exe()
            && let Some(parent) = exe.parent()
        {
            // Walk up to 3 levels: exe dir, parent (target/release -> target), grandparent (-> repo root)
            let mut dir = Some(parent);
            for _ in 0..3 {
                if let Some(d) = dir {
                    let candidate = d.join("registry");
                    if candidate.is_dir() {
                        return Some(candidate);
                    }
                    dir = d.parent();
                }
            }
        }

        // Try CARGO_MANIFEST_DIR (compile-time, works in dev builds)
        let manifest_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
        let candidate = manifest_dir.join("registry");
        if candidate.is_dir() {
            return Some(candidate);
        }

        None
    }

    /// Try to load from disk; if `registry/` cannot be found, fall back to
    /// manifests embedded into the binary at compile time.
    pub fn load_or_embedded() -> Result<Self, RegistryError> {
        if let Some(dir) = Self::find_dir() {
            return Self::load(&dir);
        }

        // Fall back to embedded catalog
        let manifests = embedded::load_embedded();
        let bundles = embedded::load_embedded_bundles();

        tracing::info!(
            "Loaded embedded registry catalog ({} extensions, {} bundles)",
            manifests.len(),
            bundles.len()
        );

        Ok(Self {
            manifests,
            bundles,
            root: PathBuf::new(),
        })
    }

    /// Load the catalog from a registry directory.
    ///
    /// Expects the structure:
    /// ```text
    /// registry/
    /// ├── tools/*.json
    /// ├── channels/*.json
    /// └── _bundles.json
    /// ```
    pub fn load(registry_dir: &Path) -> Result<Self, RegistryError> {
        if !registry_dir.exists() {
            return Err(RegistryError::DirectoryNotFound(registry_dir.to_path_buf()));
        }

        let mut manifests = HashMap::new();

        // Load tools
        let tools_dir = registry_dir.join("tools");
        if tools_dir.is_dir() {
            Self::load_manifests_from_dir(&tools_dir, "tools", &mut manifests)?;
        }

        // Load channels
        let channels_dir = registry_dir.join("channels");
        if channels_dir.is_dir() {
            Self::load_manifests_from_dir(&channels_dir, "channels", &mut manifests)?;
        }

        // Load MCP servers
        let mcp_servers_dir = registry_dir.join("mcp-servers");
        if mcp_servers_dir.is_dir() {
            Self::load_manifests_from_dir(&mcp_servers_dir, "mcp-servers", &mut manifests)?;
        }

        // Load bundles
        let bundles_path = registry_dir.join("_bundles.json");
        let bundles = if bundles_path.is_file() {
            let content = std::fs::read_to_string(&bundles_path).map_err(|e| {
                RegistryError::BundlesRead(format!("{}: {}", bundles_path.display(), e))
            })?;
            let bundles_file: BundlesFile = serde_json::from_str(&content).map_err(|e| {
                RegistryError::BundlesRead(format!("{}: {}", bundles_path.display(), e))
            })?;
            bundles_file.bundles
        } else {
            HashMap::new()
        };

        Ok(Self {
            manifests,
            bundles,
            root: registry_dir.to_path_buf(),
        })
    }

    fn load_manifests_from_dir(
        dir: &Path,
        kind_prefix: &str,
        manifests: &mut HashMap<String, ExtensionManifest>,
    ) -> Result<(), RegistryError> {
        let entries = std::fs::read_dir(dir).map_err(|e| RegistryError::ManifestRead {
            path: dir.to_path_buf(),
            reason: e.to_string(),
        })?;

        for entry in entries {
            let entry = entry.map_err(|e| RegistryError::ManifestRead {
                path: dir.to_path_buf(),
                reason: e.to_string(),
            })?;

            let path = entry.path();
            if !path.is_file() || path.extension().and_then(|e| e.to_str()) != Some("json") {
                continue;
            }

            let content =
                std::fs::read_to_string(&path).map_err(|e| RegistryError::ManifestRead {
                    path: path.clone(),
                    reason: e.to_string(),
                })?;

            let manifest: ExtensionManifest =
                serde_json::from_str(&content).map_err(|e| RegistryError::ManifestParse {
                    path: path.clone(),
                    reason: e.to_string(),
                })?;

            let key = format!("{}/{}", kind_prefix, manifest.name);
            manifests.insert(key, manifest);
        }

        Ok(())
    }

    /// The root directory this catalog was loaded from.
    pub fn root(&self) -> &Path {
        &self.root
    }

    /// Get all manifests.
    pub fn all(&self) -> Vec<&ExtensionManifest> {
        let mut items: Vec<_> = self.manifests.values().collect();
        items.sort_by(|a, b| a.name.cmp(&b.name));
        items
    }

    /// List manifests, optionally filtered by kind and/or tag.
    pub fn list(&self, kind: Option<ManifestKind>, tag: Option<&str>) -> Vec<&ExtensionManifest> {
        let mut results: Vec<_> = self
            .manifests
            .values()
            .filter(|m| kind.is_none_or(|k| m.kind == k))
            .filter(|m| tag.is_none_or(|t| m.tags.iter().any(|mt| mt == t)))
            .collect();
        results.sort_by(|a, b| a.name.cmp(&b.name));
        results
    }

    /// Get a manifest by name. Tries exact key match first ("tools/github"),
    /// then searches by bare name ("github").
    ///
    /// If a bare name matches more than one prefix, returns `None`.
    /// Use a qualified key ("tools/github", "channels/telegram", or
    /// "mcp-servers/notion") to disambiguate.
    pub fn get(&self, name: &str) -> Option<&ExtensionManifest> {
        // Try exact key first
        if let Some(m) = self.manifests.get(name) {
            return Some(m);
        }

        // Try with kind prefix, detecting collisions
        let candidates: Vec<_> = ["tools", "channels", "mcp-servers"]
            .iter()
            .filter_map(|prefix| self.manifests.get(&format!("{}/{}", prefix, name)))
            .collect();

        if candidates.len() == 1 {
            Some(candidates[0])
        } else {
            None // ambiguous or not found
        }
    }

    /// Get a manifest by name, returning a `Result` with an explicit error for
    /// ambiguous bare names.
    pub fn get_strict(&self, name: &str) -> Result<&ExtensionManifest, RegistryError> {
        // Try exact key first
        if let Some(m) = self.manifests.get(name) {
            return Ok(m);
        }

        let prefixes: &[(&str, &str)] = &[
            ("tools", "tool"),
            ("channels", "channel"),
            ("mcp-servers", "mcp_server"),
        ];

        let matches: Vec<_> = prefixes
            .iter()
            .filter(|(prefix, _)| self.manifests.contains_key(&format!("{}/{}", prefix, name)))
            .collect();

        match matches.len() {
            0 => Err(RegistryError::ExtensionNotFound(name.to_string())),
            1 => {
                let (prefix, _) = matches[0];
                let key = format!("{}/{}", prefix, name);
                self.manifests
                    .get(&key)
                    .ok_or_else(|| RegistryError::ExtensionNotFound(name.to_string()))
            }
            _ => {
                let (prefix_a, kind_a) = matches[0];
                let (prefix_b, kind_b) = matches[1];
                Err(RegistryError::AmbiguousName {
                    name: name.to_string(),
                    kind_a,
                    prefix_a,
                    kind_b,
                    prefix_b,
                })
            }
        }
    }

    /// Get the full key ("tools/github", "channels/telegram", or
    /// "mcp-servers/notion") for a manifest.
    pub fn key_for(&self, name: &str) -> Option<String> {
        if self.manifests.contains_key(name) {
            return Some(name.to_string());
        }

        let matches: Vec<String> = ["tools", "channels", "mcp-servers"]
            .iter()
            .filter_map(|prefix| {
                let key = format!("{}/{}", prefix, name);
                if self.manifests.contains_key(&key) {
                    Some(key)
                } else {
                    None
                }
            })
            .collect();

        if matches.len() == 1 {
            matches.into_iter().next()
        } else {
            None // ambiguous or not found
        }
    }

    /// Search manifests by query string (matches name, display_name, description, keywords).
    pub fn search(&self, query: &str) -> Vec<&ExtensionManifest> {
        let query_lower = query.to_lowercase();
        let tokens: Vec<&str> = query_lower.split_whitespace().collect();

        let mut scored: Vec<(&ExtensionManifest, usize)> = self
            .manifests
            .values()
            .filter_map(|m| {
                let score = Self::score_manifest(m, &tokens);
                if score > 0 { Some((m, score)) } else { None }
            })
            .collect();

        scored.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.name.cmp(&b.0.name)));
        scored.into_iter().map(|(m, _)| m).collect()
    }

    fn score_manifest(manifest: &ExtensionManifest, tokens: &[&str]) -> usize {
        let mut score = 0;
        let name_lower = manifest.name.to_lowercase();
        let display_lower = manifest.display_name.to_lowercase();
        let desc_lower = manifest.description.to_lowercase();

        for token in tokens {
            if name_lower == *token {
                score += 10;
            } else if name_lower.contains(token) {
                score += 5;
            }

            if display_lower == *token {
                score += 8;
            } else if display_lower.contains(token) {
                score += 4;
            }

            if desc_lower.contains(token) {
                score += 2;
            }

            for kw in &manifest.keywords {
                if kw.to_lowercase() == *token {
                    score += 6;
                } else if kw.to_lowercase().contains(token) {
                    score += 3;
                }
            }

            for tag in &manifest.tags {
                if tag.to_lowercase() == *token {
                    score += 4;
                }
            }
        }

        score
    }

    /// Get a bundle definition by name.
    pub fn get_bundle(&self, name: &str) -> Option<&BundleDefinition> {
        self.bundles.get(name)
    }

    /// List all bundle names.
    pub fn bundle_names(&self) -> Vec<&str> {
        let mut names: Vec<_> = self.bundles.keys().map(|s| s.as_str()).collect();
        names.sort();
        names
    }

    /// Resolve a bundle into its constituent manifests.
    /// Returns the manifests and any extension keys that couldn't be found.
    pub fn resolve_bundle(
        &self,
        bundle_name: &str,
    ) -> Result<(Vec<&ExtensionManifest>, Vec<String>), RegistryError> {
        let bundle = self
            .bundles
            .get(bundle_name)
            .ok_or_else(|| RegistryError::BundleNotFound(bundle_name.to_string()))?;

        let mut found = Vec::new();
        let mut missing = Vec::new();

        for ext_key in &bundle.extensions {
            if let Some(manifest) = self.manifests.get(ext_key) {
                found.push(manifest);
            } else {
                missing.push(ext_key.clone());
            }
        }

        Ok((found, missing))
    }

    /// Check if a name refers to a bundle rather than an individual extension.
    pub fn is_bundle(&self, name: &str) -> bool {
        self.bundles.contains_key(name)
    }

    /// Resolve a name to either a single manifest or the manifests in a bundle.
    /// Returns (manifests, bundle_definition_if_bundle).
    pub fn resolve(
        &self,
        name: &str,
    ) -> Result<(Vec<&ExtensionManifest>, Option<&BundleDefinition>), RegistryError> {
        // Check bundle first
        if let Some(bundle) = self.bundles.get(name) {
            let (manifests, missing) = self.resolve_bundle(name)?;
            if !missing.is_empty() {
                tracing::warn!(
                    "Bundle '{}' references missing extensions: {:?}",
                    name,
                    missing
                );
            }
            return Ok((manifests, Some(bundle)));
        }

        // Single extension (use get_strict to catch ambiguous bare names)
        let manifest = self.get_strict(name)?;
        Ok((vec![manifest], None))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;

    fn create_test_registry(dir: &Path) {
        let tools_dir = dir.join("tools");
        let channels_dir = dir.join("channels");
        let mcp_dir = dir.join("mcp-servers");
        fs::create_dir_all(&tools_dir).unwrap();
        fs::create_dir_all(&channels_dir).unwrap();
        fs::create_dir_all(&mcp_dir).unwrap();

        fs::write(
            tools_dir.join("slack.json"),
            r#"{
                "name": "slack",
                "display_name": "Slack",
                "kind": "tool",
                "version": "0.1.0",
                "description": "Post messages via Slack API",
                "keywords": ["messaging", "chat"],
                "source": {
                    "dir": "tools-src/slack",
                    "capabilities": "slack-tool.capabilities.json",
                    "crate_name": "slack-tool"
                },
                "auth_summary": {
                    "method": "oauth",
                    "provider": "Slack",
                    "secrets": ["slack_bot_token"]
                },
                "tags": ["default", "messaging"]
            }"#,
        )
        .unwrap();

        fs::write(
            tools_dir.join("github.json"),
            r#"{
                "name": "github",
                "display_name": "GitHub",
                "kind": "tool",
                "version": "0.1.0",
                "description": "GitHub integration for issues and PRs",
                "keywords": ["code", "git"],
                "source": {
                    "dir": "tools-src/github",
                    "capabilities": "github-tool.capabilities.json",
                    "crate_name": "github-tool"
                },
                "tags": ["default", "development"]
            }"#,
        )
        .unwrap();

        fs::write(
            channels_dir.join("telegram.json"),
            r#"{
                "name": "telegram",
                "display_name": "Telegram",
                "kind": "channel",
                "version": "0.1.0",
                "description": "Telegram Bot API channel",
                "source": {
                    "dir": "channels-src/telegram",
                    "capabilities": "telegram.capabilities.json",
                    "crate_name": "telegram-channel"
                },
                "tags": ["messaging"]
            }"#,
        )
        .unwrap();

        fs::write(
            mcp_dir.join("notion.json"),
            r#"{
                "name": "notion",
                "display_name": "Notion",
                "kind": "mcp_server",
                "description": "Connect to Notion for pages and databases",
                "keywords": ["notes", "wiki"],
                "url": "https://mcp.notion.com/mcp",
                "auth": "dcr"
            }"#,
        )
        .unwrap();

        fs::write(
            dir.join("_bundles.json"),
            r#"{
                "bundles": {
                    "default": {
                        "display_name": "Recommended",
                        "extensions": ["tools/slack", "tools/github", "channels/telegram"]
                    },
                    "messaging": {
                        "display_name": "Messaging",
                        "extensions": ["tools/slack", "channels/telegram"],
                        "shared_auth": null
                    }
                }
            }"#,
        )
        .unwrap();
    }

    #[test]
    fn test_load_catalog() {
        let tmp = tempfile::tempdir().unwrap();
        create_test_registry(tmp.path());

        let catalog = RegistryCatalog::load(tmp.path()).unwrap();
        assert_eq!(catalog.all().len(), 4);
    }

    #[test]
    fn test_list_by_kind() {
        let tmp = tempfile::tempdir().unwrap();
        create_test_registry(tmp.path());

        let catalog = RegistryCatalog::load(tmp.path()).unwrap();
        let tools = catalog.list(Some(ManifestKind::Tool), None);
        assert_eq!(tools.len(), 2);

        let channels = catalog.list(Some(ManifestKind::Channel), None);
        assert_eq!(channels.len(), 1);

        let mcp_servers = catalog.list(Some(ManifestKind::McpServer), None);
        assert_eq!(mcp_servers.len(), 1);
    }

    #[test]
    fn test_list_by_tag() {
        let tmp = tempfile::tempdir().unwrap();
        create_test_registry(tmp.path());

        let catalog = RegistryCatalog::load(tmp.path()).unwrap();
        let defaults = catalog.list(None, Some("default"));
        assert_eq!(defaults.len(), 2);

        let messaging = catalog.list(None, Some("messaging"));
        assert_eq!(messaging.len(), 2); // slack (tool) and telegram (channel) both have "messaging" tag
    }

    #[test]
    fn test_get_by_name() {
        let tmp = tempfile::tempdir().unwrap();
        create_test_registry(tmp.path());

        let catalog = RegistryCatalog::load(tmp.path()).unwrap();

        // Full key
        assert!(catalog.get("tools/slack").is_some());
        assert!(catalog.get("mcp-servers/notion").is_some());

        // Bare name
        assert!(catalog.get("slack").is_some());
        assert!(catalog.get("telegram").is_some());
        assert!(catalog.get("notion").is_some());

        // Missing
        assert!(catalog.get("nonexistent").is_none());
    }

    #[test]
    fn test_search() {
        let tmp = tempfile::tempdir().unwrap();
        create_test_registry(tmp.path());

        let catalog = RegistryCatalog::load(tmp.path()).unwrap();

        let results = catalog.search("slack");
        assert_eq!(results.len(), 1);
        assert_eq!(results[0].name, "slack");

        let results = catalog.search("messaging");
        assert!(!results.is_empty());

        let results = catalog.search("nonexistent query");
        assert!(results.is_empty());
    }

    #[test]
    fn test_resolve_bundle() {
        let tmp = tempfile::tempdir().unwrap();
        create_test_registry(tmp.path());

        let catalog = RegistryCatalog::load(tmp.path()).unwrap();

        let (manifests, missing) = catalog.resolve_bundle("default").unwrap();
        assert_eq!(manifests.len(), 3);
        assert!(missing.is_empty());

        assert!(catalog.resolve_bundle("nonexistent").is_err());
    }

    #[test]
    fn test_resolve_single_or_bundle() {
        let tmp = tempfile::tempdir().unwrap();
        create_test_registry(tmp.path());

        let catalog = RegistryCatalog::load(tmp.path()).unwrap();

        // Single extension
        let (manifests, bundle) = catalog.resolve("slack").unwrap();
        assert_eq!(manifests.len(), 1);
        assert!(bundle.is_none());

        // Bundle
        let (manifests, bundle) = catalog.resolve("default").unwrap();
        assert_eq!(manifests.len(), 3);
        assert!(bundle.is_some());
    }

    #[test]
    fn test_bundle_names() {
        let tmp = tempfile::tempdir().unwrap();
        create_test_registry(tmp.path());

        let catalog = RegistryCatalog::load(tmp.path()).unwrap();
        let names = catalog.bundle_names();
        assert_eq!(names, vec!["default", "messaging"]);
    }

    #[test]
    fn test_directory_not_found() {
        let result = RegistryCatalog::load(Path::new("/nonexistent/path"));
        assert!(result.is_err());
    }

    #[test]
    fn test_load_or_embedded_succeeds() {
        // Should always succeed: either finds registry/ on disk or falls back to embedded
        let catalog = RegistryCatalog::load_or_embedded().unwrap();
        // At minimum, the embedded catalog from the repo should have entries
        assert!(!catalog.all().is_empty() || !catalog.bundle_names().is_empty());
    }

    #[test]
    fn test_bundle_entries_resolve_against_real_registry() {
        // Load the actual registry/ directory (catches stale bundle refs after renames)
        let catalog = RegistryCatalog::load_or_embedded().unwrap();

        for bundle_name in catalog.bundle_names() {
            let (manifests, missing) = catalog.resolve_bundle(bundle_name).unwrap();
            assert!(
                missing.is_empty(),
                "Bundle '{}' has unresolved entries: {:?}. \
                 Check that _bundles.json entries match manifest name fields.",
                bundle_name,
                missing
            );
            assert!(
                !manifests.is_empty(),
                "Bundle '{}' resolved to zero manifests",
                bundle_name
            );
        }
    }
}
