pub mod builders;
pub mod policies;
pub mod text;

use std::collections::HashMap;

use super::SignalTypeDef;

pub use builders::{SchemaBuilder, SignalBuilder};
pub use policies::AgentPolicy;
pub use text::{EmbeddingSlotDef, TextFieldDef, TextFieldType};

/// User-facing decay specification (before validation computes lambda).
///
/// Users specify `DecaySpec::Exponential { half_life }` — no lambda.
/// The `SchemaBuilder` validates the duration and computes `DecayModel::Exponential { half_life, lambda }`.
#[derive(Debug, Clone)]
pub enum DecaySpec {
    /// Weight halves every `half_life`.
    Exponential { half_life: std::time::Duration },
    /// Weight drops linearly to zero over `lifetime`.
    Linear { lifetime: std::time::Duration },
    /// Never decays. Used for permanent flags: hide, block, follow.
    Permanent,
}

/// A validated, immutable schema.
///
/// Constructed exclusively through `SchemaBuilder`. Once built, the schema
/// is frozen — signal type definitions cannot be added or modified.
#[derive(Debug, Clone)]
pub struct Schema {
    signals: HashMap<String, SignalTypeDef>,
    embedding_slots: Vec<EmbeddingSlotDef>,
    text_fields: Vec<TextFieldDef>,
    creator_text_fields: Vec<TextFieldDef>,
    policies: HashMap<String, AgentPolicy>,
}

impl Schema {
    /// Construct a `Schema` from validated components.
    ///
    /// This is `pub(super)` so only the builder (within the validation module)
    /// can construct it -- external callers must go through `SchemaBuilder`.
    #[allow(clippy::missing_const_for_fn)] // HashMap prevents const
    pub(super) fn new(
        signals: HashMap<String, SignalTypeDef>,
        embedding_slots: Vec<EmbeddingSlotDef>,
        text_fields: Vec<TextFieldDef>,
        creator_text_fields: Vec<TextFieldDef>,
        policies: HashMap<String, AgentPolicy>,
    ) -> Self {
        Self {
            signals,
            embedding_slots,
            text_fields,
            creator_text_fields,
            policies,
        }
    }

    /// Look up a signal type definition by name.
    #[must_use]
    pub fn signal(&self, name: &str) -> Option<&SignalTypeDef> {
        self.signals.get(name)
    }

    /// Iterate over all signal type definitions.
    pub fn signals(&self) -> impl Iterator<Item = &SignalTypeDef> {
        self.signals.values()
    }

    /// Number of signal types defined.
    #[must_use]
    pub fn signal_count(&self) -> usize {
        self.signals.len()
    }

    /// Embedding slot definitions declared in this schema.
    #[must_use]
    pub fn embedding_slots(&self) -> &[EmbeddingSlotDef] {
        &self.embedding_slots
    }

    /// Text field definitions declared in this schema for full-text search (items).
    #[must_use]
    pub fn text_fields(&self) -> &[TextFieldDef] {
        &self.text_fields
    }

    /// Text field definitions declared in this schema for creator full-text search.
    #[must_use]
    pub fn creator_text_fields(&self) -> &[TextFieldDef] {
        &self.creator_text_fields
    }

    /// Look up a session policy by name.
    #[must_use]
    pub fn session_policy(&self, name: &str) -> Option<&AgentPolicy> {
        self.policies.get(name)
    }

    /// Iterate over all session policies.
    pub fn session_policies(&self) -> impl Iterator<Item = (&str, &AgentPolicy)> {
        self.policies.iter().map(|(k, v)| (k.as_str(), v))
    }
}

/// Check if a signal name is a valid identifier.
///
/// Must be non-empty, ASCII, lowercase alphanumeric + underscore,
/// and start with a letter. Safe for use in storage keys
/// (`SIG:{name}:{window}`) and the query language.
fn is_valid_signal_name(name: &str) -> bool {
    !name.is_empty()
        && name.is_ascii()
        && name
            .bytes()
            .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_')
        && name.as_bytes()[0].is_ascii_lowercase()
}

#[cfg(test)]
#[allow(unused_must_use, clippy::unwrap_used)]
mod tests {
    use super::*;
    use crate::schema::{EntityKind, Window};
    use std::time::Duration;

    // === Signal name validation ===

    #[test]
    fn is_valid_signal_name_unit() {
        assert!(is_valid_signal_name("view"));
        assert!(is_valid_signal_name("a"));
        assert!(is_valid_signal_name("view_count"));
        assert!(is_valid_signal_name("signal_24h"));

        assert!(!is_valid_signal_name(""));
        assert!(!is_valid_signal_name("View"));
        assert!(!is_valid_signal_name("1view"));
        assert!(!is_valid_signal_name("_view"));
        assert!(!is_valid_signal_name("view count"));
        assert!(!is_valid_signal_name("view-count"));
        assert!(!is_valid_signal_name("view!"));
    }

    // === UAT-style integration test ===

    #[test]
    fn milestone_1_uat_schema() {
        let mut builder = SchemaBuilder::new();
        builder
            .signal(
                "view",
                EntityKind::Item,
                DecaySpec::Exponential {
                    half_life: Duration::from_secs(7 * 24 * 3600), // 7 days
                },
            )
            .windows(&[Window::OneHour, Window::TwentyFourHours, Window::SevenDays])
            .velocity(true)
            .add();
        builder
            .signal(
                "like",
                EntityKind::Item,
                DecaySpec::Exponential {
                    half_life: Duration::from_secs(14 * 24 * 3600), // 14 days
                },
            )
            .windows(&[Window::TwentyFourHours, Window::SevenDays, Window::AllTime])
            .velocity(true)
            .add();
        builder
            .signal(
                "skip",
                EntityKind::Item,
                DecaySpec::Exponential {
                    half_life: Duration::from_secs(24 * 3600), // 1 day
                },
            )
            .windows(&[Window::OneHour, Window::TwentyFourHours])
            .velocity(false)
            .add();

        let schema = builder.build().expect("UAT schema should be valid");
        assert_eq!(schema.signal_count(), 3);

        // Verify view signal
        let view = schema.signal("view").unwrap();
        assert_eq!(view.windows().len(), 3);
        assert!(view.velocity_enabled());
        let lambda = view.decay().lambda().unwrap();
        let expected_lambda = std::f64::consts::LN_2 / (7.0 * 24.0 * 3600.0);
        assert!((lambda - expected_lambda).abs() < 1e-15);

        // Verify like signal
        let like = schema.signal("like").unwrap();
        assert_eq!(like.windows().len(), 3);
        assert!(like.windows().contains(&Window::AllTime));

        // Verify skip signal
        let skip = schema.signal("skip").unwrap();
        assert!(!skip.velocity_enabled());
        let skip_lambda = skip.decay().lambda().unwrap();
        let expected_skip_lambda = std::f64::consts::LN_2 / (24.0 * 3600.0);
        assert!((skip_lambda - expected_skip_lambda).abs() < 1e-15);
    }

    // === Schema query API ===

    #[test]
    fn schema_signal_returns_none_for_missing() {
        let mut builder = SchemaBuilder::new();
        builder
            .signal("view", EntityKind::Item, DecaySpec::Permanent)
            .add();
        let schema = builder.build().unwrap();
        assert!(schema.signal("nonexistent").is_none());
    }

    // === Property tests ===

    mod proptests {
        use super::*;
        use proptest::prelude::*;

        proptest! {
            #[test]
            fn signal_name_validation_consistent(name in "\\PC{0,100}") {
                let valid = is_valid_signal_name(&name);
                let expected = !name.is_empty()
                    && name.is_ascii()
                    && name.bytes().all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_')
                    && name.as_bytes()[0].is_ascii_lowercase();
                prop_assert_eq!(valid, expected);
            }

            #[test]
            fn schema_contains_all_defined_signals(count in 1usize..10) {
                let mut builder = SchemaBuilder::new();
                let names: Vec<String> = (0..count)
                    .map(|i| format!("signal_{i}"))
                    .collect();

                for name in &names {
                    builder.signal(name, EntityKind::Item, DecaySpec::Permanent).add();
                }

                let schema = builder.build().unwrap();
                prop_assert_eq!(schema.signal_count(), count);
                for name in &names {
                    prop_assert!(schema.signal(name).is_some());
                }
            }
        }
    }
}
