//! Boot-time construction of the `Archivist` coordinator from a parsed
//! `ArchivesConfig` and a `BackendRegistry` of factories.

use std::sync::Arc;

use tokio::sync::RwLock;

use crate::backend::HealthStatus;
use crate::error::ArchivistBootError;
use crate::registry::{
    cache::ReadCache, ArchiveRegistration, ArchivesConfig, BackendRegistry, FailureMode,
    WritePolicy,
};

use super::Archivist;

impl Archivist {
    /// Construct the coordinator from a parsed `[[archives]]` config block
    /// and a registry of backend factories.
    ///
    /// - Validates the config (duplicate-name / no-primary rules).
    /// - Instantiates every enabled backend via the factory.
    /// - Runs a startup `health_check` per backend.
    /// - Sorts registrations by `read_priority` (ties by declaration order).
    /// - Writer tasks for `WritePolicy::Queued` backends are wired in Task 17;
    ///   for now every backend boots with `writer = None`.
    pub async fn from_config(
        mut config: ArchivesConfig,
        registry: &BackendRegistry,
        base_dir: Option<&std::path::Path>,
    ) -> Result<Self, ArchivistBootError> {
        config.validate()?;

        // Filter-level validation (Phase 4, Task 19).
        //
        // 1. At least one enabled write-active archive must have an
        //    unrestricted filter. Otherwise there is no default home for
        //    a session that does not match any filter, and the primary
        //    target would silently exclude sessions despite being the
        //    "write-always" backend.
        // 2. No archive may declare a filter whose `include_connectors`
        //    set is `Some(empty)` — that form rejects every session
        //    unconditionally and is almost always a config typo.
        let mut has_unrestricted_write_active = false;
        for entry in &config.entries {
            if let Some(inc) = &entry.filter.include_connectors {
                if inc.is_empty() {
                    return Err(ArchivistBootError::FilterRejectsEverything {
                        archive: entry.name.clone(),
                    });
                }
            }
            if entry.enabled && entry.write_active && entry.filter.is_unrestricted() {
                has_unrestricted_write_active = true;
            }
        }
        if !config.entries.is_empty() && !has_unrestricted_write_active {
            return Err(ArchivistBootError::NoUnrestrictedPrimary);
        }

        // Resolve relative `params.path` values against `base_dir` so that
        // archives declared with relative paths land under the data directory
        // rather than the binary's CWD.
        if let Some(base) = base_dir {
            for entry in &mut config.entries {
                if let toml::Value::Table(ref mut table) = entry.params {
                    if let Some(toml::Value::String(ref mut path_str)) = table.get_mut("path") {
                        let p = std::path::Path::new(path_str.as_str());
                        if p.is_relative() {
                            *path_str = base.join(&*path_str).to_string_lossy().into_owned();
                        }
                    }
                }
            }
        }

        let mut registrations: Vec<Arc<ArchiveRegistration>> = Vec::new();

        for entry in config.entries.into_iter() {
            let backend = registry
                .build(&entry.name, &entry.type_name, entry.params)
                .await
                .map_err(|e| match e {
                    crate::registry::BackendBuildError::UnknownType(t) => {
                        ArchivistBootError::UnknownType {
                            name: entry.name.clone(),
                            type_name: t,
                        }
                    }
                    other => ArchivistBootError::BackendBuild {
                        name: entry.name.clone(),
                        source: other,
                    },
                })?;

            let initial_health = backend.health_check().await;

            if entry.failure_mode == FailureMode::Required {
                if let HealthStatus::Unavailable { reason } = &initial_health {
                    return Err(ArchivistBootError::UnavailableRequiredBackend {
                        name: entry.name.clone(),
                        reason: reason.clone(),
                    });
                }
            }

            let runtime_policy: WritePolicy = entry.write_policy.into_runtime();

            // Build shared drift state up-front so the writer task (if any)
            // and the registration's health-drift helpers mutate the SAME
            // `Arc<RwLock<_>>` cells. This keeps Task 22's drift semantics
            // coherent across the inline and queued paths.
            let health_state: Arc<RwLock<HealthStatus>> =
                Arc::new(RwLock::new(initial_health.clone()));
            let error_state: Arc<
                RwLock<Option<(chrono::DateTime<chrono::Utc>, String)>>,
            > = Arc::new(RwLock::new(None));
            let failure_counter: Arc<RwLock<u32>> = Arc::new(RwLock::new(0u32));

            let writer = match &runtime_policy {
                WritePolicy::Inline => None,
                WritePolicy::Queued {
                    batch_window_ms,
                    capacity,
                    overflow,
                } => Some(crate::registry::writer::spawn_writer(
                    backend.clone(),
                    entry.name.clone(),
                    *capacity,
                    std::time::Duration::from_millis(*batch_window_ms),
                    *overflow,
                    health_state.clone(),
                    error_state.clone(),
                    failure_counter.clone(),
                )),
            };

            // Leak `type_name` to satisfy &'static str on the registration; safe at boot,
            // and a constant number of entries (O(archives in config)).
            let type_name_static: &'static str = Box::leak(entry.type_name.into_boxed_str());

            let registration = ArchiveRegistration::new_with_shared_state(
                entry.name,
                type_name_static,
                backend,
                entry.write_active,
                entry.failure_mode,
                entry.read_priority,
                entry.enabled,
                runtime_policy,
                writer,
                health_state,
                error_state,
                failure_counter,
            )
            .with_filter(entry.filter);

            registrations.push(Arc::new(registration));
        }

        // Sort by `read_priority`. Rust's sort is stable, so ties keep declaration order.
        registrations.sort_by_key(|r| r.read_priority);

        Ok(Self {
            registrations: RwLock::new(registrations),
            read_cache: Arc::new(ReadCache::new()),
            registry_path: std::path::PathBuf::new(),
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::registry::{ArchivesConfig, BackendRegistry};

    fn parse(toml_src: &str) -> ArchivesConfig {
        toml::from_str(toml_src).unwrap()
    }

    #[tokio::test]
    async fn relative_archive_path_resolved_against_base_dir() {
        let base = tempfile::tempdir().unwrap();
        let cfg = parse(
            r#"
            [[archives]]
            name = "main"
            type = "jsonl"
            [archives.params]
            path = "my_archive"
            "#,
        );
        let registry = BackendRegistry::with_jsonl();
        let archivist = Archivist::from_config(cfg, &registry, Some(base.path()))
            .await
            .unwrap();

        // The archive should have been created under base_dir/my_archive.
        // Verify by checking the .contexts directory exists.
        let expected = base.path().join("my_archive").join(".contexts");
        assert!(
            expected.exists(),
            "expected {expected:?} to exist after boot with relative path"
        );

        // Also verify the archivist is functional (has one registration).
        let archives = archivist.list_archives().await.unwrap();
        assert_eq!(archives.len(), 1);
    }

    #[tokio::test]
    async fn absolute_archive_path_not_affected_by_base_dir() {
        let base = tempfile::tempdir().unwrap();
        let archive_dir = tempfile::tempdir().unwrap();
        let abs_path = archive_dir.path().to_string_lossy().replace('\\', "/");

        let cfg = parse(&format!(
            r#"
            [[archives]]
            name = "main"
            type = "jsonl"
            [archives.params]
            path = "{abs_path}"
            "#,
        ));
        let registry = BackendRegistry::with_jsonl();
        let archivist = Archivist::from_config(cfg, &registry, Some(base.path()))
            .await
            .unwrap();

        // The archive should be at the absolute path, NOT under base_dir.
        let expected = archive_dir.path().join(".contexts");
        assert!(
            expected.exists(),
            "expected {expected:?} to exist (absolute path should be used as-is)"
        );

        // Verify nothing was created under base_dir with the archive name.
        // If base_dir resolution incorrectly touched the absolute path, we'd
        // see stray directories under base_dir.
        let base_entries: Vec<_> = std::fs::read_dir(base.path())
            .unwrap()
            .collect();
        assert!(
            base_entries.is_empty(),
            "base_dir should be untouched when archive path is absolute, found: {base_entries:?}"
        );

        let archives = archivist.list_archives().await.unwrap();
        assert_eq!(archives.len(), 1);
    }

    #[tokio::test]
    async fn none_base_dir_preserves_existing_behavior() {
        let archive_dir = tempfile::tempdir().unwrap();
        let abs_path = archive_dir.path().to_string_lossy().replace('\\', "/");

        let cfg = parse(&format!(
            r#"
            [[archives]]
            name = "main"
            type = "jsonl"
            [archives.params]
            path = "{abs_path}"
            "#,
        ));
        let registry = BackendRegistry::with_jsonl();
        let archivist = Archivist::from_config(cfg, &registry, None)
            .await
            .unwrap();

        let expected = archive_dir.path().join(".contexts");
        assert!(
            expected.exists(),
            "expected {expected:?} to exist with None base_dir and absolute path"
        );

        let archives = archivist.list_archives().await.unwrap();
        assert_eq!(archives.len(), 1);
    }
}
