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

use dashmap::DashMap;
use serde::{Deserialize, Serialize};
use tracing::warn;

use crate::actors::ActorRegistry;
use crate::cache::NclCache;
use crate::notifications::NotificationStore;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Role {
    Admin,
    Viewer,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeyEntry {
    pub role: Role,
    /// Argon2id PHC string — produced by `ontoref-daemon --hash-password <pw>`
    pub hash: String,
}

/// Serialisable entry used for TOML read/write.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegistryEntry {
    pub slug: String,
    pub root: PathBuf,
    #[serde(default)]
    pub keys: Vec<KeyEntry>,
}

#[derive(Debug, Serialize, Deserialize)]
struct RegistryFile {
    projects: Vec<RegistryEntry>,
}

/// Per-project runtime state owned by the registry.
pub struct ProjectContext {
    pub slug: String,
    pub root: PathBuf,
    pub import_path: Option<String>,
    pub cache: Arc<NclCache>,
    pub actors: Arc<ActorRegistry>,
    pub notifications: Arc<NotificationStore>,
    /// Stored for TOML serialisation round-trips.
    pub keys: Vec<KeyEntry>,
}

impl ProjectContext {
    pub fn auth_enabled(&self) -> bool {
        !self.keys.is_empty()
    }

    /// Returns the role of the first key whose argon2id hash matches
    /// `password`.
    pub fn verify_key(&self, password: &str) -> Option<Role> {
        use argon2::{Argon2, PasswordHash, PasswordVerifier};
        for key in &self.keys {
            let Ok(parsed) = PasswordHash::new(&key.hash) else {
                continue;
            };
            if Argon2::default()
                .verify_password(password.as_bytes(), &parsed)
                .is_ok()
            {
                return Some(key.role);
            }
        }
        None
    }
}

pub struct ProjectRegistry {
    contexts: DashMap<String, Arc<ProjectContext>>,
    pub path: PathBuf,
    stale_actor_timeout: u64,
    max_notifications: usize,
}

impl ProjectRegistry {
    /// Load and parse a TOML registry file, creating per-project runtime state.
    pub fn load(
        path: &Path,
        stale_actor_timeout: u64,
        max_notifications: usize,
    ) -> anyhow::Result<Self> {
        let registry = Self {
            contexts: DashMap::new(),
            path: path.to_path_buf(),
            stale_actor_timeout,
            max_notifications,
        };
        registry.reload_from_file(path)?;
        Ok(registry)
    }

    /// Hot-reload from the registry file; preserves existing caches for
    /// unchanged slugs.
    pub fn reload(&self) -> anyhow::Result<()> {
        self.reload_from_file(&self.path.clone())
    }

    fn reload_from_file(&self, path: &Path) -> anyhow::Result<()> {
        let contents = std::fs::read_to_string(path)?;
        let file: RegistryFile = toml::from_str(&contents)?;

        let new_slugs: std::collections::HashSet<String> =
            file.projects.iter().map(|p| p.slug.clone()).collect();

        // Remove projects no longer in the file.
        self.contexts
            .retain(|slug, _| new_slugs.contains(slug.as_str()));

        for entry in file.projects {
            let root = entry
                .root
                .canonicalize()
                .map_err(|e| anyhow::anyhow!("project '{}': root path error: {}", entry.slug, e))?;

            if let Some(existing) = self.contexts.get(&entry.slug) {
                // Project already loaded — update keys only, reuse warm cache/actors.
                if existing.root == root {
                    // Re-insert a new Arc with updated keys but same internals.
                    let ctx = ProjectContext {
                        slug: existing.slug.clone(),
                        root: existing.root.clone(),
                        import_path: existing.import_path.clone(),
                        cache: Arc::clone(&existing.cache),
                        actors: Arc::clone(&existing.actors),
                        notifications: Arc::clone(&existing.notifications),
                        keys: entry.keys,
                    };
                    drop(existing);
                    self.contexts.insert(entry.slug, Arc::new(ctx));
                    continue;
                }
                drop(existing);
            }

            // New project — create fresh context.
            let import_path = load_import_path(&root);
            let ctx = make_context(
                entry.slug.clone(),
                root,
                import_path,
                entry.keys,
                self.stale_actor_timeout,
                self.max_notifications,
            );
            self.contexts.insert(entry.slug, Arc::new(ctx));
        }
        Ok(())
    }

    /// Add a project at runtime and persist to the TOML file.
    pub fn add_project(&self, entry: RegistryEntry) -> anyhow::Result<()> {
        if self.contexts.contains_key(&entry.slug) {
            anyhow::bail!("project '{}' already exists", entry.slug);
        }
        let root = entry
            .root
            .canonicalize()
            .map_err(|e| anyhow::anyhow!("project '{}': root path error: {}", entry.slug, e))?;
        let import_path = load_import_path(&root);
        let ctx = make_context(
            entry.slug.clone(),
            root,
            import_path,
            entry.keys,
            self.stale_actor_timeout,
            self.max_notifications,
        );
        self.contexts.insert(entry.slug, Arc::new(ctx));
        self.write_toml()
    }

    /// Remove a project by slug and persist to the TOML file.
    pub fn remove_project(&self, slug: &str) -> anyhow::Result<()> {
        self.contexts.remove(slug);
        self.write_toml()
    }

    /// Serialise current in-memory state back to the registry TOML file.
    pub fn write_toml(&self) -> anyhow::Result<()> {
        let mut projects: Vec<RegistryEntry> = self
            .contexts
            .iter()
            .map(|r| RegistryEntry {
                slug: r.slug.clone(),
                root: r.root.clone(),
                keys: r.keys.clone(),
            })
            .collect();
        projects.sort_by(|a, b| a.slug.cmp(&b.slug));

        let file = RegistryFile { projects };
        let toml = toml::to_string_pretty(&file)?;

        if let Some(parent) = self.path.parent() {
            std::fs::create_dir_all(parent)?;
        }
        std::fs::write(&self.path, toml)?;
        Ok(())
    }

    pub fn get(&self, slug: &str) -> Option<Arc<ProjectContext>> {
        self.contexts.get(slug).map(|r| Arc::clone(&*r))
    }

    pub fn all(&self) -> Vec<Arc<ProjectContext>> {
        let mut list: Vec<_> = self.contexts.iter().map(|r| Arc::clone(&*r)).collect();
        list.sort_by(|a, b| a.slug.cmp(&b.slug));
        list
    }

    pub fn count(&self) -> usize {
        self.contexts.len()
    }
}

fn make_context(
    slug: String,
    root: PathBuf,
    import_path: Option<String>,
    keys: Vec<KeyEntry>,
    stale_actor_timeout: u64,
    max_notifications: usize,
) -> ProjectContext {
    let sessions_dir = root.join(".ontoref").join("sessions");
    let actors = Arc::new(ActorRegistry::new(stale_actor_timeout).with_persist_dir(sessions_dir));
    actors.load_persisted();

    ProjectContext {
        slug,
        root,
        import_path,
        cache: Arc::new(NclCache::new()),
        actors,
        notifications: Arc::new(NotificationStore::new(
            max_notifications,
            vec![".ontology".into(), "adrs".into()],
        )),
        keys,
    }
}

fn load_import_path(root: &Path) -> Option<String> {
    use std::process::Command;
    let config_path = root.join(".ontoref").join("config.ncl");
    if !config_path.exists() {
        return None;
    }
    let output = Command::new("nickel")
        .arg("export")
        .arg(&config_path)
        .output()
        .ok()?;
    if !output.status.success() {
        warn!(path = %config_path.display(), "nickel export failed for registry project config");
        return None;
    }
    let json: serde_json::Value = serde_json::from_slice(&output.stdout).ok()?;
    let paths = json.get("nickel_import_paths")?.as_array()?;
    // Resolve each path against the project root so that relative entries like
    // "." or "reflection/schemas" work correctly in multi-project mode where the
    // daemon CWD is not the project root.
    let joined = paths
        .iter()
        .filter_map(|v| v.as_str())
        .map(|p| {
            let candidate = std::path::Path::new(p);
            if candidate.is_absolute() {
                p.to_string()
            } else {
                root.join(candidate).display().to_string()
            }
        })
        .collect::<Vec<_>>()
        .join(":");
    if joined.is_empty() {
        None
    } else {
        Some(joined)
    }
}
