//! RLHF training data export and cross-session aggregation types.

use std::collections::{HashMap, HashSet};

use crate::schema::{SchemaError, SignalTypeDef, TidalError};

// ── ExportFormat ──────────────────────────────────────────────────────────────

/// Output format for exported signals.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExportFormat {
    /// One JSON object per line, no enclosing array.
    JsonLines,
}

// ── ExportRequest ─────────────────────────────────────────────────────────────

/// Request parameters for signal export.
#[derive(Debug, Clone)]
pub struct ExportRequest {
    /// Filter to signals for a specific user. `None` means all users.
    pub user_id: Option<u64>,
    /// Signal type names to include. Empty means all signal types.
    pub signal_types: Vec<String>,
    /// Start of the time range (inclusive). Nanosecond timestamp.
    pub since: Option<u64>,
    /// End of the time range (exclusive). Nanosecond timestamp.
    pub until: Option<u64>,
    /// Output format.
    pub format: ExportFormat,
    /// Maximum number of events to return. `None` means no limit.
    pub limit: Option<usize>,
}

impl ExportRequest {
    /// Maximum number of signals that can be exported in a single request.
    pub const MAX_EXPORT_LIMIT: usize = 500_000;
    /// Default limit applied when `limit` is `None`.
    pub const DEFAULT_LIMIT: usize = 100_000;
}

impl ExportRequest {
    /// Create a request for all signals in a time range.
    #[must_use]
    pub const fn time_range(since: u64, until: u64) -> Self {
        Self {
            user_id: None,
            signal_types: Vec::new(),
            since: Some(since),
            until: Some(until),
            format: ExportFormat::JsonLines,
            limit: None,
        }
    }

    /// Create a request for specific signal types in a time range.
    #[must_use]
    pub const fn signals_in_range(signal_types: Vec<String>, since: u64, until: u64) -> Self {
        Self {
            user_id: None,
            signal_types,
            since: Some(since),
            until: Some(until),
            format: ExportFormat::JsonLines,
            limit: None,
        }
    }
}

// ── ExportedSignal ────────────────────────────────────────────────────────────

/// A single exported signal event.
#[derive(Debug, Clone, PartialEq)]
pub struct ExportedSignal {
    /// The entity (content item) that received the signal.
    pub entity_id: u64,
    /// The signal type name (e.g., "view", "like", "skip").
    pub signal_type: String,
    /// The signal weight at write time.
    pub weight: f32,
    /// Nanosecond timestamp when the signal was written.
    pub timestamp_ns: u64,
    /// The user who generated the signal, if known (from session journal).
    pub user_id: Option<u64>,
    /// The session in which the signal was written, if known.
    pub session_id: Option<u64>,
    /// Optional annotation attached to the signal (e.g., RLHF label).
    pub annotation: Option<String>,
}

impl ExportedSignal {
    /// Render as a JSON line (no trailing newline).
    ///
    /// Optional fields (`user_id`, `session_id`, `annotation`) are included
    /// only when `Some`, keeping the format compact for anonymous batch signals.
    #[must_use]
    pub fn to_json_line(&self) -> String {
        use std::fmt::Write as _;
        let mut s = format!(
            r#"{{"entity_id":{},"signal_type":"{}","weight":{},"timestamp_ns":{}"#,
            self.entity_id, self.signal_type, self.weight, self.timestamp_ns
        );
        if let Some(uid) = self.user_id {
            let _ = write!(s, r#","user_id":{uid}"#);
        }
        if let Some(sid) = self.session_id {
            let _ = write!(s, r#","session_id":{sid}"#);
        }
        if let Some(ref ann) = self.annotation {
            // Escape double quotes and backslashes in annotation text.
            let escaped = ann.replace('\\', "\\\\").replace('"', "\\\"");
            let _ = write!(s, r#","annotation":"{escaped}""#);
        }
        s.push('}');
        s
    }
}

// ── UserSessionSummary ────────────────────────────────────────────────────────

/// Aggregate summary of a user's session history.
///
/// Produced by `TidalDb::user_session_summary()` (Task 09). The struct is
/// defined here so the export module owns all cross-session analysis types.
#[derive(Debug, Clone)]
pub struct UserSessionSummary {
    /// Number of closed sessions for this user in the time range.
    pub sessions_count: u64,
    /// Total signal writes across all matching sessions.
    pub total_signals: u64,
    /// Total policy rejections across all matching sessions.
    pub total_rejections: u64,
    /// Top signal types by frequency, sorted descending. Limited to top 10.
    pub top_signal_types: Vec<(String, u64)>,
    /// Cosine distance between earliest and latest preference vectors.
    /// `None` if no preference vector snapshots are available.
    pub preference_drift: Option<f64>,
    /// The user ID this summary is for.
    pub user_id: u64,
    /// The `since_ns` filter applied.
    pub since_ns: u64,
    /// Nanosecond timestamp of the earliest matching session start.
    pub earliest_session_ns: Option<u64>,
    /// Nanosecond timestamp of the latest matching session close.
    pub latest_session_ns: Option<u64>,
}

// ── cosine_distance ──────────────────────────────────────────────────────────

/// Cosine distance: `1.0 - cosine_similarity`.
///
/// Returns `0.0` for identical vectors, up to `2.0` for opposite vectors.
/// Returns `0.0` if either input is a zero vector (avoids division by zero).
#[must_use]
#[allow(dead_code)] // Used by unit tests; will be called from preference drift computation.
pub fn cosine_distance(a: &[f32], b: &[f32]) -> f64 {
    debug_assert_eq!(a.len(), b.len());
    let mut dot = 0.0_f64;
    let mut norm_a = 0.0_f64;
    let mut norm_b = 0.0_f64;
    for (x, y) in a.iter().zip(b.iter()) {
        let xf = f64::from(*x);
        let yf = f64::from(*y);
        dot += xf * yf;
        norm_a += xf * xf;
        norm_b += yf * yf;
    }
    let denom = norm_a.sqrt() * norm_b.sqrt();
    if denom < f64::EPSILON {
        return 0.0;
    }
    1.0 - (dot / denom)
}

// ── TidalDb::export_signals ───────────────────────────────────────────────────

impl super::TidalDb {
    /// Export signal events from the WAL for offline analysis or RLHF training.
    ///
    /// Reads WAL segment files directly (all segments, not just post-checkpoint).
    /// Does not interfere with the live write path.
    ///
    /// # Errors
    ///
    /// - `TidalError::Internal` if WAL segments cannot be read.
    /// - `TidalError::Schema(UnknownSignalType)` if a requested signal type
    ///   name is not defined in the schema.
    pub fn export_signals(&self, request: &ExportRequest) -> crate::Result<Vec<ExportedSignal>> {
        // Validate export limit.
        if let Some(limit) = request.limit
            && limit > ExportRequest::MAX_EXPORT_LIMIT
        {
            return Err(TidalError::invalid_input(format!(
                "export limit {limit} exceeds maximum {}",
                ExportRequest::MAX_EXPORT_LIMIT
            )));
        }

        // Apply default limit when none is specified.
        let effective_limit = request.limit.unwrap_or(ExportRequest::DEFAULT_LIMIT);

        // Build signal type name lookup: ID -> name (sorted alphabetically,
        // matching the ID assignment order in SignalLedger::new).
        let type_names: HashMap<u8, String> =
            self.schema_def
                .as_ref()
                .map_or_else(HashMap::new, |schema| {
                    let mut signal_list: Vec<&SignalTypeDef> = schema.signals().collect();
                    signal_list.sort_by_key(|s| s.name());
                    signal_list
                        .iter()
                        .enumerate()
                        .map(|(i, def)| {
                            debug_assert!(
                                i < 256,
                                "more than 256 signal types defined; index overflows u8"
                            );
                            (i as u8, def.name().to_owned())
                        })
                        .collect()
                });

        // Build type filter set: if signal_types non-empty, resolve to u8 IDs.
        let type_filter: HashSet<u8> = if request.signal_types.is_empty() {
            HashSet::new()
        } else {
            let mut filter = HashSet::with_capacity(request.signal_types.len());
            for name in &request.signal_types {
                let id = type_names
                    .iter()
                    .find(|(_, n)| *n == name)
                    .map(|(id, _)| *id)
                    .ok_or_else(|| {
                        TidalError::Schema(SchemaError::UnknownSignalType(name.clone()))
                    })?;
                filter.insert(id);
            }
            filter
        };

        // Find WAL directory.
        let Some(wal_dir) = self.config.data_dir.as_ref().map(|d| d.join("wal")) else {
            // Ephemeral mode: no WAL on disk.
            return Ok(Vec::new());
        };

        if !wal_dir.exists() {
            return Ok(Vec::new());
        }

        // Batch WAL events: skip entirely when user_id filter is set because
        // batch WAL records have no user context — including them would pollute
        // user-filtered results with anonymous events.
        let mut results = Vec::new();
        if request.user_id.is_none() {
            let all_events = crate::wal::reader::read_all_events(&wal_dir)
                .map_err(|e| TidalError::internal("export_signals", e.to_string()))?;

            for event in all_events {
                // Time range filter: since is inclusive, until is exclusive.
                if let Some(since) = request.since
                    && event.timestamp_nanos < since
                {
                    continue;
                }
                if let Some(until) = request.until
                    && event.timestamp_nanos >= until
                {
                    continue;
                }

                // Signal type filter: empty filter means all types pass.
                if !type_filter.is_empty() && !type_filter.contains(&event.signal_type) {
                    continue;
                }

                // Resolve signal type name from the u8 ID.
                let signal_type_name = type_names
                    .get(&event.signal_type)
                    .cloned()
                    .unwrap_or_else(|| format!("unknown:{}", event.signal_type));

                results.push(ExportedSignal {
                    entity_id: event.entity_id,
                    signal_type: signal_type_name,
                    weight: event.weight,
                    timestamp_ns: event.timestamp_nanos,
                    user_id: None,
                    session_id: None,
                    annotation: None,
                });
            }
        }

        // Session journal events: always read (they carry user context).
        let session_signals = read_session_journal_signals(&wal_dir, request);
        results.extend(session_signals);

        // Sort merged results by timestamp ascending for deterministic output.
        results.sort_by_key(|s| s.timestamp_ns);

        // Apply limit across the merged, sorted result.
        results.truncate(effective_limit);

        Ok(results)
    }
}

// ── read_session_journal_signals ──────────────────────────────────────────────

/// Read session journal signals from disk and apply the same filters as the
/// batch WAL path (time range, signal type name, user ID).
///
/// Returns an empty vec if the session journal does not exist or cannot be read
/// (non-fatal — the batch WAL results are still valid).
fn read_session_journal_signals(
    wal_dir: &std::path::Path,
    request: &ExportRequest,
) -> Vec<ExportedSignal> {
    use crate::wal::format::{SessionWalEvent, decode_session_events};
    use crate::wal::session_journal::SESSION_JOURNAL_FILENAME;

    let journal_path = wal_dir.join(SESSION_JOURNAL_FILENAME);
    if !journal_path.exists() {
        return Vec::new();
    }

    let Ok(bytes) = std::fs::read(&journal_path) else {
        return Vec::new();
    };

    let events = decode_session_events(&bytes);

    // First pass: build session_id -> user_id mapping from Start events.
    let mut session_user: HashMap<u64, u64> = HashMap::new();
    for event in &events {
        if let SessionWalEvent::Start {
            session_id,
            user_id,
            ..
        } = event
        {
            session_user.insert(*session_id, *user_id);
        }
    }

    // Build signal type name filter set for string-based matching.
    let type_filter_set: HashSet<&str> = request.signal_types.iter().map(String::as_str).collect();

    // Second pass: collect matching Signal events.
    let mut results = Vec::new();
    for event in &events {
        if let SessionWalEvent::Signal {
            session_id,
            entity_id,
            weight,
            ts_ns,
            signal_name,
            annotation,
            ..
        } = event
        {
            // User filter: look up user_id from session start mapping.
            let mapped_user_id = session_user.get(session_id).copied();
            if let Some(filter_uid) = request.user_id
                && mapped_user_id != Some(filter_uid)
            {
                continue;
            }

            // Time range filter: since is inclusive, until is exclusive.
            if let Some(since) = request.since
                && *ts_ns < since
            {
                continue;
            }
            if let Some(until) = request.until
                && *ts_ns >= until
            {
                continue;
            }

            // Signal type filter: if non-empty, check signal_name against the list.
            if !type_filter_set.is_empty() && !type_filter_set.contains(signal_name.as_str()) {
                continue;
            }

            results.push(ExportedSignal {
                entity_id: *entity_id,
                signal_type: signal_name.clone(),
                weight: *weight,
                timestamp_ns: *ts_ns,
                user_id: mapped_user_id,
                session_id: Some(*session_id),
                annotation: annotation.clone(),
            });
        }
    }

    results
}

// ── TidalDb::user_session_summary ────────────────────────────────────────────

impl super::TidalDb {
    /// Compute an aggregate summary of a user's closed session history.
    ///
    /// Scans `closed_sessions` in memory for sessions belonging to `user_id`
    /// that started at or after `since_ns`.
    ///
    /// # Errors
    ///
    /// Returns `TidalError::NotFound` if no closed sessions exist for the
    /// specified user in the given time range.
    pub fn user_session_summary(
        &self,
        user_id: u64,
        since_ns: u64,
    ) -> crate::Result<UserSessionSummary> {
        use crate::schema::{EntityId, EntityKind};

        let mut sessions_count: u64 = 0;
        let mut total_signals: u64 = 0;
        let mut total_rejections: u64 = 0;
        let mut signal_freq: std::collections::HashMap<String, u64> =
            std::collections::HashMap::new();
        let mut earliest_ns: Option<u64> = None;
        let mut latest_ns: Option<u64> = None;

        for entry in &self.closed_sessions {
            let snapshot = entry.value();
            if snapshot.user_id != user_id {
                continue;
            }
            if snapshot.started_at_ns < since_ns {
                continue;
            }

            sessions_count += 1;
            total_signals += snapshot.signals_written;
            total_rejections += snapshot.signals_rejected;

            // Accumulate signal type frequencies from the per-signal count.
            for (signal_name, signal_entry) in &snapshot.signals {
                *signal_freq.entry(signal_name.clone()).or_insert(0) += signal_entry.count;
            }

            // Track time range.
            earliest_ns = Some(earliest_ns.map_or(snapshot.started_at_ns, |e: u64| {
                e.min(snapshot.started_at_ns)
            }));
            latest_ns = Some(
                latest_ns.map_or(snapshot.closed_at_ns, |l: u64| l.max(snapshot.closed_at_ns)),
            );
        }

        if sessions_count == 0 {
            return Err(TidalError::NotFound {
                kind: EntityKind::User,
                id: EntityId::new(user_id),
            });
        }

        // Top signal types sorted descending by count, limited to 10.
        let mut top_signal_types: Vec<(String, u64)> = signal_freq.into_iter().collect();
        top_signal_types.sort_by(|a, b| b.1.cmp(&a.1));
        top_signal_types.truncate(10);

        // Preference drift: None for now (no historical preference snapshots).
        let preference_drift = None;

        Ok(UserSessionSummary {
            sessions_count,
            total_signals,
            total_rejections,
            top_signal_types,
            preference_drift,
            user_id,
            since_ns,
            earliest_session_ns: earliest_ns,
            latest_session_ns: latest_ns,
        })
    }
}

// ── Tests ─────────────────────────────────────────────────────────────────────

#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[path = "export_tests.rs"]
mod tests;
