// SPDX-License-Identifier: MIT
//! Boot prompt compiler — capsule-based system.
//!
//! Ported from `src/compiler.js` (707 lines).  Builds an identity capsule
//! (stable, ~200 tokens) and a delta capsule (what changed since last boot,
//! ~300 tokens), then assembles them within a token budget.
//!
//! The compiler also reads state.md, memory files, lessons, conductor state
//! (sessions, locks, feed, tasks, messages), and recent decisions/memories
//! to produce a compact, high-signal boot prompt.

use std::collections::HashSet;
use std::env;
use std::path::Path;

use chrono::{DateTime, NaiveDateTime, TimeZone, Utc};
use regex::Regex;
use rusqlite::{params, Connection, OptionalExtension};
use serde_json::{json, Value};

use crate::handlers::{estimate_tokens, estimate_tokens_from_chars};

const DEFAULT_BOOT_MIN_SOURCE_TOKENS: usize = 40;
const DEFAULT_BOOT_MAX_SOURCE_TOKENS: usize = 600;
const DEFAULT_BOOT_RANK_TOP_N: usize = 5;
const SCORE_VARIANCE_FLAT_THRESHOLD: f64 = 0.0001;
const RANK_WEIGHT_CLASS: f64 = 0.30;
const RANK_WEIGHT_RECENCY: f64 = 0.30;
const RANK_WEIGHT_RELEVANCE: f64 = 0.25;
const RANK_WEIGHT_ACTIVITY: f64 = 0.15;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum BootPackingMode {
    Auto,
    LegacyGreedy,
    ScoreAdaptive,
}

fn boot_packing_mode() -> BootPackingMode {
    match env::var("CORTEX_BOOT_PACKING_MODE")
        .unwrap_or_default()
        .trim()
        .to_ascii_lowercase()
        .as_str()
    {
        "legacy" | "greedy" | "v0.5" | "v0.5.0" => BootPackingMode::LegacyGreedy,
        "adaptive" | "score-adaptive" | "score_adaptive" => BootPackingMode::ScoreAdaptive,
        _ => BootPackingMode::Auto,
    }
}

fn detect_identity() -> String {
    let user = env::var("USERNAME")
        .or_else(|_| env::var("USER"))
        .unwrap_or_else(|_| "cortex-user".to_string());

    let platform = match env::consts::OS {
        "windows" => "Windows",
        "macos" => "macOS",
        "linux" => "Linux",
        other => other,
    };

    let shell = env::var("SHELL")
        .or_else(|_| env::var("COMSPEC"))
        .map(|s| s.rsplit(['/', '\\']).next().unwrap_or(&s).to_string())
        .unwrap_or_else(|_| "unknown".to_string());

    format!("User: {user}. Platform: {platform}. Shell: {shell}.")
}

// ─── Public types ───────────────────────────────────────────────────────────

/// The assembled boot prompt and its metadata.
pub struct BootResult {
    pub boot_prompt: String,
    pub token_estimate: usize,
    pub savings: Value,
    pub capsules: Vec<Value>,
}

// ─── Token estimation ───────────────────────────────────────────────────────

// Estimate tokens from character length (~3.8 chars/token, matching Node.js).
// ─── Content-addressed cache ────────────────────────────────────────────────

/// Compute a fast content hash for cache invalidation.
/// Uses FNV-1a for speed (not crypto-secure, just change detection).
fn content_hash(data: &str) -> String {
    let mut hash: u64 = 0xcbf29ce484222325;
    for byte in data.bytes() {
        hash ^= byte as u64;
        hash = hash.wrapping_mul(0x100000001b3);
    }
    format!("{hash:016x}")
}

/// Check the context cache for a cached result.
fn cache_get(conn: &Connection, key: &str, expected_hash: &str) -> Option<(String, usize)> {
    conn.query_row(
        "SELECT compressed, tokens, content_hash FROM context_cache WHERE cache_key = ?1",
        params![key],
        |row| {
            let compressed: String = row.get(0)?;
            let tokens: usize = row.get::<_, i64>(1)? as usize;
            let stored_hash: String = row.get(2)?;
            Ok((compressed, tokens, stored_hash))
        },
    )
    .ok()
    .and_then(|(compressed, tokens, stored_hash)| {
        if stored_hash == expected_hash {
            // Cache hit -- bump hit count
            let _ = conn.execute(
                "UPDATE context_cache SET hits = hits + 1 WHERE cache_key = ?1",
                params![key],
            );
            Some((compressed, tokens))
        } else {
            None // Hash mismatch -- content changed
        }
    })
}

/// Store a compiled result in the cache.
fn cache_set(conn: &Connection, key: &str, hash: &str, compressed: &str, tokens: usize) {
    let _ = conn.execute(
        "INSERT OR REPLACE INTO context_cache (cache_key, content_hash, compressed, tokens) \
         VALUES (?1, ?2, ?3, ?4)",
        params![key, hash, compressed, tokens as i64],
    );
}

// State.md helpers removed — session-auto-restore.js handles state.md injection.

// ─── Identity capsule ───────────────────────────────────────────────────────

/// Build the identity capsule — stable across sessions, ~200 tokens.
/// Contains core user identity, hard constraints, and platform sharp edges.
/// Uses content-addressed cache: if feedback memories haven't changed, reuse.
fn build_identity_capsule(conn: &Connection) -> (String, usize) {
    // Compute hash of the feedback memories that feed this capsule
    let feedback_hash = {
        let mut all_feedback = String::new();
        if let Ok(mut stmt) = conn.prepare(
            "SELECT text FROM memories WHERE type = 'feedback' AND status = 'active' ORDER BY id",
        ) {
            if let Ok(rows) = stmt.query_map([], |r| r.get::<_, String>(0)) {
                for text in rows.flatten() {
                    all_feedback.push_str(&text);
                    all_feedback.push('\n');
                }
            }
        }
        content_hash(&all_feedback)
    };

    // Check cache
    if let Some((cached, tokens)) = cache_get(conn, "identity_capsule", &feedback_hash) {
        return (cached, tokens);
    }
    let mut parts = vec![detect_identity()];

    // Hard constraints (never/always/must rules)
    if let Ok(constraint_re) =
        Regex::new(r"(?i)\b(never|always|must|do not|don't|required|mandatory)\b")
    {
        if let Ok(mut stmt) = conn.prepare(
            "SELECT text FROM memories WHERE type = 'feedback' AND status = 'active' ORDER BY score DESC LIMIT 20",
        ) {
            if let Ok(rows) = stmt.query_map([], |r| r.get::<_, String>(0)) {
                let constraints: Vec<String> = rows
                    .filter_map(|r| r.ok())
                    .filter(|t| constraint_re.is_match(t))
                    .take(5)
                    .map(|t| t.chars().take(120).collect::<String>())
                    .collect();
                if !constraints.is_empty() {
                    parts.push(format!("Rules: {}", constraints.join(" | ")));
                }
            }
        }
    }

    // Platform sharp edges (Windows-specific gotchas)
    if let Ok(edge_re) = Regex::new(r"(?i)\b(windows|win32|encoding|cp1252|bash\.exe|CRLF)\b") {
        if let Ok(mut stmt) = conn.prepare(
            "SELECT text FROM memories WHERE type = 'feedback' AND status = 'active' ORDER BY score DESC LIMIT 20",
        ) {
            if let Ok(rows) = stmt.query_map([], |r| r.get::<_, String>(0)) {
                let edges: Vec<String> = rows
                    .filter_map(|r| r.ok())
                    .filter(|t| edge_re.is_match(t))
                    .take(3)
                    .map(|t| t.chars().take(100).collect::<String>())
                    .collect();
                if !edges.is_empty() {
                    parts.push(format!("Sharp edges: {}", edges.join(" | ")));
                }
            }
        }
    }

    let text = parts.join("\n");
    let tokens = estimate_tokens(&text);

    // Cache the result for next boot
    cache_set(conn, "identity_capsule", &feedback_hash, &text, tokens);

    (text, tokens)
}

// ─── Last boot time ─────────────────────────────────────────────────────────

fn get_last_boot_time(conn: &Connection, agent: &str) -> Option<String> {
    conn.query_row(
        "SELECT data FROM events WHERE type = 'agent_boot' AND source_agent = ?1 ORDER BY created_at DESC LIMIT 1",
        params![agent],
        |r| r.get::<_, String>(0),
    )
    .ok()
    .and_then(|data| {
        serde_json::from_str::<Value>(&data)
            .ok()?
            .get("timestamp")?
            .as_str()
            .map(|s| s.to_string())
    })
}

// ─── Conductor state helpers ────────────────────────────────────────────────

fn fetch_messages_for_agent(conn: &Connection, agent: &str) -> Vec<Value> {
    let mut out = Vec::new();
    if let Ok(mut stmt) = conn
        .prepare("SELECT sender, message FROM messages WHERE recipient = ?1 ORDER BY timestamp ASC")
    {
        if let Ok(rows) = stmt.query_map(params![agent], |r| {
            Ok(json!({
                "from": r.get::<_, String>(0)?,
                "message": r.get::<_, String>(1)?
            }))
        }) {
            for row in rows.flatten() {
                out.push(row);
            }
        }
    }
    out
}

fn fetch_sessions(conn: &Connection) -> Vec<Value> {
    let mut out = Vec::new();
    if let Ok(mut stmt) = conn.prepare(
        "SELECT agent, project, description, files_json FROM sessions WHERE expires_at > ?1",
    ) {
        let now = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
        if let Ok(rows) = stmt.query_map(params![now], |r| {
            let files_json: String = r.get(3)?;
            Ok(json!({
                "agent": r.get::<_, String>(0)?,
                "project": r.get::<_, Option<String>>(1)?,
                "description": r.get::<_, Option<String>>(2)?,
                "files": serde_json::from_str::<Value>(&files_json).unwrap_or(json!([]))
            }))
        }) {
            for row in rows.flatten() {
                out.push(row);
            }
        }
    }
    out
}

fn fetch_locks(conn: &Connection) -> Vec<Value> {
    let mut out = Vec::new();
    let now = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
    if let Ok(mut stmt) =
        conn.prepare("SELECT path, agent, expires_at FROM locks WHERE expires_at > ?1")
    {
        if let Ok(rows) = stmt.query_map(params![now], |r| {
            Ok(json!({
                "path": r.get::<_, String>(0)?,
                "agent": r.get::<_, String>(1)?,
                "expiresAt": r.get::<_, String>(2)?
            }))
        }) {
            for row in rows.flatten() {
                out.push(row);
            }
        }
    }
    out
}

fn fetch_unread_feed(conn: &Connection, agent: &str) -> Vec<Value> {
    let ack: Option<String> = conn
        .query_row(
            "SELECT last_seen_id FROM feed_acks WHERE agent = ?1",
            params![agent],
            |row| row.get(0),
        )
        .optional()
        .ok()
        .flatten();

    let mut all: Vec<(String, String, String, String)> = Vec::new();
    if let Ok(mut stmt) =
        conn.prepare("SELECT id, agent, kind, summary FROM feed ORDER BY timestamp ASC")
    {
        if let Ok(rows) = stmt.query_map([], |r| {
            Ok((
                r.get::<_, String>(0)?,
                r.get::<_, String>(1)?,
                r.get::<_, String>(2)?,
                r.get::<_, String>(3)?,
            ))
        }) {
            for row in rows.flatten() {
                all.push(row);
            }
        }
    }

    if let Some(ack_id) = ack {
        let mut past_ack = false;
        let mut unread = Vec::new();
        for (id, entry_agent, kind, summary) in all {
            if id == ack_id {
                past_ack = true;
                continue;
            }
            if past_ack && entry_agent != agent {
                unread.push(json!({
                    "kind": kind,
                    "agent": entry_agent,
                    "summary": summary
                }));
            }
        }
        unread
    } else {
        // No ack — all entries from other agents are unread
        all.into_iter()
            .filter(|(_, entry_agent, _, _)| entry_agent != agent)
            .map(|(_, entry_agent, kind, summary)| {
                json!({
                    "kind": kind,
                    "agent": entry_agent,
                    "summary": summary
                })
            })
            .collect()
    }
}

fn fetch_pending_tasks(conn: &Connection) -> Vec<Value> {
    let mut out = Vec::new();
    if let Ok(mut stmt) = conn.prepare(
        "SELECT task_id, title, priority, project, files_json FROM tasks WHERE status = 'pending' ORDER BY created_at ASC LIMIT 5",
    ) {
        if let Ok(rows) = stmt.query_map([], |r| {
            let files_json: String = r.get(4)?;
            Ok(json!({
                "id": r.get::<_, String>(0)?,
                "title": r.get::<_, String>(1)?,
                "priority": r.get::<_, String>(2)?,
                "project": r.get::<_, Option<String>>(3)?,
                "files": serde_json::from_str::<Value>(&files_json).unwrap_or(json!([]))
            }))
        }) {
            for row in rows.flatten() {
                out.push(row);
            }
        }
    }
    out
}

fn fetch_claimed_tasks_for_agent(conn: &Connection, agent: &str) -> Vec<Value> {
    let mut out = Vec::new();
    if let Ok(mut stmt) = conn.prepare(
        "SELECT task_id, title, priority, claimed_at FROM tasks WHERE status = 'claimed' AND claimed_by = ?1 ORDER BY claimed_at ASC",
    ) {
        if let Ok(rows) = stmt.query_map(params![agent], |r| {
            Ok(json!({
                "id": r.get::<_, String>(0)?,
                "title": r.get::<_, String>(1)?,
                "priority": r.get::<_, String>(2)?,
                "claimedAt": r.get::<_, Option<String>>(3)?
            }))
        }) {
            for row in rows.flatten() {
                out.push(row);
            }
        }
    }
    out
}

// ─── Delta capsule ──────────────────────────────────────────────────────────

/// Build the delta capsule — what changed since the agent's last boot.
/// High relevance, changes every session.  Target: ~300 tokens.
fn build_delta_capsule(conn: &Connection, agent: &str) -> (String, usize, String) {
    let last_boot = get_last_boot_time(conn, agent);
    let mut parts: Vec<String> = Vec::new();

    // 0. Pending messages (highest priority)
    let messages = fetch_messages_for_agent(conn, agent);
    if !messages.is_empty() {
        let lines: Vec<String> = messages
            .iter()
            .map(|m| {
                let from = m.get("from").and_then(|v| v.as_str()).unwrap_or("?");
                let msg = m.get("message").and_then(|v| v.as_str()).unwrap_or("");
                let truncated: String = msg.chars().take(200).collect();
                format!("- From {from}: \"{truncated}\"")
            })
            .collect();
        parts.push(format!("## Pending Messages\n{}", lines.join("\n")));
    }

    // 0b. Active agents (session bus — who else is online)
    let sessions = fetch_sessions(conn);
    let other_sessions: Vec<&Value> = sessions
        .iter()
        .filter(|s| s.get("agent").and_then(|v| v.as_str()) != Some(agent))
        .collect();
    if !other_sessions.is_empty() {
        let lines: Vec<String> = other_sessions
            .iter()
            .map(|s| {
                let ag = s.get("agent").and_then(|v| v.as_str()).unwrap_or("?");
                let proj = s
                    .get("project")
                    .and_then(|v| v.as_str())
                    .unwrap_or("unknown");
                let desc = s
                    .get("description")
                    .and_then(|v| v.as_str())
                    .unwrap_or("no description");
                format!("- {ag} working on {proj}: \"{desc}\"")
            })
            .collect();
        parts.push(format!("## Active Agents\n{}", lines.join("\n")));
    }

    // 0c. Active locks
    let locks = fetch_locks(conn);
    if !locks.is_empty() {
        let lines: Vec<String> = locks
            .iter()
            .map(|l| {
                let path = l.get("path").and_then(|v| v.as_str()).unwrap_or("?");
                let ag = l.get("agent").and_then(|v| v.as_str()).unwrap_or("?");
                format!("- {path} locked by {ag}")
            })
            .collect();
        parts.push(format!("## Active Locks\n{}", lines.join("\n")));
    }

    // 0d. Shared feed (unread entries from other agents)
    let mut feed = fetch_unread_feed(conn, agent);
    if feed.len() > 10 {
        feed = feed.split_off(feed.len() - 10);
    }
    if !feed.is_empty() {
        let lines: Vec<String> = feed
            .iter()
            .map(|e| {
                let kind = e.get("kind").and_then(|v| v.as_str()).unwrap_or("?");
                let ag = e.get("agent").and_then(|v| v.as_str()).unwrap_or("?");
                let summary = e.get("summary").and_then(|v| v.as_str()).unwrap_or("");
                format!("- [{kind}] {ag}: {summary}")
            })
            .collect();
        parts.push(format!("## Feed\n{}", lines.join("\n")));
    }

    // 0e. Task board (pending tasks + agent's claimed tasks)
    let pending_tasks = fetch_pending_tasks(conn);
    if !pending_tasks.is_empty() {
        let lines: Vec<String> = pending_tasks
            .iter()
            .map(|t| {
                let pri = t.get("priority").and_then(|v| v.as_str()).unwrap_or("?");
                let title = t.get("title").and_then(|v| v.as_str()).unwrap_or("?");
                format!("- [{pri}] {title}")
            })
            .collect();
        parts.push(format!("## Pending Tasks\n{}", lines.join("\n")));
    }

    let my_tasks = fetch_claimed_tasks_for_agent(conn, agent);
    if !my_tasks.is_empty() {
        let lines: Vec<String> = my_tasks
            .iter()
            .map(|t| {
                let pri = t.get("priority").and_then(|v| v.as_str()).unwrap_or("?");
                let title = t.get("title").and_then(|v| v.as_str()).unwrap_or("?");
                format!("- [{pri}] {title}")
            })
            .collect();
        parts.push(format!("## Your Active Tasks\n{}", lines.join("\n")));
    }

    // 1. Open conflicts (always include — highest priority)
    if let Ok(mut stmt) = conn.prepare(
        "SELECT id, decision, source_agent, disputes_id FROM decisions WHERE status = 'disputed' ORDER BY created_at DESC LIMIT 6",
    ) {
        if let Ok(rows) = stmt.query_map([], |r| {
            Ok((
                r.get::<_, i64>(0)?,
                r.get::<_, String>(1)?,
                r.get::<_, String>(2)?,
                r.get::<_, Option<i64>>(3)?,
            ))
        }) {
            let mut seen = HashSet::new();
            let mut lines: Vec<String> = Vec::new();
            for (id, decision, source_agent, disputes_id) in rows.flatten() {
                if seen.contains(&id) {
                    continue;
                }
                seen.insert(id);
                if let Some(did) = disputes_id {
                    seen.insert(did);
                }

                let mut line = format!("#{id} ({source_agent}): {decision}");
                if let Some(did) = disputes_id {
                    if let Ok((partner_dec, partner_agent)) = conn.query_row(
                        "SELECT decision, source_agent FROM decisions WHERE id = ?1",
                        params![did],
                        |r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)),
                    ) {
                        line.push_str(&format!(
                            " vs #{did} ({partner_agent}): {partner_dec}"
                        ));
                    }
                }
                lines.push(line);
            }
            if !lines.is_empty() {
                parts.push(format!(
                    "CONFLICTS:\n{}",
                    lines.iter().map(|l| format!("- {l}")).collect::<Vec<_>>().join("\n")
                ));
            }
        }
    }

    // 2. Active focus session (sawtooth pattern indicator)
    if let Some(focus) = crate::focus::focus_current(conn, agent) {
        let label = focus.get("label").and_then(|v| v.as_str()).unwrap_or("?");
        let entries = focus.get("entries").and_then(|v| v.as_u64()).unwrap_or(0);
        parts.push(format!("## Active Focus\n- {label} ({entries} entries)"));
    }

    // 3. New decisions since last boot
    if let Some(ref lb) = last_boot {
        if let Ok(mut stmt) = conn.prepare(
            "SELECT decision, context, source_agent FROM decisions WHERE status = 'active' AND created_at >= ?1 ORDER BY created_at DESC LIMIT 5",
        ) {
            if let Ok(rows) = stmt.query_map(params![lb], |r| {
                Ok((
                    r.get::<_, String>(0)?,
                    r.get::<_, Option<String>>(1)?,
                    r.get::<_, String>(2)?,
                ))
            }) {
                let lines: Vec<String> = rows
                    .flatten()
                    .map(|(dec, ctx, ag)| {
                        let c = ctx.map(|c| format!(" ({c})")).unwrap_or_default();
                        format!("- [{ag}] {dec}{c}")
                    })
                    .collect();
                if !lines.is_empty() {
                    parts.push(format!("New decisions:\n{}", lines.join("\n")));
                }
            }
        }

        // 4. New memories since last boot
        if let Ok(mut stmt) = conn.prepare(
            "SELECT text, type FROM memories WHERE status = 'active' AND updated_at >= ?1 AND type != 'state' ORDER BY updated_at DESC LIMIT 3",
        ) {
            if let Ok(rows) = stmt.query_map(params![lb], |r| {
                Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?))
            }) {
                let lines: Vec<String> = rows
                    .flatten()
                    .map(|(text, mtype)| {
                        let truncated: String = text.chars().take(100).collect();
                        format!("- [{mtype}] {truncated}")
                    })
                    .collect();
                if !lines.is_empty() {
                    parts.push(format!("New knowledge:\n{}", lines.join("\n")));
                }
            }
        }

        // 5. Events since last boot (summarized)
        if let Ok(mut stmt) = conn.prepare(
            "SELECT type, COUNT(*) as cnt FROM events WHERE created_at > ?1 AND type NOT IN ('brain_init', 'index_all', 'agent_boot') GROUP BY type",
        ) {
            if let Ok(rows) = stmt.query_map(params![lb], |r| {
                Ok((r.get::<_, String>(0)?, r.get::<_, i64>(1)?))
            }) {
                let entries: Vec<String> = rows
                    .flatten()
                    .map(|(etype, cnt)| {
                        format!("{cnt} {}", etype.replace('_', " "))
                    })
                    .collect();
                if !entries.is_empty() {
                    parts.push(format!("Activity since last boot: {}", entries.join(", ")));
                }
            }
        }
    } else {
        // First boot for this agent — include recent decisions as orientation
        if let Ok(mut stmt) = conn.prepare(
            "SELECT decision, context FROM decisions WHERE status = 'active' ORDER BY created_at DESC LIMIT 5",
        ) {
            if let Ok(rows) = stmt.query_map([], |r| {
                Ok((r.get::<_, String>(0)?, r.get::<_, Option<String>>(1)?))
            }) {
                let lines: Vec<String> = rows
                    .flatten()
                    .map(|(dec, ctx)| {
                        let c = ctx.map(|c| format!(" — {c}")).unwrap_or_default();
                        format!("- {dec}{c}")
                    })
                    .collect();
                if !lines.is_empty() {
                    parts.push(format!("Recent decisions:\n{}", lines.join("\n")));
                }
            }
        }
    }

    let text = parts.join("\n\n");
    let tokens = estimate_tokens(&text);
    let freshness = last_boot
        .as_ref()
        .map(|lb| {
            let prefix: String = lb.chars().take(16).collect();
            format!("since {prefix}")
        })
        .unwrap_or_else(|| "first boot".to_string());
    (text, tokens, freshness)
}

// ─── Raw baseline estimation ────────────────────────────────────────────────

/// Estimate what raw context would cost — the baseline Cortex replaces.
/// Counts chars in the same DB-backed context families the compiler actually
/// assembles for boot prompts (active memories + active decisions).
///
/// We intentionally do not include filesystem-size heuristics from custom
/// source directories here, because that can drastically overstate savings
/// without reflecting prompt payload reality.
fn estimate_raw_baseline(conn: &Connection, _home: &Path) -> usize {
    let mut total_chars: usize = 0;

    // DB memories: active entries only
    let mem_chars: i64 = conn
        .query_row(
            "SELECT COALESCE(SUM(LENGTH(text)), 0) FROM memories WHERE status = 'active'",
            [],
            |r| r.get(0),
        )
        .unwrap_or(0);
    total_chars += mem_chars as usize;

    // DB decisions: active entries only
    let dec_chars: i64 = conn
        .query_row(
            "SELECT COALESCE(SUM(LENGTH(decision)), 0) FROM decisions WHERE status = 'active'",
            [],
            |r| r.get(0),
        )
        .unwrap_or(0);
    total_chars += dec_chars as usize;

    estimate_tokens_from_chars(total_chars)
}

// ─── Record boot ────────────────────────────────────────────────────────────

/// Record this boot so the next session's delta knows when we last connected.
fn record_boot(conn: &Connection, agent: &str) {
    let now = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
    let _ = conn.execute(
        "INSERT INTO events (type, data, source_agent) VALUES (?1, ?2, ?3)",
        params![
            "agent_boot",
            serde_json::to_string(&json!({ "timestamp": &now, "agent": agent }))
                .unwrap_or_default(),
            agent
        ],
    );
}

// ─── Public API ─────────────────────────────────────────────────────────────

// ─── Context Item for ranked compilation ───────────────────────────────────

#[derive(Clone, Copy, Debug, PartialEq)]
struct RankComponents {
    class_score: f64,
    recency_score: f64,
    relevance_score: f64,
    activity_score: f64,
    total_score: f64,
}

#[derive(Clone, Debug, PartialEq)]
struct RankAudit {
    source_kind: &'static str,
    source_id: i64,
    retention_class: String,
    components: RankComponents,
}

#[derive(Clone, Debug)]
struct RankedCandidate {
    source_kind: &'static str,
    source_id: i64,
    retention_class: String,
    title: String,
    body: String,
    updated_at: Option<String>,
    created_at: Option<String>,
    last_accessed: Option<String>,
    retrievals: i64,
    relevance: f64,
    components: RankComponents,
}

fn clamp01(value: f64) -> f64 {
    if value.is_finite() {
        value.clamp(0.0, 1.0)
    } else {
        0.0
    }
}

fn retention_class_score(retention_class: &str) -> f64 {
    match retention_class {
        "durable" => 1.0,
        "operational" => 0.8,
        "audit" => 0.4,
        "ephemeral" => 0.2,
        _ => 0.6,
    }
}

fn parse_timestamp(value: Option<&str>) -> Option<DateTime<Utc>> {
    let value = value?.trim();
    if value.is_empty() {
        return None;
    }
    DateTime::parse_from_rfc3339(value)
        .map(|dt| dt.with_timezone(&Utc))
        .ok()
        .or_else(|| {
            NaiveDateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S")
                .ok()
                .map(|dt| Utc.from_utc_datetime(&dt))
        })
}

fn recency_score(timestamp: Option<&str>, now: DateTime<Utc>) -> f64 {
    let Some(timestamp) = parse_timestamp(timestamp) else {
        return 0.05;
    };
    let age_hours = now.signed_duration_since(timestamp).num_hours().max(0);
    match age_hours {
        0..=1 => 1.0,
        2..=24 => 0.85,
        25..=168 => 0.60,
        169..=720 => 0.35,
        721..=2160 => 0.15,
        _ => 0.05,
    }
}

fn activity_score(retrievals: i64, last_accessed: Option<&str>, now: DateTime<Utc>) -> f64 {
    let retrieval_score = (retrievals.max(0) as f64 / 10.0).min(1.0);
    let access_score = recency_score(last_accessed, now);
    (retrieval_score * 0.55) + (access_score * 0.45)
}

fn rank_components_for(candidate: &RankedCandidate, now: DateTime<Utc>) -> RankComponents {
    let timestamp = candidate
        .updated_at
        .as_deref()
        .or(candidate.created_at.as_deref());
    let class_score = retention_class_score(&candidate.retention_class);
    let recency_score = recency_score(timestamp, now);
    let relevance_score = clamp01(candidate.relevance);
    let activity_score = activity_score(
        candidate.retrievals,
        candidate.last_accessed.as_deref(),
        now,
    );
    let total_score = (class_score * RANK_WEIGHT_CLASS)
        + (recency_score * RANK_WEIGHT_RECENCY)
        + (relevance_score * RANK_WEIGHT_RELEVANCE)
        + (activity_score * RANK_WEIGHT_ACTIVITY);

    RankComponents {
        class_score,
        recency_score,
        relevance_score,
        activity_score,
        total_score,
    }
}

fn rank_candidates(
    mut candidates: Vec<RankedCandidate>,
    top_n: usize,
    now: DateTime<Utc>,
) -> Vec<RankedCandidate> {
    for candidate in &mut candidates {
        candidate.components = rank_components_for(candidate, now);
    }
    candidates.sort_by(|left, right| {
        right
            .components
            .total_score
            .partial_cmp(&left.components.total_score)
            .unwrap_or(std::cmp::Ordering::Equal)
            .then_with(|| right.updated_at.cmp(&left.updated_at))
            .then_with(|| left.source_id.cmp(&right.source_id))
    });
    candidates.truncate(top_n);
    candidates
}

fn rank_audit_json(audit: &RankAudit) -> Value {
    json!({
        "sourceKind": audit.source_kind,
        "sourceId": audit.source_id,
        "retentionClass": audit.retention_class,
        "rankComponents": {
            "class": (audit.components.class_score * 10000.0).round() / 10000.0,
            "recency": (audit.components.recency_score * 10000.0).round() / 10000.0,
            "relevance": (audit.components.relevance_score * 10000.0).round() / 10000.0,
            "activity": (audit.components.activity_score * 10000.0).round() / 10000.0,
            "total": (audit.components.total_score * 10000.0).round() / 10000.0
        }
    })
}

struct ContextItem {
    name: String,
    text: String,
    tokens: usize,
    /// Base priority: 1.0 = must-have, 0.5 = important, 0.2 = nice-to-have
    priority: f64,
    /// Utility score: priority / token_cost (higher = more efficient)
    utility: f64,
    rank_audit: Option<RankAudit>,
}

impl ContextItem {
    fn new(name: &str, text: String, priority: f64) -> Self {
        let tokens = estimate_tokens(&text);
        let utility = if tokens > 0 {
            priority / (tokens as f64)
        } else {
            0.0
        };
        Self {
            name: name.to_string(),
            text,
            tokens,
            priority,
            utility,
            rank_audit: None,
        }
    }

    fn from_ranked_candidate(candidate: RankedCandidate) -> Self {
        let title = candidate.title.chars().take(160).collect::<String>();
        let body = candidate.body.chars().take(420).collect::<String>();
        let text = format!(
            "## Ranked {} Context\n- {}: {}",
            candidate.source_kind, title, body
        );
        let mut item = Self::new(
            &format!("ranked:{}:{}", candidate.source_kind, candidate.source_id),
            text,
            candidate.components.total_score.max(0.01),
        );
        item.rank_audit = Some(RankAudit {
            source_kind: candidate.source_kind,
            source_id: candidate.source_id,
            retention_class: candidate.retention_class,
            components: candidate.components,
        });
        item
    }
}

fn attach_rank_audit(mut entry: Value, item: &ContextItem) -> Value {
    if let (Some(object), Some(audit)) = (entry.as_object_mut(), item.rank_audit.as_ref()) {
        if let Value::Object(rank_object) = rank_audit_json(audit) {
            for (key, value) in rank_object {
                object.insert(key, value);
            }
        }
    }
    entry
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct SourceTokenBounds {
    min: usize,
    max: usize,
}

impl SourceTokenBounds {
    fn new(min: usize, max: usize) -> Self {
        let min = min.max(1);
        Self {
            min,
            max: max.max(min),
        }
    }
}

struct PackedContext {
    assembled_parts: Vec<String>,
    admitted: Vec<Value>,
    rejected: Vec<Value>,
}

fn read_usize_env(name: &str, default: usize) -> usize {
    env::var(name)
        .ok()
        .and_then(|value| value.trim().parse::<usize>().ok())
        .filter(|value| *value > 0)
        .unwrap_or(default)
}

fn boot_source_token_bounds() -> SourceTokenBounds {
    SourceTokenBounds::new(
        read_usize_env(
            "CORTEX_BOOT_MIN_SOURCE_TOKENS",
            DEFAULT_BOOT_MIN_SOURCE_TOKENS,
        ),
        read_usize_env(
            "CORTEX_BOOT_MAX_SOURCE_TOKENS",
            DEFAULT_BOOT_MAX_SOURCE_TOKENS,
        ),
    )
}

fn boot_rank_top_n() -> usize {
    read_usize_env("CORTEX_BOOT_RANK_TOP_N", DEFAULT_BOOT_RANK_TOP_N).min(20)
}

fn empty_rank_components() -> RankComponents {
    RankComponents {
        class_score: 0.0,
        recency_score: 0.0,
        relevance_score: 0.0,
        activity_score: 0.0,
        total_score: 0.0,
    }
}

fn fetch_rank_candidates(conn: &Connection) -> Vec<RankedCandidate> {
    let mut candidates = Vec::new();

    if let Ok(mut stmt) = conn.prepare(
        "SELECT id, text, type, retention_class, score, retrievals, last_accessed, updated_at, created_at
         FROM memories
         WHERE status = 'active' AND type != 'state'
         ORDER BY updated_at DESC
         LIMIT 80",
    ) {
        if let Ok(rows) = stmt.query_map([], |row| {
            Ok(RankedCandidate {
                source_kind: "memory",
                source_id: row.get::<_, i64>(0)?,
                body: row.get::<_, String>(1)?,
                title: row.get::<_, Option<String>>(2)?.unwrap_or_else(|| "memory".to_string()),
                retention_class: row
                    .get::<_, Option<String>>(3)?
                    .unwrap_or_else(|| "operational".to_string()),
                relevance: row.get::<_, Option<f64>>(4)?.unwrap_or(0.5),
                retrievals: row.get::<_, Option<i64>>(5)?.unwrap_or(0),
                last_accessed: row.get::<_, Option<String>>(6)?,
                updated_at: row.get::<_, Option<String>>(7)?,
                created_at: row.get::<_, Option<String>>(8)?,
                components: empty_rank_components(),
            })
        }) {
            candidates.extend(rows.flatten());
        }
    }

    if let Ok(mut stmt) = conn.prepare(
        "SELECT id, decision, context, type, retention_class, score, retrievals, last_accessed, updated_at, created_at
         FROM decisions
         WHERE status = 'active'
         ORDER BY updated_at DESC
         LIMIT 80",
    ) {
        if let Ok(rows) = stmt.query_map([], |row| {
            let decision: String = row.get(1)?;
            let context: Option<String> = row.get(2)?;
            let body = match context {
                Some(context) if !context.trim().is_empty() => format!("{decision} ({context})"),
                _ => decision,
            };
            Ok(RankedCandidate {
                source_kind: "decision",
                source_id: row.get::<_, i64>(0)?,
                body,
                title: row
                    .get::<_, Option<String>>(3)?
                    .unwrap_or_else(|| "decision".to_string()),
                retention_class: row
                    .get::<_, Option<String>>(4)?
                    .unwrap_or_else(|| "operational".to_string()),
                relevance: row.get::<_, Option<f64>>(5)?.unwrap_or(0.5),
                retrievals: row.get::<_, Option<i64>>(6)?.unwrap_or(0),
                last_accessed: row.get::<_, Option<String>>(7)?,
                updated_at: row.get::<_, Option<String>>(8)?,
                created_at: row.get::<_, Option<String>>(9)?,
                components: empty_rank_components(),
            })
        }) {
            candidates.extend(rows.flatten());
        }
    }

    candidates
}

fn score_signal_is_flat(items: &[ContextItem]) -> bool {
    let mut count = 0usize;
    let mut sum = 0.0;
    for item in items.iter().filter(|item| !item.text.is_empty()) {
        count += 1;
        sum += item.priority;
    }
    if count <= 1 {
        return true;
    }

    let mean = sum / count as f64;
    let variance = items
        .iter()
        .filter(|item| !item.text.is_empty())
        .map(|item| {
            let delta = item.priority - mean;
            delta * delta
        })
        .sum::<f64>()
        / count as f64;
    variance < SCORE_VARIANCE_FLAT_THRESHOLD
}

fn truncate_to_token_budget(text: &str, token_budget: usize) -> (String, usize) {
    if token_budget == 0 {
        return (String::new(), 0);
    }

    let total_chars = text.chars().count();
    let mut char_budget = ((token_budget as f64 * 3.5) as usize)
        .max(1)
        .min(total_chars);

    loop {
        let prefix: String = text.chars().take(char_budget).collect();
        let candidate = format!("{prefix}...");
        let tokens = estimate_tokens(&candidate);
        if tokens <= token_budget || char_budget <= 1 {
            return (candidate, tokens);
        }
        char_budget -= 1;
    }
}

fn pack_context_items_greedy(items: &[ContextItem], max_tokens: usize) -> PackedContext {
    let mut budget_remaining = max_tokens;
    let mut admitted: Vec<Value> = Vec::new();
    let mut rejected: Vec<Value> = Vec::new();
    let mut assembled_parts: Vec<String> = Vec::new();

    for item in items {
        if item.tokens <= budget_remaining && !item.text.is_empty() {
            assembled_parts.push(item.text.clone());
            budget_remaining -= item.tokens;
            admitted.push(attach_rank_audit(
                json!({
                    "name": item.name,
                    "tokens": item.tokens,
                    "priority": item.priority,
                    "utility": (item.utility * 10000.0).round() / 10000.0
                }),
                item,
            ));
        } else if !item.text.is_empty() {
            // Try truncation for high-priority items
            if item.priority >= 0.7 && budget_remaining > 30 {
                let trunc_chars = (budget_remaining as f64 * 3.5) as usize;
                let truncated: String = item.text.chars().take(trunc_chars).collect();
                let trunc_tokens = estimate_tokens(&truncated);
                assembled_parts.push(format!("{truncated}..."));
                budget_remaining = budget_remaining.saturating_sub(trunc_tokens);
                admitted.push(attach_rank_audit(
                    json!({
                        "name": item.name,
                        "tokens": trunc_tokens,
                        "priority": item.priority,
                        "truncated": true
                    }),
                    item,
                ));
            } else {
                rejected.push(attach_rank_audit(
                    json!({
                        "name": item.name,
                        "tokens": item.tokens,
                        "priority": item.priority,
                        "reason": "budget_exceeded"
                    }),
                    item,
                ));
            }
        }
    }

    PackedContext {
        assembled_parts,
        admitted,
        rejected,
    }
}

fn score_adaptive_allocations(
    items: &[ContextItem],
    max_tokens: usize,
    bounds: SourceTokenBounds,
) -> Vec<usize> {
    let mut order: Vec<usize> = items
        .iter()
        .enumerate()
        .filter(|(_, item)| !item.text.is_empty())
        .map(|(idx, _)| idx)
        .collect();
    order.sort_by(|left, right| {
        items[*right]
            .priority
            .partial_cmp(&items[*left].priority)
            .unwrap_or(std::cmp::Ordering::Equal)
            .then_with(|| {
                items[*right]
                    .utility
                    .partial_cmp(&items[*left].utility)
                    .unwrap_or(std::cmp::Ordering::Equal)
            })
    });

    let mut allocations = vec![0usize; items.len()];
    let mut floor_spent = 0usize;
    for idx in order {
        let item = &items[idx];
        let floor = item.tokens.min(bounds.min).min(bounds.max);
        if floor == 0 {
            continue;
        }
        if floor_spent.saturating_add(floor) <= max_tokens {
            allocations[idx] = floor;
            floor_spent += floor;
        } else if allocations.iter().all(|allocation| *allocation == 0) && max_tokens > 0 {
            allocations[idx] = item.tokens.min(bounds.max).min(max_tokens);
            floor_spent += allocations[idx];
        }
    }

    let mut remaining = max_tokens.saturating_sub(floor_spent);
    while remaining > 0 {
        let eligible: Vec<usize> = allocations
            .iter()
            .enumerate()
            .filter(|(idx, allocation)| {
                **allocation > 0 && **allocation < items[*idx].tokens.min(bounds.max)
            })
            .map(|(idx, _)| idx)
            .collect();
        if eligible.is_empty() {
            break;
        }

        let total_score = eligible
            .iter()
            .map(|idx| items[*idx].priority.max(0.01))
            .sum::<f64>();
        let mut allocated_any = false;
        for idx in eligible {
            if remaining == 0 {
                break;
            }
            let cap = items[idx].tokens.min(bounds.max);
            let room = cap.saturating_sub(allocations[idx]);
            if room == 0 {
                continue;
            }
            let share = ((remaining as f64) * (items[idx].priority.max(0.01) / total_score)).ceil()
                as usize;
            let delta = share.max(1).min(room).min(remaining);
            allocations[idx] += delta;
            remaining -= delta;
            allocated_any = true;
        }
        if !allocated_any {
            break;
        }
    }

    allocations
}

fn pack_context_items_score_adaptive(
    items: &[ContextItem],
    max_tokens: usize,
    bounds: SourceTokenBounds,
) -> PackedContext {
    let allocations = score_adaptive_allocations(items, max_tokens, bounds);
    let mut admitted: Vec<Value> = Vec::new();
    let mut rejected: Vec<Value> = Vec::new();
    let mut assembled_parts: Vec<String> = Vec::new();

    for (idx, item) in items.iter().enumerate() {
        if item.text.is_empty() {
            continue;
        }
        let allocation = allocations[idx];
        if allocation == 0 {
            rejected.push(attach_rank_audit(
                json!({
                    "name": item.name,
                    "tokens": item.tokens,
                    "priority": item.priority,
                    "reason": "score_adaptive_budget_exceeded"
                }),
                item,
            ));
            continue;
        }

        if item.tokens <= allocation {
            assembled_parts.push(item.text.clone());
            admitted.push(attach_rank_audit(
                json!({
                    "name": item.name,
                    "tokens": item.tokens,
                    "allocatedTokens": allocation,
                    "priority": item.priority,
                    "utility": (item.utility * 10000.0).round() / 10000.0,
                    "packing": "score_adaptive"
                }),
                item,
            ));
        } else {
            let (truncated, trunc_tokens) = truncate_to_token_budget(&item.text, allocation);
            assembled_parts.push(truncated);
            admitted.push(attach_rank_audit(
                json!({
                    "name": item.name,
                    "tokens": trunc_tokens,
                    "allocatedTokens": allocation,
                    "priority": item.priority,
                    "truncated": true,
                    "packing": "score_adaptive"
                }),
                item,
            ));
        }
    }

    PackedContext {
        assembled_parts,
        admitted,
        rejected,
    }
}

fn pack_context_items(
    items: &[ContextItem],
    max_tokens: usize,
    bounds: SourceTokenBounds,
) -> PackedContext {
    pack_context_items_with_mode(items, max_tokens, bounds, boot_packing_mode())
}

fn pack_context_items_with_mode(
    items: &[ContextItem],
    max_tokens: usize,
    bounds: SourceTokenBounds,
    mode: BootPackingMode,
) -> PackedContext {
    match mode {
        BootPackingMode::LegacyGreedy => pack_context_items_greedy(items, max_tokens),
        BootPackingMode::ScoreAdaptive => {
            pack_context_items_score_adaptive(items, max_tokens, bounds)
        }
        BootPackingMode::Auto => {
            if score_signal_is_flat(items) {
                pack_context_items_greedy(items, max_tokens)
            } else {
                pack_context_items_score_adaptive(items, max_tokens, bounds)
            }
        }
    }
}

/// Compile the boot prompt for an agent within a token budget.
///
/// Prompt Compiler Pipeline (v3 -- score-adaptive context packing):
///  1. Gather all context items with priority scores
///  2. Sort by utility (priority / token_cost) -- best bang-per-token first
///  3. Pack within budget using score-adaptive truncation when score variance exists
///  4. Record admitted vs rejected for observability
///  5. Return prompt with compilation metadata and savings
pub fn compile(conn: &Connection, home: &Path, agent: &str, max_tokens: usize) -> BootResult {
    // ── 1. Gather context items with priorities ─────────────────────────────

    let mut items: Vec<ContextItem> = Vec::new();

    // Identity capsule: must-have (priority 1.0)
    let (identity_text, _) = build_identity_capsule(conn);
    if !identity_text.is_empty() {
        items.push(ContextItem::new(
            "identity",
            format!("## Identity\n{identity_text}"),
            1.0,
        ));
    }

    // Delta capsule: broken into sub-items with individual priorities
    let (delta_text, _, _delta_freshness) = build_delta_capsule(conn, agent);
    if !delta_text.is_empty() {
        // Split delta into sections, each scored independently
        let sections: Vec<(&str, f64)> = vec![
            ("## Pending Messages", 0.95),  // Messages from other agents = urgent
            ("## Active Agents", 0.60),     // Who's online = coordination
            ("## Active Locks", 0.70),      // Locks = collision prevention
            ("## Feed", 0.40),              // Feed = nice context
            ("## Pending Tasks", 0.75),     // Task board = actionable
            ("## Your Active Tasks", 0.80), // Your tasks = high priority
            ("CONFLICTS:", 0.90),           // Conflicts = must resolve
            ("## Active Focus", 0.85),      // Focus scope = context boundary
            ("New decisions:", 0.55),       // Recent decisions = orientation
            ("New knowledge:", 0.45),       // New memories
            ("Activity since last boot:", 0.30), // Activity summary = low value
            ("Recent decisions:", 0.50),    // First-boot orientation
        ];

        // Try to split delta into scored sub-sections
        let remaining_delta = delta_text.as_str();
        let mut matched_any = false;

        for (header, priority) in &sections {
            if let Some(start) = remaining_delta.find(header) {
                // Find end: next section header or end of string
                let content_start = start;
                let after_header = start + header.len();
                let end = remaining_delta[after_header..]
                    .find("\n\n")
                    .map(|p| after_header + p)
                    .unwrap_or(remaining_delta.len());

                let section_text = remaining_delta[content_start..end].trim().to_string();
                if !section_text.is_empty() {
                    items.push(ContextItem::new(header, section_text, *priority));
                    matched_any = true;
                }
            }
        }

        // Fallback: if no sections matched, treat delta as one block
        if !matched_any {
            items.push(ContextItem::new(
                "delta",
                format!("## Delta\n{delta_text}"),
                0.70,
            ));
        }
    }

    // ── 2. Record boot ──────────────────────────────────────────────────────
    for candidate in rank_candidates(fetch_rank_candidates(conn), boot_rank_top_n(), Utc::now()) {
        items.push(ContextItem::from_ranked_candidate(candidate));
    }

    record_boot(conn, agent);

    // ── 3. Sort by utility (priority / token_cost) descending ───────────────
    items.sort_by(|a, b| {
        b.utility
            .partial_cmp(&a.utility)
            .unwrap_or(std::cmp::Ordering::Equal)
    });

    // ── 4. Score-adaptive budget packing ────────────────────────────────────
    let packed = pack_context_items(&items, max_tokens, boot_source_token_bounds());
    let admitted = packed.admitted;
    let rejected = packed.rejected;
    let assembled_parts = packed.assembled_parts;

    let assembled = assembled_parts.join("\n\n");
    let token_estimate = estimate_tokens(&assembled);

    // ── 5. Savings and observability ────────────────────────────────────────
    let raw_baseline = estimate_raw_baseline(conn, home);
    let saved = raw_baseline.saturating_sub(token_estimate);
    let percent = if raw_baseline > 0 {
        (saved * 100) / raw_baseline
    } else {
        0
    };

    // Only record savings when baseline is meaningful (skip empty-DB boots)
    if raw_baseline > 0 {
        let _ = conn.execute(
            "INSERT INTO events (type, data, source_agent) VALUES (?1, ?2, ?3)",
            params![
                "boot_savings",
                serde_json::to_string(&json!({
                    "agent": agent,
                    "served": token_estimate,
                    "baseline": raw_baseline,
                    "saved": saved,
                    "percent": percent,
                    "admitted": admitted.len(),
                    "rejected": rejected.len()
                }))
                .unwrap_or_default(),
                "rust-daemon"
            ],
        );
    }

    BootResult {
        boot_prompt: assembled,
        token_estimate,
        savings: json!({
            "rawBaseline": raw_baseline,
            "served": token_estimate,
            "saved": saved,
            "percent": percent
        }),
        capsules: admitted,
    }
}

// Dead code removed: find_memory_dir, read_memory_files, read_lessons
// (indexer.rs has its own implementation; these were ported but unused)

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

    fn repeated_text(label: &str, repeats: usize) -> String {
        let mut text = format!("## {label}\n");
        for idx in 0..repeats {
            text.push_str(&format!("{label} detail {idx}. "));
        }
        text
    }

    fn admitted_tokens(admitted: &[Value], name: &str, key: &str) -> usize {
        admitted
            .iter()
            .find(|item| item.get("name").and_then(Value::as_str) == Some(name))
            .and_then(|item| item.get(key))
            .and_then(Value::as_u64)
            .map(|value| value as usize)
            .unwrap_or(0)
    }

    fn fixed_now() -> DateTime<Utc> {
        Utc.with_ymd_and_hms(2026, 5, 5, 12, 0, 0)
            .single()
            .expect("valid fixed time")
    }

    fn candidate(
        source_id: i64,
        retention_class: &str,
        updated_at: &str,
        retrievals: i64,
        last_accessed: Option<&str>,
        relevance: f64,
    ) -> RankedCandidate {
        RankedCandidate {
            source_kind: "memory",
            source_id,
            retention_class: retention_class.to_string(),
            title: format!("candidate-{source_id}"),
            body: format!("candidate body {source_id}"),
            updated_at: Some(updated_at.to_string()),
            created_at: Some(updated_at.to_string()),
            last_accessed: last_accessed.map(str::to_string),
            retrievals,
            relevance,
            components: empty_rank_components(),
        }
    }

    #[test]
    fn ranking_component_scores_cover_class_recency_relevance_and_activity() {
        let now = fixed_now();

        assert_eq!(retention_class_score("durable"), 1.0);
        assert_eq!(retention_class_score("operational"), 0.8);
        assert_eq!(retention_class_score("audit"), 0.4);
        assert_eq!(retention_class_score("ephemeral"), 0.2);
        assert!(recency_score(Some("2026-05-05T11:30:00Z"), now) > 0.9);
        assert!(recency_score(Some("2025-01-01T00:00:00Z"), now) < 0.1);
        assert!(
            activity_score(10, Some("2026-05-05T11:30:00Z"), now) > activity_score(0, None, now)
        );

        let ranked = rank_components_for(
            &candidate(
                1,
                "operational",
                "2026-05-05T11:30:00Z",
                6,
                Some("2026-05-05T11:45:00Z"),
                1.4,
            ),
            now,
        );
        assert_eq!(ranked.relevance_score, 1.0);
        assert!(ranked.total_score > 0.75);
    }

    #[test]
    fn active_operational_context_ranks_above_stale_durable_context() {
        let now = fixed_now();
        let stale_durable = candidate(1, "durable", "2025-01-01T00:00:00Z", 0, None, 0.85);
        let active_operational = candidate(
            2,
            "operational",
            "2026-05-05T11:50:00Z",
            9,
            Some("2026-05-05T11:55:00Z"),
            0.70,
        );

        let ranked = rank_candidates(vec![stale_durable, active_operational], 2, now);

        assert_eq!(ranked[0].source_id, 2);
        assert!(ranked[0].components.total_score > ranked[1].components.total_score);
    }

    #[test]
    fn ranked_context_items_emit_boot_audit_components() {
        let now = fixed_now();
        let ranked = rank_candidates(
            vec![candidate(
                7,
                "operational",
                "2026-05-05T11:50:00Z",
                4,
                Some("2026-05-05T11:55:00Z"),
                0.90,
            )],
            1,
            now,
        );
        let item = ContextItem::from_ranked_candidate(ranked[0].clone());
        let packed = pack_context_items(&[item], 200, SourceTokenBounds::new(20, 200));
        let capsule = &packed.admitted[0];

        assert_eq!(capsule["sourceKind"], "memory");
        assert_eq!(capsule["sourceId"], 7);
        assert_eq!(capsule["retentionClass"], "operational");
        assert!(capsule["rankComponents"]["class"].as_f64().unwrap() > 0.0);
        assert!(capsule["rankComponents"]["total"].as_f64().unwrap() > 0.0);
    }

    #[test]
    fn flat_score_fallback_matches_legacy_greedy_packing() {
        let items = vec![
            ContextItem::new("alpha", repeated_text("alpha", 40), 0.5),
            ContextItem::new("beta", repeated_text("beta", 40), 0.5),
            ContextItem::new("gamma", repeated_text("gamma", 40), 0.5),
        ];

        let legacy = pack_context_items_greedy(&items, 120);
        let packed = pack_context_items(&items, 120, SourceTokenBounds::new(20, 200));

        assert_eq!(packed.assembled_parts, legacy.assembled_parts);
        assert_eq!(packed.admitted, legacy.admitted);
        assert_eq!(packed.rejected, legacy.rejected);
    }

    #[test]
    fn forced_legacy_mode_uses_greedy_packer_for_variance_fixture() {
        let items = vec![
            ContextItem::new("high", repeated_text("high", 90), 0.95),
            ContextItem::new("low", repeated_text("low", 10), 0.20),
            ContextItem::new("medium", repeated_text("medium", 40), 0.60),
        ];

        let legacy = pack_context_items_greedy(&items, 120);
        let packed = pack_context_items_with_mode(
            &items,
            120,
            SourceTokenBounds::new(24, 120),
            BootPackingMode::LegacyGreedy,
        );

        assert_eq!(packed.assembled_parts, legacy.assembled_parts);
        assert_eq!(packed.admitted, legacy.admitted);
        assert_eq!(packed.rejected, legacy.rejected);
    }

    #[test]
    fn score_adaptive_packing_gives_high_score_sources_more_tokens() {
        let items = vec![
            ContextItem::new("high", repeated_text("high", 140), 0.95),
            ContextItem::new("low", repeated_text("low", 140), 0.15),
        ];

        let packed = pack_context_items(&items, 140, SourceTokenBounds::new(24, 120));
        let high_allocated = admitted_tokens(&packed.admitted, "high", "allocatedTokens");
        let low_allocated = admitted_tokens(&packed.admitted, "low", "allocatedTokens");
        let high_tokens = admitted_tokens(&packed.admitted, "high", "tokens");
        let low_tokens = admitted_tokens(&packed.admitted, "low", "tokens");

        assert!(
            high_allocated > low_allocated,
            "expected high-score allocation > low-score allocation: {packed:?}",
            packed = packed.admitted
        );
        assert!(
            high_tokens > low_tokens,
            "expected high-score source to receive more emitted tokens: {packed:?}",
            packed = packed.admitted
        );
    }

    #[test]
    fn source_token_bounds_keeps_max_at_or_above_min() {
        assert_eq!(
            SourceTokenBounds::new(80, 40),
            SourceTokenBounds { min: 80, max: 80 }
        );
    }

    #[test]
    fn score_adaptive_allocation_respects_budget_below_floor() {
        let items = vec![ContextItem::new("tiny", repeated_text("tiny", 80), 1.0)];
        let allocations = score_adaptive_allocations(&items, 10, SourceTokenBounds::new(40, 120));

        assert_eq!(allocations, vec![10]);
    }
}
