//! The public entry point for tidalDB: [`TidalDb`] handle and [`TidalDbBuilder`].

pub(crate) mod backup;
pub mod builder;
mod cohorts;
pub(crate) mod collections;
pub mod config;
mod creators;
pub(crate) mod export;
#[cfg(feature = "metrics")]
pub mod http;
mod items;
pub(crate) mod metadata;
pub mod metrics;
pub(crate) mod notification_tracker;
mod open;
pub mod paths;
mod query_ops;
mod relationships;
pub(crate) mod schema_fingerprint;
mod session_restore;
mod sessions;
mod signals;
mod state_rebuild;
pub(crate) mod storage_box;
#[cfg(any(test, feature = "test-utils"))]
pub mod temp;
mod users;
pub(crate) mod wal_bridge;

pub use backup::BackupInfo;
pub use builder::TidalDbBuilder;
pub use config::{Config, ConfigError, NodeConfig, StorageMode};
pub use export::UserSessionSummary;
pub(crate) use metadata::deserialize_metadata;
pub use metrics::MetricsState;
pub use paths::Paths;
#[cfg(any(test, feature = "test-utils"))]
pub use temp::TempTidalHome;

use std::sync::atomic::{AtomicBool, AtomicU64};
use std::sync::{Arc, RwLock};

use roaring::RoaringBitmap;

use crate::entities::{
    CoEngagementIndex, CollectionIndex, CreatorItemsBitmap, HardNegIndex, InteractionLedger,
    PreferenceVectors, UserSignalIndex, UserStateIndex,
};
use crate::ranking::builtins::register_builtins;
use crate::ranking::registry::ProfileRegistry;
use crate::schema::{EntityKind, Schema};
use crate::session::{SessionId, SessionSnapshot, SessionState};
use crate::signals::SignalLedger;
use crate::storage::StorageEngine;
use crate::storage::indexes::bitmap::BitmapIndex;
use crate::storage::indexes::range::RangeIndex;
use crate::storage::vector::registry::EmbeddingSlotRegistry;
use crate::wal::WalHandle;

use self::open::OpenResult;
use self::state_rebuild::run_checkpoint_thread;
use self::storage_box::StorageBox;

mod diagnostics;
mod lifecycle;
mod replication_ops;
mod sweeper;
mod text_syncer;

use self::text_syncer::spawn_text_syncer;

/// A tidalDB database instance.
///
/// Created via [`TidalDb::builder()`]. Call [`close`](Self::close) for
/// explicit shutdown; [`Drop`] runs best-effort cleanup if not called.
/// `Send + Sync` -- wrap in `Arc` for multi-threaded access.
pub struct TidalDb {
    config: Config,
    closed: AtomicBool,
    metrics: Arc<MetricsState>,
    #[cfg(feature = "metrics")]
    metrics_handle: Option<http::MetricsHandle>,
    ledger: Option<Arc<SignalLedger>>,
    storage: Option<StorageBox>,
    wal: std::sync::Mutex<Option<WalHandle>>,
    last_wal_seq: Arc<AtomicU64>,
    shutdown_checkpoint: Arc<AtomicBool>,
    checkpoint_thread: std::sync::Mutex<Option<std::thread::JoinHandle<()>>>,
    // M2 indexes
    category_index: BitmapIndex,
    format_index: BitmapIndex,
    creator_index: BitmapIndex,
    tag_index: BitmapIndex,
    duration_index: RangeIndex<u32>,
    created_at_index: RangeIndex<u64>,
    universe: Arc<RwLock<RoaringBitmap>>,
    embedding_registry: Arc<RwLock<EmbeddingSlotRegistry>>,
    profile_registry: ProfileRegistry,
    #[allow(dead_code)]
    schema_def: Option<Schema>,
    // M3 entities
    creator_items: Arc<CreatorItemsBitmap>,
    user_state: Arc<UserStateIndex>,
    hard_negatives: Arc<HardNegIndex>,
    interaction_ledger: Arc<InteractionLedger>,
    preference_vectors: Arc<PreferenceVectors>,
    // M5 text index
    #[allow(dead_code)]
    text_index: Option<Arc<crate::text::TextIndex>>,
    text_tx: std::sync::Mutex<Option<crossbeam::channel::Sender<crate::text::PendingWrite>>>,
    text_syncer_thread: std::sync::Mutex<Option<std::thread::JoinHandle<crate::Result<()>>>>,
    creator_text_index: Option<Arc<crate::text::TextIndex>>,
    creator_text_tx:
        std::sync::Mutex<Option<crossbeam::channel::Sender<crate::text::PendingWrite>>>,
    creator_text_syncer_thread:
        std::sync::Mutex<Option<std::thread::JoinHandle<crate::Result<()>>>>,
    #[allow(dead_code)]
    text_flush_tx: Option<crossbeam::channel::Sender<crossbeam::channel::Sender<()>>>,
    #[allow(dead_code)]
    creator_text_flush_tx: Option<crossbeam::channel::Sender<crossbeam::channel::Sender<()>>>,
    // M4 sessions
    #[allow(dead_code)]
    sessions: dashmap::DashMap<SessionId, Arc<SessionState>>,
    #[allow(dead_code)]
    next_session_id: AtomicU64,
    #[allow(dead_code)]
    closed_sessions: dashmap::DashMap<SessionId, SessionSnapshot>,
    // M6 co-engagement + social graph
    co_engagement: Arc<CoEngagementIndex>,
    user_signal_index: Arc<UserSignalIndex>,
    // M6 cohort engine
    cohort_registry: Arc<crate::cohort::CohortRegistry>,
    cohort_resolver: Arc<crate::cohort::CohortResolver>,
    cohort_ledger: Arc<crate::cohort::CohortSignalLedger>,
    // M6p4 collections
    collection_index: Arc<CollectionIndex>,
    // M6p5 suggestion index
    suggestion_index: Arc<crate::query::suggest::SuggestionIndex>,
    // M6p6 notification tracker
    notification_tracker: Arc<notification_tracker::NotificationTracker>,
    // M7p2 load detector
    load_detector: Arc<crate::load::LoadDetector>,
    // M7p2 backpressure config
    backpressure_config: crate::load::BackpressureConfig,
    // M7p2 per-agent rate limiter
    rate_limiter: Arc<crate::load::RateLimiter>,
    // M7p2 session TTL sweeper
    shutdown_sweeper: Arc<AtomicBool>,
    sweeper_thread: std::sync::Mutex<Option<std::thread::JoinHandle<()>>>,
    // True when a backup is in progress. Signal writes return Backpressure
    // during the backup window. Cleared by the BackupGuard RAII drop.
    backup_in_progress: Arc<AtomicBool>,
    // M8p2 replication
    replication_state: Arc<crate::replication::state::ReplicationState>,
    receiver_handle: std::sync::Mutex<Option<crate::replication::receiver::SegmentReceiverHandle>>,
    #[allow(dead_code)] // Held for its Drop side effect (thread join on shutdown).
    shipper_handle: Option<crate::replication::WalShipperHandle>,
    // M8p4 session replication bridge (dedup + cross-region session visibility)
    session_bridge: Arc<crate::replication::SessionReplicationBridge>,
    // M8p5 control plane + multi-tenancy
    tenant_router: Arc<crate::replication::TenantRouter>,
    control_plane: Arc<crate::replication::ControlPlane>,
    // Directory-level exclusive lock (persistent mode only).
    // Held for the lifetime of the process; released on Drop when the
    // File handle is closed. Advisory flock prevents two processes from
    // opening the same data directory simultaneously.
    #[allow(dead_code)] // Held for its Drop side effect (flock release).
    lock_file: Option<std::fs::File>,
}

impl TidalDb {
    /// Returns a new [`TidalDbBuilder`] with default (ephemeral) configuration.
    #[must_use]
    pub fn builder() -> TidalDbBuilder {
        TidalDbBuilder::new()
    }

    /// Build a default single-shard `SessionReplicationBridge` for single-node deployments.
    ///
    /// Uses an in-process self-loop transport (`ShardId::SINGLE`). In single-node mode
    /// the bridge is never actually used for shipping (no peers), but the seqno tracker
    /// and idempotency store are exercised on replay to prevent duplicate application.
    fn make_single_node_session_bridge() -> Arc<crate::replication::SessionReplicationBridge> {
        use crate::replication::{
            IdempotencyStore, InProcessSessionTransportFactory, SessionReplicationBridge, ShardId,
        };
        use crate::session::state::SessionSeqNoTracker;
        let shards = [ShardId::SINGLE];
        let mut transports = InProcessSessionTransportFactory::new(&shards).build();
        let transport = transports
            .remove(&ShardId::SINGLE)
            .expect("transport for ShardId::SINGLE always present");
        Arc::new(SessionReplicationBridge::new(
            transport,
            Arc::new(IdempotencyStore::default_capacity()),
            Arc::new(SessionSeqNoTracker::new()),
        ))
    }

    /// Construct a `TidalDb` without a schema (M0 compatibility mode).
    #[allow(clippy::missing_const_for_fn)]
    pub(crate) fn from_config(
        config: Config,
        metrics: Arc<MetricsState>,
        #[cfg(feature = "metrics")] metrics_handle: Option<http::MetricsHandle>,
    ) -> Self {
        let mut profile_registry = ProfileRegistry::new();
        // Register builtins even in no-schema mode so health_check and metrics
        // still work. Ignore errors -- builtins always validate.
        let _ = register_builtins(&mut profile_registry);

        let cohort_registry = Arc::new(crate::cohort::CohortRegistry::new());
        let cohort_resolver = Arc::new(crate::cohort::CohortResolver::new(Arc::clone(
            &cohort_registry,
        )));

        Self {
            config,
            closed: AtomicBool::new(false),
            metrics,
            #[cfg(feature = "metrics")]
            metrics_handle,
            ledger: None,
            storage: None,
            wal: std::sync::Mutex::new(None),
            last_wal_seq: Arc::new(AtomicU64::new(0)),
            shutdown_checkpoint: Arc::new(AtomicBool::new(false)),
            checkpoint_thread: std::sync::Mutex::new(None),
            category_index: BitmapIndex::new("category"),
            format_index: BitmapIndex::new("format"),
            creator_index: BitmapIndex::new("creator"),
            tag_index: BitmapIndex::new("tags"),
            duration_index: RangeIndex::new("duration"),
            created_at_index: RangeIndex::new("created_at"),
            universe: Arc::new(RwLock::new(RoaringBitmap::new())),
            embedding_registry: Arc::new(RwLock::new(EmbeddingSlotRegistry::new())),
            profile_registry,
            schema_def: None,
            creator_items: Arc::new(CreatorItemsBitmap::new()),
            user_state: Arc::new(UserStateIndex::new()),
            hard_negatives: Arc::new(HardNegIndex::new()),
            interaction_ledger: Arc::new(InteractionLedger::new()),
            preference_vectors: Arc::new(PreferenceVectors::new(128)),
            text_index: None,
            text_tx: std::sync::Mutex::new(None),
            text_syncer_thread: std::sync::Mutex::new(None),
            creator_text_index: None,
            creator_text_tx: std::sync::Mutex::new(None),
            creator_text_syncer_thread: std::sync::Mutex::new(None),
            text_flush_tx: None,
            creator_text_flush_tx: None,
            sessions: dashmap::DashMap::new(),
            next_session_id: AtomicU64::new(1),
            closed_sessions: dashmap::DashMap::new(),
            co_engagement: Arc::new(CoEngagementIndex::new()),
            user_signal_index: Arc::new(UserSignalIndex::new()),
            cohort_registry,
            cohort_resolver,
            cohort_ledger: Arc::new(crate::cohort::CohortSignalLedger::empty()),
            collection_index: Arc::new(CollectionIndex::new()),
            suggestion_index: Arc::new(crate::query::suggest::SuggestionIndex::new()),
            notification_tracker: Arc::new(notification_tracker::NotificationTracker::new()),
            load_detector: Arc::new(crate::load::LoadDetector::new(
                crate::load::DegradationThresholds::default(),
            )),
            backpressure_config: crate::load::BackpressureConfig::default(),
            rate_limiter: Arc::new(crate::load::RateLimiter::new(
                crate::load::RateLimiterConfig::default(),
            )),
            shutdown_sweeper: Arc::new(AtomicBool::new(false)),
            sweeper_thread: std::sync::Mutex::new(None),
            backup_in_progress: Arc::new(AtomicBool::new(false)),
            replication_state: Arc::new(crate::replication::state::ReplicationState::single()),
            receiver_handle: std::sync::Mutex::new(None),
            shipper_handle: None,
            session_bridge: Self::make_single_node_session_bridge(),
            lock_file: None,
            tenant_router: {
                let topo = Arc::new(RwLock::new(
                    crate::replication::tenant::ClusterTopology::single(),
                ));
                Arc::new(crate::replication::TenantRouter::new(topo))
            },
            control_plane: {
                let topo = Arc::new(RwLock::new(
                    crate::replication::tenant::ClusterTopology::single(),
                ));
                let router = Arc::new(crate::replication::TenantRouter::new(Arc::clone(&topo)));
                let rep_state = Arc::new(crate::replication::state::ReplicationState::single());
                let lag = Arc::new(crate::replication::ReplicationLagGauge::new(
                    crate::replication::ShardId::SINGLE,
                    rep_state,
                ));
                Arc::new(crate::replication::ControlPlane::new(topo, router, lag))
            },
        }
    }

    /// Construct a `TidalDb` from all opened components.
    #[allow(
        clippy::too_many_arguments,
        clippy::missing_const_for_fn,
        clippy::too_many_lines
    )]
    pub(crate) fn from_parts(
        config: Config,
        metrics: Arc<MetricsState>,
        #[cfg(feature = "metrics")] metrics_handle: Option<http::MetricsHandle>,
        result: OpenResult,
    ) -> Self {
        let OpenResult {
            storage,
            ledger,
            wal,
            last_seq,
            profile_registry,
            category_index,
            format_index,
            creator_index,
            tag_index,
            duration_index,
            created_at_index,
            universe,
            embedding_registry,
            schema_def,
            creator_items,
            user_state,
            hard_negatives,
            interaction_ledger,
            preference_vectors,
            session_events,
        } = result;

        let ledger = Some(ledger);
        let storage = Some(storage);

        // M6 co-engagement index: restore from durable storage if available.
        let co_engagement = Arc::new(CoEngagementIndex::new());
        if let Some(ref sb) = storage
            && let Err(e) = co_engagement.restore(sb.items_engine())
        {
            tracing::warn!(
                error = %e,
                "co-engagement restore failed; starting from empty state"
            );
        }
        // M6 user signal index: ephemeral by design; not persisted.
        let user_signal_index = Arc::new(UserSignalIndex::new());

        // M6 cohort engine: initialize from schema and restore from checkpoint.
        let cohort_registry = Arc::new(crate::cohort::CohortRegistry::new());

        // Reload persisted cohort definitions (persistent mode only).
        if let Some(ref sb) = storage {
            let prefix = crate::storage::entity_tag_prefix(
                crate::schema::EntityId::new(0),
                crate::storage::Tag::CohortDef,
            );
            let iter = sb.items_engine().scan_prefix(&prefix);
            for (_, value) in iter.flatten() {
                if let Some(def) = crate::cohort::deserialize_cohort_def(&value) {
                    // Skip if already registered (should not happen on fresh open,
                    // but guards against duplicate keys in storage).
                    if cohort_registry.get(&def.name).is_none() {
                        let _ = cohort_registry.define(def);
                    }
                }
            }
        }

        let cohort_resolver = Arc::new(crate::cohort::CohortResolver::new(Arc::clone(
            &cohort_registry,
        )));
        let cohort_ledger = Arc::new(crate::cohort::CohortSignalLedger::new(&schema_def));

        // Restore cohort signal state from durable storage (persistent mode only).
        if let Some(ref sb) = storage {
            match cohort_ledger.restore(sb.items_engine()) {
                Ok(Some(meta)) => {
                    tracing::info!(
                        entries = cohort_ledger.entry_count(),
                        wal_sequence = meta.wal_sequence,
                        "cohort signal ledger restored from checkpoint"
                    );
                }
                Ok(None) => {
                    // First boot or no cohort checkpoint yet -- nothing to restore.
                }
                Err(e) => {
                    tracing::warn!(
                        error = %e,
                        "cohort signal ledger restore failed; starting from empty state"
                    );
                }
            }
        }

        let text_bundle = spawn_text_syncer(
            schema_def.text_fields(),
            &config,
            "text_index",
            "tidaldb-text-syncer",
        );
        let creator_text_bundle = spawn_text_syncer(
            schema_def.creator_text_fields(),
            &config,
            "creator_text_index",
            "tidaldb-creator-text-syncer",
        );

        // Wrap the embedding registry early so the checkpoint thread can share it.
        let embedding_registry = Arc::new(RwLock::new(embedding_registry));

        // Spawn periodic checkpoint thread (persistent mode only).
        let shutdown_checkpoint = Arc::new(AtomicBool::new(false));
        let checkpoint_thread = {
            let handle = match (storage.as_ref(), ledger.as_ref()) {
                (Some(StorageBox::Fjall(f)), Some(ledger_arc)) => {
                    let items = Box::new(f.backend(EntityKind::Item).clone())
                        as Box<dyn StorageEngine + Send + Sync>;
                    let shutdown = Arc::clone(&shutdown_checkpoint);
                    let ledger_clone = Arc::clone(ledger_arc);
                    let cohort_clone = Arc::clone(&cohort_ledger);
                    let seq_clone = Arc::clone(&last_seq);
                    let wal_dir = config.data_dir.as_ref().map(|d| d.join("wal"));
                    let metrics_clone = Arc::clone(&metrics);

                    #[cfg(feature = "metrics")]
                    let index_handles = state_rebuild::IndexMetricsHandles {
                        text_index: text_bundle.index.clone(),
                        embedding_registry: Arc::clone(&embedding_registry),
                        bitmap_category: category_index.clone(),
                        bitmap_format: format_index.clone(),
                        bitmap_creator: creator_index.clone(),
                        bitmap_tag: tag_index.clone(),
                    };
                    #[cfg(not(feature = "metrics"))]
                    let index_handles = state_rebuild::IndexMetricsHandles;

                    std::thread::Builder::new()
                        .name("tidaldb-checkpoint".to_string())
                        .spawn(move || {
                            run_checkpoint_thread(
                                shutdown,
                                ledger_clone,
                                cohort_clone,
                                items,
                                seq_clone,
                                wal_dir,
                                metrics_clone,
                                index_handles,
                            );
                        })
                        .ok()
                }
                _ => None,
            };
            std::sync::Mutex::new(handle)
        };

        // Compute the replication state before moving config into the struct.
        // Include both the local shard and any configured peer shards.
        let replication_state = {
            let mut shards: Vec<crate::replication::ShardId> = vec![config.cluster.shard_id];
            shards.extend_from_slice(&config.cluster.peer_shards);
            Arc::new(crate::replication::state::ReplicationState::new(&shards))
        };

        let db = Self {
            config,
            closed: AtomicBool::new(false),
            metrics,
            #[cfg(feature = "metrics")]
            metrics_handle,
            ledger,
            storage,
            wal: std::sync::Mutex::new(wal),
            last_wal_seq: last_seq,
            shutdown_checkpoint,
            checkpoint_thread,
            category_index,
            format_index,
            creator_index,
            tag_index,
            duration_index,
            created_at_index,
            universe: Arc::new(RwLock::new(universe)),
            embedding_registry,
            profile_registry,
            schema_def: Some(schema_def),
            creator_items: Arc::new(creator_items),
            user_state: Arc::new(user_state),
            hard_negatives: Arc::new(hard_negatives),
            interaction_ledger: Arc::new(interaction_ledger),
            preference_vectors: Arc::new(preference_vectors),
            text_index: text_bundle.index,
            text_tx: std::sync::Mutex::new(text_bundle.write_tx),
            text_syncer_thread: text_bundle.thread,
            creator_text_index: creator_text_bundle.index,
            creator_text_tx: std::sync::Mutex::new(creator_text_bundle.write_tx),
            creator_text_syncer_thread: creator_text_bundle.thread,
            text_flush_tx: text_bundle.flush_tx,
            creator_text_flush_tx: creator_text_bundle.flush_tx,
            sessions: dashmap::DashMap::new(),
            next_session_id: AtomicU64::new(1),
            closed_sessions: dashmap::DashMap::new(),
            co_engagement,
            user_signal_index,
            cohort_registry,
            cohort_resolver,
            cohort_ledger,
            collection_index: Arc::new(CollectionIndex::new()),
            suggestion_index: Arc::new(crate::query::suggest::SuggestionIndex::new()),
            notification_tracker: Arc::new(notification_tracker::NotificationTracker::new()),
            load_detector: Arc::new(crate::load::LoadDetector::new(
                crate::load::DegradationThresholds::default(),
            )),
            backpressure_config: crate::load::BackpressureConfig::default(),
            rate_limiter: Arc::new(crate::load::RateLimiter::new(
                crate::load::RateLimiterConfig::default(),
            )),
            shutdown_sweeper: Arc::new(AtomicBool::new(false)),
            sweeper_thread: std::sync::Mutex::new(None),
            backup_in_progress: Arc::new(AtomicBool::new(false)),
            replication_state,
            receiver_handle: std::sync::Mutex::new(None),
            shipper_handle: None,
            session_bridge: Self::make_single_node_session_bridge(),
            lock_file: None,
            tenant_router: {
                let topo = Arc::new(RwLock::new(
                    crate::replication::tenant::ClusterTopology::single(),
                ));
                Arc::new(crate::replication::TenantRouter::new(topo))
            },
            control_plane: {
                let topo = Arc::new(RwLock::new(
                    crate::replication::tenant::ClusterTopology::single(),
                ));
                let router = Arc::new(crate::replication::TenantRouter::new(Arc::clone(&topo)));
                let rep_state = Arc::new(crate::replication::state::ReplicationState::single());
                let lag = Arc::new(crate::replication::ReplicationLagGauge::new(
                    crate::replication::ShardId::SINGLE,
                    rep_state,
                ));
                Arc::new(crate::replication::ControlPlane::new(topo, router, lag))
            },
        };

        // M6p4: rebuild collection index from durable storage.
        // M6p5: rebuild suggestion index title terms from durable item metadata.
        if let Some(ref sb) = db.storage {
            crate::db::collections::rebuild_collections(&db.collection_index, sb.items_engine());
            crate::db::state_rebuild::rebuild_suggestion_index(sb, &db.suggestion_index);
        }

        if !session_events.is_empty() {
            db.restore_session_wal_events(&session_events);
        }
        db
    }

    /// Returns the bound address of the metrics HTTP server, if running.
    ///
    /// Useful when port 0 was requested to discover the OS-assigned port.
    /// Returns `None` if the `metrics` feature is disabled or if
    /// `enable_metrics` was not called on the builder.
    #[must_use]
    #[allow(clippy::missing_const_for_fn)] // cfg-gated body prevents const
    pub fn metrics_addr(&self) -> Option<std::net::SocketAddr> {
        #[cfg(feature = "metrics")]
        {
            self.metrics_handle.as_ref().map(|h| h.addr)
        }
        #[cfg(not(feature = "metrics"))]
        {
            None
        }
    }
}
