//! Declarative `[[archives]]` config block parsed from `dirigent.toml`.
//!
//! The TOML schema is documented in `docs/plans/2026-04-19-archivist-phase3-design.md`.

use serde::{Deserialize, Serialize};

use super::filter::ArchiveFilter;
use super::registration::{FailureMode, OverflowPolicy};

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ArchivesConfig {
    #[serde(default, rename = "archives")]
    pub entries: Vec<ArchiveConfig>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ArchiveConfig {
    pub name: String,
    #[serde(rename = "type")]
    pub type_name: String,
    #[serde(default = "default_write_active")]
    pub write_active: bool,
    #[serde(default)]
    pub failure_mode: FailureMode,
    #[serde(default)]
    pub read_priority: u32,
    #[serde(default = "default_enabled")]
    pub enabled: bool,
    #[serde(default)]
    pub write_policy: WritePolicyConfig,
    /// Per-archive include/exclude filter applied during non-primary write
    /// fanout. Absent or `{}` means unrestricted.
    #[serde(default)]
    pub filter: ArchiveFilter,
    #[serde(default = "default_params")]
    pub params: toml::Value,
}

fn default_params() -> toml::Value {
    toml::Value::Table(toml::value::Table::new())
}

fn default_write_active() -> bool {
    true
}
fn default_enabled() -> bool {
    true
}

#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum WritePolicyConfig {
    Tag(WritePolicyTag),
    Detailed(WritePolicyDetailed),
}

#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum WritePolicyTag {
    Inline,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum WritePolicyDetailed {
    Inline,
    Queued {
        #[serde(default = "default_batch_window_ms")]
        batch_window_ms: u64,
        #[serde(default = "default_capacity")]
        capacity: usize,
        #[serde(default)]
        overflow: OverflowPolicy,
    },
}

fn default_batch_window_ms() -> u64 {
    50
}
fn default_capacity() -> usize {
    1024
}

impl Default for WritePolicyConfig {
    fn default() -> Self {
        WritePolicyConfig::Tag(WritePolicyTag::Inline)
    }
}

impl WritePolicyConfig {
    pub fn into_runtime(self) -> super::registration::WritePolicy {
        use super::registration::WritePolicy;
        match self {
            WritePolicyConfig::Tag(WritePolicyTag::Inline) => WritePolicy::Inline,
            WritePolicyConfig::Detailed(WritePolicyDetailed::Inline) => WritePolicy::Inline,
            WritePolicyConfig::Detailed(WritePolicyDetailed::Queued {
                batch_window_ms,
                capacity,
                overflow,
            }) => WritePolicy::Queued {
                batch_window_ms,
                capacity,
                overflow,
            },
        }
    }
}

use std::collections::BTreeSet;

#[derive(Debug, thiserror::Error, PartialEq)]
pub enum ConfigValidationError {
    #[error("duplicate archive name `{0}`")]
    DuplicateName(String),
    #[error("no `required` write-active backend configured (need at least one)")]
    NoPrimary,
}

impl ArchivesConfig {
    pub fn validate(&self) -> Result<(), ConfigValidationError> {
        let mut seen: BTreeSet<&str> = BTreeSet::new();
        for entry in &self.entries {
            if !seen.insert(entry.name.as_str()) {
                return Err(ConfigValidationError::DuplicateName(entry.name.clone()));
            }
        }

        // Empty config is allowed (ephemeral mode).
        if self.entries.is_empty() {
            return Ok(());
        }

        let has_primary = self
            .entries
            .iter()
            .any(|e| e.enabled && e.write_active && e.failure_mode == FailureMode::Required);

        if !has_primary {
            return Err(ConfigValidationError::NoPrimary);
        }

        Ok(())
    }
}

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

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

    #[test]
    fn empty_config_is_ephemeral() {
        let cfg: ArchivesConfig = toml::from_str("").unwrap();
        assert!(cfg.entries.is_empty());
        assert!(cfg.validate().is_ok());
    }

    #[test]
    fn minimal_single_archive() {
        let cfg = parse(
            r#"
            [[archives]]
            name = "main"
            type = "jsonl"
            [archives.params]
            path = "dirigent_archive"
            "#,
        );
        assert_eq!(cfg.entries.len(), 1);
        let e = &cfg.entries[0];
        assert_eq!(e.name, "main");
        assert_eq!(e.type_name, "jsonl");
        assert!(e.write_active);
        assert_eq!(e.failure_mode, FailureMode::Required);
        assert_eq!(e.read_priority, 0);
        assert!(e.enabled);
        assert!(matches!(e.write_policy, WritePolicyConfig::Tag(WritePolicyTag::Inline)));
        cfg.validate().unwrap();
    }

    #[test]
    fn duplicate_name_rejected() {
        let cfg = parse(
            r#"
            [[archives]]
            name = "main"
            type = "jsonl"
            [archives.params]
            path = "a"

            [[archives]]
            name = "main"
            type = "jsonl"
            [archives.params]
            path = "b"
            "#,
        );
        assert_eq!(
            cfg.validate(),
            Err(ConfigValidationError::DuplicateName("main".into()))
        );
    }

    #[test]
    fn no_primary_rejected() {
        let cfg = parse(
            r#"
            [[archives]]
            name = "mirror"
            type = "jsonl"
            failure_mode = "best_effort"
            [archives.params]
            path = "a"
            "#,
        );
        assert_eq!(cfg.validate(), Err(ConfigValidationError::NoPrimary));
    }

    #[test]
    fn queued_write_policy_parses() {
        let cfg = parse(
            r#"
            [[archives]]
            name = "main"
            type = "jsonl"
            [archives.params]
            path = "a"

            [archives.write_policy]
            type = "queued"
            batch_window_ms = 100
            capacity = 4096
            overflow = "drop_oldest"
            "#,
        );
        let entry = &cfg.entries[0];
        match &entry.write_policy {
            WritePolicyConfig::Detailed(WritePolicyDetailed::Queued {
                batch_window_ms,
                capacity,
                overflow,
            }) => {
                assert_eq!(*batch_window_ms, 100);
                assert_eq!(*capacity, 4096);
                assert_eq!(*overflow, OverflowPolicy::DropOldest);
            }
            other => panic!("unexpected write_policy: {:?}", other),
        }
    }
}
