use std::collections::HashMap;

use crate::schema::error::SchemaError;
use crate::schema::{DecayModel, EntityKind, SignalTypeDef, Window, WindowSet};

use super::policies::{AgentPolicy, PolicyEntry};
use super::text::{EmbeddingSlotDef, TextFieldDef, TextFieldType};
use super::{DecaySpec, Schema};

/// Internal entry for a signal being built.
#[derive(Debug)]
struct SignalEntry {
    name: String,
    target: EntityKind,
    decay: DecaySpec,
    windows: Vec<Window>,
    velocity: bool,
}

/// Builder for constructing a validated `Schema`.
///
/// # Example
///
/// ```ignore
/// let mut builder = SchemaBuilder::new();
/// builder.signal("view", EntityKind::Item, DecaySpec::Exponential {
///     half_life: Duration::from_secs(604_800),
/// })
///     .windows(&[Window::OneHour, Window::TwentyFourHours])
///     .velocity(true)
///     .add();
/// let schema = builder.build()?;
/// ```
#[derive(Debug)]
pub struct SchemaBuilder {
    entries: Vec<SignalEntry>,
    embedding_slots: Vec<EmbeddingSlotDef>,
    text_fields: Vec<TextFieldDef>,
    creator_text_fields: Vec<TextFieldDef>,
    policies: Vec<PolicyEntry>,
}

impl SchemaBuilder {
    #[must_use]
    pub const fn new() -> Self {
        Self {
            entries: Vec::new(),
            embedding_slots: Vec::new(),
            text_fields: Vec::new(),
            creator_text_fields: Vec::new(),
            policies: Vec::new(),
        }
    }

    /// Declare an embedding vector slot.
    pub fn embedding_slot(
        &mut self,
        name: &str,
        entity_kind: EntityKind,
        dimensions: usize,
    ) -> &mut Self {
        self.embedding_slots.push(EmbeddingSlotDef {
            name: name.to_owned(),
            entity_kind,
            dimensions,
        });
        self
    }

    /// Declare a text field for full-text search indexing (items).
    ///
    /// Each text field maps a metadata key to a Tantivy indexing mode.
    /// `TextFieldType::Text` enables tokenized full-text search;
    /// `TextFieldType::Keyword` enables exact-match filtering.
    pub fn text_field(&mut self, key: &str, field_type: TextFieldType) -> &mut Self {
        self.text_fields.push(TextFieldDef {
            key: key.to_owned(),
            field_type,
        });
        self
    }

    /// Declare a text field for creator full-text search indexing.
    ///
    /// Same semantics as `text_field()` but populates the creator Tantivy index.
    /// Use for creator metadata keys like "name", "handle", "bio".
    pub fn creator_text_field(&mut self, key: &str, field_type: TextFieldType) -> &mut Self {
        self.creator_text_fields.push(TextFieldDef {
            key: key.to_owned(),
            field_type,
        });
        self
    }

    /// Declare a session policy for agent sessions.
    ///
    /// Policies are validated at `build()` time: all signal names in
    /// `allowed_signals` and `denied_signals` must exist in the schema.
    ///
    /// Call `session_policy()` after all `signal()` declarations, or validation
    /// at `build()` time will catch any missing signal references.
    pub fn session_policy(&mut self, name: &str, policy: AgentPolicy) -> &mut Self {
        self.policies.push(PolicyEntry {
            name: name.to_owned(),
            policy,
        });
        self
    }

    /// Register the `viewer_count` signal for live content ranking.
    ///
    /// Exponential decay with 5-minute half-life, no windowed aggregation,
    /// no velocity. Used by the `LiveViewerCount` sort mode to rank items
    /// by current live audience size.
    ///
    /// This is a convenience method; the signal can also be registered
    /// manually via `signal("viewer_count", ...)`.
    pub fn with_viewer_count_signal(&mut self) -> &mut Self {
        let _ = self
            .signal(
                "viewer_count",
                EntityKind::Item,
                DecaySpec::Exponential {
                    half_life: std::time::Duration::from_secs(300), // 5 minutes
                },
            )
            .windows(&[Window::AllTime])
            .velocity(false)
            .add();
        self
    }

    /// Begin defining a signal type. Returns a `SignalBuilder` for configuring
    /// windows and velocity before calling `.add()`.
    pub fn signal(
        &mut self,
        name: &str,
        target: EntityKind,
        decay: DecaySpec,
    ) -> SignalBuilder<'_> {
        SignalBuilder {
            builder: self,
            entry: SignalEntry {
                name: name.to_owned(),
                target,
                decay,
                windows: Vec::new(),
                velocity: false,
            },
        }
    }

    /// Validate all entries and produce an immutable `Schema`.
    ///
    /// # Errors
    ///
    /// Returns `SchemaError` if any validation rule fails:
    /// - `NoSignalsDefined` if no signals were added
    /// - `InvalidSignalName` if a name is not a valid identifier
    /// - `DuplicateSignalName` if two signals share a name
    /// - `InvalidHalfLife` if exponential decay has zero/negative half-life
    /// - `InvalidLifetime` if linear decay has zero/negative lifetime
    /// - `EmptyWindows` if a non-permanent signal has no windows
    /// - `VelocityWithoutWindows` if velocity is enabled without windows
    pub fn build(self) -> Result<Schema, SchemaError> {
        if self.entries.is_empty() {
            return Err(SchemaError::NoSignalsDefined);
        }

        let mut signals = HashMap::with_capacity(self.entries.len());

        for entry in self.entries {
            // Name validation
            if !super::is_valid_signal_name(&entry.name) {
                return Err(SchemaError::InvalidSignalName(entry.name));
            }

            // Duplicate check
            if signals.contains_key(&entry.name) {
                return Err(SchemaError::DuplicateSignalName(entry.name));
            }

            // Decay-specific validation and conversion
            let decay_model = match &entry.decay {
                DecaySpec::Exponential { half_life } => {
                    let secs = half_life.as_secs_f64();
                    if secs <= 0.0 || !secs.is_finite() {
                        return Err(SchemaError::InvalidHalfLife {
                            signal_name: entry.name,
                            half_life_secs: secs,
                        });
                    }
                    DecayModel::exponential(*half_life)
                }
                DecaySpec::Linear { lifetime } => {
                    let secs = lifetime.as_secs_f64();
                    if secs <= 0.0 || !secs.is_finite() {
                        return Err(SchemaError::InvalidLifetime {
                            signal_name: entry.name,
                            lifetime_secs: secs,
                        });
                    }
                    DecayModel::linear(*lifetime)
                }
                DecaySpec::Permanent => DecayModel::Permanent,
            };

            // Window check for non-permanent signals
            let is_permanent = matches!(entry.decay, DecaySpec::Permanent);
            if !is_permanent && entry.windows.is_empty() {
                return Err(SchemaError::EmptyWindows {
                    signal_name: entry.name,
                });
            }

            // Velocity check
            if entry.velocity && entry.windows.is_empty() {
                return Err(SchemaError::VelocityWithoutWindows {
                    signal_name: entry.name,
                });
            }

            // Construct validated types
            let windows = WindowSet::new(&entry.windows);
            let signal_def = SignalTypeDef::new(
                entry.name.clone(),
                entry.target,
                decay_model,
                windows,
                entry.velocity,
            );

            signals.insert(entry.name, signal_def);
        }

        // Validate and collect session policies.
        let mut seen_policy_names = std::collections::HashSet::new();
        let mut policies = HashMap::new();
        for entry in self.policies {
            if !super::is_valid_signal_name(&entry.name) {
                return Err(SchemaError::InvalidPolicyName(entry.name));
            }
            if !seen_policy_names.insert(entry.name.clone()) {
                return Err(SchemaError::DuplicatePolicyName(entry.name));
            }
            // Validate all signal names referenced in the policy exist in schema.
            for sig in entry
                .policy
                .allowed_signals
                .iter()
                .chain(entry.policy.denied_signals.iter())
            {
                if !signals.contains_key(sig) {
                    return Err(SchemaError::PolicySignalNotInSchema {
                        policy: entry.name.clone(),
                        signal: sig.clone(),
                    });
                }
            }
            // Validate no signal appears in both allow and deny lists.
            for sig in &entry.policy.allowed_signals {
                if entry.policy.denied_signals.contains(sig) {
                    return Err(SchemaError::PolicySignalConflict {
                        policy: entry.name.clone(),
                        signal: sig.clone(),
                    });
                }
            }
            policies.insert(entry.name, entry.policy);
        }

        Ok(Schema::new(
            signals,
            self.embedding_slots,
            self.text_fields,
            self.creator_text_fields,
            policies,
        ))
    }
}

impl Default for SchemaBuilder {
    fn default() -> Self {
        Self::new()
    }
}

/// Intermediate builder for configuring a single signal type.
///
/// Created by `SchemaBuilder::signal()`. Call `.windows()` and `.velocity()`
/// to configure, then `.add()` to finalize and return to the schema builder.
#[derive(Debug)]
#[must_use = "call .add() to include this signal in the schema"]
pub struct SignalBuilder<'a> {
    builder: &'a mut SchemaBuilder,
    entry: SignalEntry,
}

impl<'a> SignalBuilder<'a> {
    /// Set the time windows for this signal.
    pub fn windows(mut self, windows: &[Window]) -> Self {
        self.entry.windows = windows.to_vec();
        self
    }

    /// Enable or disable velocity computation.
    pub const fn velocity(mut self, enabled: bool) -> Self {
        self.entry.velocity = enabled;
        self
    }

    /// Finalize this signal definition and return to the schema builder.
    #[must_use]
    pub fn add(self) -> &'a mut SchemaBuilder {
        self.builder.entries.push(self.entry);
        self.builder
    }
}

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

    // === Validation: valid schemas ===

    #[test]
    fn valid_schema_round_trip() {
        let mut builder = SchemaBuilder::new();
        builder
            .signal(
                "view",
                EntityKind::Item,
                DecaySpec::Exponential {
                    half_life: Duration::from_secs(604_800),
                },
            )
            .windows(&[
                Window::OneHour,
                Window::TwentyFourHours,
                Window::SevenDays,
                Window::AllTime,
            ])
            .velocity(true)
            .add();
        builder
            .signal("hide", EntityKind::Item, DecaySpec::Permanent)
            .add();

        let schema = builder.build().expect("valid schema");

        assert_eq!(schema.signal_count(), 2);

        let view = schema.signal("view").unwrap();
        assert_eq!(view.name(), "view");
        assert_eq!(view.target(), EntityKind::Item);
        assert!(view.velocity_enabled());
        assert_eq!(view.windows().len(), 4);
        assert!(view.decay().lambda().is_some());

        let hide = schema.signal("hide").unwrap();
        assert_eq!(hide.windows().len(), 0);
        assert!(!hide.velocity_enabled());
        assert_eq!(*hide.decay(), DecayModel::Permanent);
    }

    #[test]
    fn accepts_permanent_with_empty_windows() {
        let mut builder = SchemaBuilder::new();
        builder
            .signal("hide", EntityKind::Item, DecaySpec::Permanent)
            .add();
        let result = builder.build();
        assert!(result.is_ok());
    }

    #[test]
    fn accepts_valid_signal_names() {
        let names = [
            "view",
            "like",
            "skip",
            "hide",
            "search_click",
            "autoplay_accept",
            "view_24h",
        ];
        for name in names {
            let mut builder = SchemaBuilder::new();
            builder
                .signal(name, EntityKind::Item, DecaySpec::Permanent)
                .add();
            let r = builder.build();
            assert!(r.is_ok(), "should accept signal name '{name}'");
        }
    }

    #[test]
    fn accepts_linear_decay() {
        let mut builder = SchemaBuilder::new();
        builder
            .signal(
                "impression",
                EntityKind::Item,
                DecaySpec::Linear {
                    lifetime: Duration::from_secs(86_400),
                },
            )
            .windows(&[Window::TwentyFourHours])
            .add();
        let schema = builder.build().expect("valid linear decay schema");
        let sig = schema.signal("impression").unwrap();
        assert!(sig.decay().lambda().is_none());
    }

    // === Validation: rejections ===

    #[test]
    fn rejects_duplicate_signal_name() {
        let mut builder = SchemaBuilder::new();
        builder
            .signal(
                "view",
                EntityKind::Item,
                DecaySpec::Exponential {
                    half_life: Duration::from_secs(604_800),
                },
            )
            .windows(&[Window::AllTime])
            .add();
        builder
            .signal(
                "view",
                EntityKind::Item,
                DecaySpec::Exponential {
                    half_life: Duration::from_secs(86_400),
                },
            )
            .windows(&[Window::AllTime])
            .add();
        let result = builder.build();
        assert!(matches!(
            result,
            Err(SchemaError::DuplicateSignalName(ref name)) if name == "view"
        ));
    }

    #[test]
    fn rejects_zero_half_life() {
        let mut builder = SchemaBuilder::new();
        builder
            .signal(
                "bad",
                EntityKind::Item,
                DecaySpec::Exponential {
                    half_life: Duration::ZERO,
                },
            )
            .windows(&[Window::AllTime])
            .add();
        let result = builder.build();
        assert!(matches!(result, Err(SchemaError::InvalidHalfLife { .. })));
    }

    #[test]
    fn rejects_zero_linear_lifetime() {
        let mut builder = SchemaBuilder::new();
        builder
            .signal(
                "bad",
                EntityKind::Item,
                DecaySpec::Linear {
                    lifetime: Duration::ZERO,
                },
            )
            .windows(&[Window::AllTime])
            .add();
        let result = builder.build();
        assert!(matches!(result, Err(SchemaError::InvalidLifetime { .. })));
    }

    #[test]
    fn rejects_empty_windows_on_exponential() {
        let mut builder = SchemaBuilder::new();
        builder
            .signal(
                "bad",
                EntityKind::Item,
                DecaySpec::Exponential {
                    half_life: Duration::from_secs(3600),
                },
            )
            .add(); // no windows
        let result = builder.build();
        assert!(matches!(result, Err(SchemaError::EmptyWindows { .. })));
    }

    #[test]
    fn rejects_empty_windows_on_linear() {
        let mut builder = SchemaBuilder::new();
        builder
            .signal(
                "bad",
                EntityKind::Item,
                DecaySpec::Linear {
                    lifetime: Duration::from_secs(3600),
                },
            )
            .add(); // no windows
        let result = builder.build();
        assert!(matches!(result, Err(SchemaError::EmptyWindows { .. })));
    }

    #[test]
    fn rejects_velocity_without_windows() {
        let mut builder = SchemaBuilder::new();
        builder
            .signal("bad", EntityKind::Item, DecaySpec::Permanent)
            .velocity(true)
            .add();
        let result = builder.build();
        assert!(matches!(
            result,
            Err(SchemaError::VelocityWithoutWindows { .. })
        ));
    }

    #[test]
    fn rejects_empty_schema() {
        let result = SchemaBuilder::new().build();
        assert!(matches!(result, Err(SchemaError::NoSignalsDefined)));
    }

    #[test]
    fn rejects_invalid_signal_names() {
        let invalid = [
            "",           // empty
            "View",       // uppercase
            "1view",      // starts with digit
            "view count", // space
            "view-count", // hyphen
            "_view",      // starts with underscore
            "view!",      // special character
        ];
        for name in invalid {
            let mut builder = SchemaBuilder::new();
            builder
                .signal(name, EntityKind::Item, DecaySpec::Permanent)
                .add();
            let r = builder.build();
            assert!(
                matches!(r, Err(SchemaError::InvalidSignalName(_))),
                "should reject signal name '{name}'"
            );
        }
    }
}
