//! Local Episteme instance for persistent storage and alias management.
//!
//! Provides ingestion, conflict checking, and auto-alias creation backed by
//! write-ahead log and KV store.

mod queries;
mod store;

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

use ed25519_dalek::SigningKey;
use stemedb_ingest::Ingestor;
use stemedb_storage::{
    GenericAliasStore, GenericPackSourceStore, GenericPredicateAliasStore,
    GenericPredicateIndexStore, HybridStore, KVStore, PredicateAliasStore, StoredPredicateAliasSet,
};
use stemedb_wal::Journal;
use tokio::sync::Mutex;
use tracing::{info, instrument};

use crate::bridge::load_or_generate_key;
use crate::config::AphoriaConfig;
use crate::types::PredicateAliasSet;
use crate::AphoriaError;

/// Local Episteme instance for Aphoria.
pub struct LocalEpisteme {
    pub(super) journal: Arc<Mutex<Journal>>,
    pub(super) store: Arc<HybridStore>, // KV store for assertions
    pub(super) ingestor: Ingestor<HybridStore>,
    pub(super) signing_key: SigningKey,
    pub(super) alias_store: GenericAliasStore<Arc<HybridStore>>,
    pub(super) predicate_index_store: GenericPredicateIndexStore<Arc<HybridStore>>,
    pub(super) pack_source_store: GenericPackSourceStore<Arc<HybridStore>>,
    /// Predicate alias store for persisting semantic predicate equivalences.
    pub(super) predicate_alias_store: GenericPredicateAliasStore<Arc<HybridStore>>,
    /// Predicate aliases from imported Trust Packs (loaded from storage on startup).
    pub(super) predicate_aliases: Vec<PredicateAliasSet>,
    /// Project root directory for git operations.
    pub(super) project_root: PathBuf,
}

impl LocalEpisteme {
    /// Open corpus database (shared across projects).
    ///
    /// This opens a separate database for corpus assertions (RFC, OWASP, etc.)
    /// stored in `~/.aphoria/corpus-db/` instead of the project-local database.
    #[instrument(fields(corpus_data_dir = ?corpus_data_dir))]
    pub async fn open_corpus_db(
        corpus_data_dir: &Path,
        project_root: &Path,
    ) -> Result<Self, AphoriaError> {
        // Expand tilde if present
        let corpus_path = if let Some(path_str) = corpus_data_dir.to_str() {
            if path_str.starts_with('~') {
                let expanded = shellexpand::tilde(path_str).into_owned();
                PathBuf::from(expanded)
            } else {
                corpus_data_dir.to_path_buf()
            }
        } else {
            corpus_data_dir.to_path_buf()
        };

        // Create directory if it doesn't exist
        tokio::fs::create_dir_all(&corpus_path).await.map_err(AphoriaError::Io)?;

        // Canonicalize (required by fjall/lsm-tree)
        let corpus_path = corpus_path.canonicalize().map_err(|e| {
            AphoriaError::Storage(format!("Failed to canonicalize corpus_data_dir: {}", e))
        })?;

        let wal_dir = corpus_path.join("wal");
        std::fs::create_dir_all(&wal_dir)?;

        info!("Opening corpus database at {}", corpus_path.display());

        // Open WAL
        let journal = Arc::new(Mutex::new(Journal::open(&wal_dir).map_err(|e| {
            AphoriaError::Storage(format!(
                "Failed to open corpus WAL at {}: {e}",
                wal_dir.display()
            ))
        })?));

        // Open store (directly at corpus_path, matching API behavior)
        let store = Arc::new(HybridStore::open(&corpus_path).map_err(|e| {
            AphoriaError::Storage(format!(
                "Failed to open corpus store at {}: {e}",
                corpus_path.display()
            ))
        })?);

        // Create ingestor
        let mut ingestor = Ingestor::new(journal.clone(), store.clone())
            .await
            .map_err(|e| AphoriaError::Storage(format!("Failed to create corpus ingestor: {e}")))?;
        ingestor.start();

        // Load or generate signing key (from project root)
        let signing_key = load_or_generate_key(project_root).map_err(|e| {
            AphoriaError::Storage(format!(
                "Failed to load/generate signing key at {}: {e}",
                project_root.display()
            ))
        })?;

        // Create stores
        let alias_store = GenericAliasStore::new(store.clone());
        let predicate_index_store = GenericPredicateIndexStore::new(store.clone());
        let pack_source_store = GenericPackSourceStore::new(store.clone());
        let predicate_alias_store = GenericPredicateAliasStore::new(store.clone());

        // Load predicate aliases
        let stored_aliases =
            predicate_alias_store.list_all_predicate_aliases().await.map_err(|e| {
                AphoriaError::Storage(format!("Failed to load corpus predicate aliases: {e}"))
            })?;
        let predicate_aliases: Vec<PredicateAliasSet> = stored_aliases
            .into_iter()
            .map(|s| PredicateAliasSet::new(s.canonical, s.aliases))
            .collect();

        if !predicate_aliases.is_empty() {
            info!(count = predicate_aliases.len(), "Loaded predicate aliases from corpus storage");
        }

        Ok(Self {
            journal,
            store,
            ingestor,
            signing_key,
            alias_store,
            predicate_index_store,
            pack_source_store,
            predicate_alias_store,
            predicate_aliases,
            project_root: project_root.to_path_buf(),
        })
    }

    /// Open or create a local Episteme instance.
    #[instrument(skip(config), fields(data_dir = %config.episteme.data_dir.display()))]
    pub async fn open(config: &AphoriaConfig, project_root: &Path) -> Result<Self, AphoriaError> {
        let data_dir = &config.episteme.data_dir;

        // Create directories if needed
        std::fs::create_dir_all(data_dir)?;

        // Canonicalize paths (required by fjall/lsm-tree)
        let data_dir = data_dir.canonicalize().map_err(|e| {
            AphoriaError::Storage(format!("Failed to canonicalize data_dir: {}", e))
        })?;

        let wal_dir = data_dir.join("wal");
        let store_dir = data_dir.join("store");
        std::fs::create_dir_all(&wal_dir)?;
        std::fs::create_dir_all(&store_dir)?;

        info!("Opening local Episteme at {}", data_dir.display());

        // Open WAL
        let journal = Arc::new(Mutex::new(Journal::open(&wal_dir).map_err(|e| {
            AphoriaError::Storage(format!("Failed to open WAL at {}: {e}", wal_dir.display()))
        })?));

        // Open store
        let store = Arc::new(HybridStore::open(&store_dir).map_err(|e| {
            AphoriaError::Storage(format!("Failed to open store at {}: {e}", store_dir.display()))
        })?);

        // Create ingestor
        let mut ingestor = Ingestor::new(journal.clone(), store.clone())
            .await
            .map_err(|e| AphoriaError::Storage(format!("Failed to create ingestor: {e}")))?;
        ingestor.start();

        // Load or generate signing key
        let signing_key = load_or_generate_key(project_root).map_err(|e| {
            AphoriaError::Storage(format!(
                "Failed to load/generate signing key at {}: {e}",
                project_root.display()
            ))
        })?;

        // Create alias store for auto-alias persistence
        let alias_store = GenericAliasStore::new(store.clone());

        // Create predicate index store for predicate-based queries
        let predicate_index_store = GenericPredicateIndexStore::new(store.clone());

        // Create pack source store for policy attribution
        let pack_source_store = GenericPackSourceStore::new(store.clone());

        // Create predicate alias store for semantic predicate matching
        let predicate_alias_store = GenericPredicateAliasStore::new(store.clone());

        // Load persisted predicate aliases from storage
        let stored_aliases = predicate_alias_store
            .list_all_predicate_aliases()
            .await
            .map_err(|e| AphoriaError::Storage(format!("Failed to load predicate aliases: {e}")))?;
        let predicate_aliases: Vec<PredicateAliasSet> = stored_aliases
            .into_iter()
            .map(|s| PredicateAliasSet::new(s.canonical, s.aliases))
            .collect();

        if !predicate_aliases.is_empty() {
            info!(count = predicate_aliases.len(), "Loaded predicate aliases from storage");
        }

        Ok(Self {
            journal,
            store,
            ingestor,
            signing_key,
            alias_store,
            predicate_index_store,
            pack_source_store,
            predicate_alias_store,
            predicate_aliases,
            project_root: project_root.to_path_buf(),
        })
    }

    /// Shut down the Episteme instance gracefully.
    pub async fn shutdown(&mut self) {
        info!("Shutting down local Episteme");
        self.ingestor.shutdown(std::time::Duration::from_secs(2)).await;

        // Flush the store to ensure all data is persisted to disk.
        // This is critical for pack_source data written during policy import.
        if let Err(e) = self.store.as_ref().flush().await {
            tracing::warn!(error = %e, "Failed to flush store during shutdown");
        }
    }

    /// Get the signing key's public key bytes for alias creation.
    pub fn agent_id(&self) -> [u8; 32] {
        self.signing_key.verifying_key().to_bytes()
    }

    /// Get a reference to the signing key for creating assertions.
    pub fn signing_key(&self) -> &SigningKey {
        &self.signing_key
    }

    /// Get a reference to the alias store for querying created aliases.
    #[allow(dead_code)]
    pub fn alias_store(&self) -> &GenericAliasStore<Arc<HybridStore>> {
        &self.alias_store
    }

    /// Build authoritative corpus including community patterns.
    ///
    /// This builds the corpus from all configured sources (RFC, OWASP, Vendor)
    /// PLUS community corpus from pattern aggregates (if enabled in config).
    ///
    /// Unlike `create_authoritative_corpus()` which only uses hardcoded sources,
    /// this method uses the real StemeDB stores to query pattern aggregates.
    #[instrument(skip(self, config), fields(use_community = config.use_community))]
    pub async fn build_corpus_with_stores(
        &self,
        config: &crate::config::CorpusConfig,
    ) -> Result<Vec<stemedb_core::types::Assertion>, AphoriaError> {
        use crate::corpus::CorpusRegistry;
        use crate::episteme::current_timestamp;

        info!("Building authoritative corpus with stores");

        // Create registry with all builders including community (if enabled)
        // Note: GenericPredicateIndexStore doesn't implement Clone, so we create a new one
        let predicate_index = Arc::new(GenericPredicateIndexStore::new(self.store.clone()));

        // No corpus_store here - CLI-created items are only needed in explicit corpus builds,
        // not during scans (which use project-local episteme)
        let registry =
            CorpusRegistry::with_stores(config, self.store.clone(), predicate_index, None);

        let timestamp = current_timestamp();

        // Build in offline mode to avoid network delays during scan
        let result = registry.build_all(&self.signing_key, timestamp, config, true).await?;

        info!(
            total = result.total_assertions(),
            successful_builders = result.successful_builders(),
            failed_builders = result.failed_builders(),
            "Corpus build complete"
        );

        Ok(result.assertions)
    }

    /// Get a reference to the underlying KV store.
    ///
    /// Used for direct storage operations like importing policies.
    pub fn store(&self) -> &Arc<HybridStore> {
        &self.store
    }

    /// Get a cloned Arc to the KV store for pattern aggregation.
    pub fn get_kv_store(&self) -> Arc<HybridStore> {
        self.store.clone()
    }

    /// Get a cloned Arc to the predicate index store for pattern aggregation.
    pub fn get_predicate_index(&self) -> Arc<GenericPredicateIndexStore<Arc<HybridStore>>> {
        Arc::new(GenericPredicateIndexStore::new(self.store.clone()))
    }

    /// Get a reference to the pack source store for policy attribution.
    pub fn pack_source_store(&self) -> &GenericPackSourceStore<Arc<HybridStore>> {
        &self.pack_source_store
    }

    /// Get the current predicate aliases from imported Trust Packs.
    pub fn predicate_aliases(&self) -> &[PredicateAliasSet] {
        &self.predicate_aliases
    }

    /// Persist predicate aliases to storage and update in-memory cache.
    ///
    /// This is called during policy import to ensure aliases survive restarts.
    /// Uses merge semantics: if aliases for the same canonical predicate already
    /// exist, the new aliases are added to the existing set.
    ///
    /// # Arguments
    /// * `aliases` - The predicate alias sets to persist
    pub async fn persist_predicate_aliases(
        &mut self,
        aliases: Vec<PredicateAliasSet>,
    ) -> Result<(), AphoriaError> {
        for alias in &aliases {
            let stored = StoredPredicateAliasSet {
                canonical: alias.canonical.clone(),
                aliases: alias.aliases.clone(),
            };
            self.predicate_alias_store.set_predicate_alias_set(&stored).await.map_err(|e| {
                AphoriaError::Storage(format!("Failed to persist predicate alias: {e}"))
            })?;
        }

        // Update in-memory cache (merge with existing)
        for new_alias in aliases {
            if let Some(existing) =
                self.predicate_aliases.iter_mut().find(|a| a.canonical == new_alias.canonical)
            {
                // Merge aliases
                for alias in new_alias.aliases {
                    if !existing.aliases.contains(&alias) {
                        existing.aliases.push(alias);
                    }
                }
            } else {
                self.predicate_aliases.push(new_alias);
            }
        }

        Ok(())
    }

    /// Add predicate aliases from an imported Trust Pack (in-memory only).
    ///
    /// Deprecated: Use `persist_predicate_aliases` instead to ensure aliases
    /// survive restarts.
    #[deprecated(note = "Use persist_predicate_aliases instead")]
    #[allow(dead_code)]
    pub fn add_predicate_aliases(&mut self, aliases: Vec<PredicateAliasSet>) {
        self.predicate_aliases.extend(aliases);
    }
}
