#![allow(dead_code)]

mod codex;
mod commit;
mod content_ops;
mod convert;
mod embeddings;
mod engage;
mod github;
mod index;
mod knowledge;
pub mod paths;
mod session;
mod state;
mod store;
mod surreal_db;
mod sync;
mod tensor;
mod types;
mod wake_chunk;
mod wake_ritual;
mod wake_token;

use anyhow::{Context, Result, bail};
use clap::{Parser, Subcommand, ValueEnum};

use crate::index::{
    IndexConfig, export_csv, export_jsonl, export_markdown, import_jsonl, rebuild_index,
};

#[derive(Parser)]
#[command(name = "mx")]
#[command(about = "Tsunderground CLI - memory, workflow, and identity tooling")]
#[command(version)]
struct Cli {
    /// Enable verbose output (show connection logs)
    #[arg(short = 'v', long, global = true)]
    verbose: bool,

    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
#[allow(clippy::large_enum_variant)]
enum Commands {
    /// Knowledge base operations (CRUD, search, wake, facts)
    Memory {
        #[command(subcommand)]
        command: MemoryCommands,
    },

    /// Create an encoded git commit
    Commit {
        /// Commit message (human-readable, will be encoded)
        #[arg(required_unless_present_any = ["title", "encode_only"])]
        message: Option<String>,

        /// Stage all changes before committing
        #[arg(short = 'a', long)]
        all: bool,

        /// Push after committing
        #[arg(short, long)]
        push: bool,

        /// Only generate and print encoded message (don't commit)
        #[arg(long, conflicts_with_all = ["all", "push"])]
        encode_only: bool,

        /// Title text for PR-style encoding (requires --encode-only)
        #[arg(short, long, requires = "encode_only", requires = "body")]
        title: Option<String>,

        /// Body text for PR-style encoding (requires --encode-only)
        #[arg(short, long, requires = "encode_only", requires = "title")]
        body: Option<String>,

        /// Show the full encoded commit fields (Title/Body/Dejavu/Footer).
        /// Default output is just the footer line and `Committed.`
        #[arg(long, conflicts_with = "encode_only", conflicts_with_all = ["title", "body"])]
        show_encoded: bool,
    },

    /// Pull request operations
    Pr {
        #[command(subcommand)]
        command: PrCommands,
    },

    /// GitHub sync operations
    Sync {
        #[command(subcommand)]
        command: SyncCommands,
    },

    /// GitHub operations
    Github {
        #[command(subcommand)]
        command: GithubCommands,
    },

    /// Wiki operations
    Wiki {
        #[command(subcommand)]
        command: WikiCommands,
    },

    /// Session export operations
    Session {
        #[command(subcommand)]
        command: SessionCommands,
    },

    /// Codex - session conversation archival
    Codex {
        #[command(subcommand)]
        command: CodexCommands,
    },

    /// Conversion utilities
    Convert {
        #[command(subcommand)]
        command: ConvertCommands,
    },

    /// Heartbeat - calming co-regulation prompt
    Heartbeat {
        /// Milliseconds since last heartbeat (for BPM calculation)
        #[arg(long)]
        since: Option<u64>,

        /// Reset the heartbeat session
        #[arg(long)]
        reset: bool,
    },

    /// Decoded git log (decodes encoded commit messages)
    Log {
        /// Number of commits to show
        #[arg(short = 'n', long, default_value = "10")]
        count: usize,

        /// Show full commit details
        #[arg(long)]
        full: bool,

        /// Pass through additional git log arguments
        #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
        args: Vec<String>,
    },

    /// Emotional state tensor operations
    State {
        #[command(subcommand)]
        command: StateCommands,
    },
}

#[derive(Subcommand)]
enum ConvertCommands {
    /// Convert markdown to YAML for GitHub sync
    Md2yaml {
        /// Input file or directory
        input: String,

        /// Output directory (defaults to current directory)
        #[arg(short, long)]
        output: Option<String>,

        /// Dry run - show what would be created
        #[arg(long)]
        dry_run: bool,
    },

    /// Convert YAML to markdown for human reading
    Yaml2md {
        /// Input file or directory
        input: String,

        /// Output directory (defaults to current directory)
        #[arg(short, long)]
        output: Option<String>,

        /// Repository in owner/repo format (for GitHub URLs)
        #[arg(short, long)]
        repo: Option<String>,

        /// Dry run - show what would be created
        #[arg(long)]
        dry_run: bool,
    },
}

#[derive(Subcommand)]
enum StateCommands {
    /// Encode state tensor from dimensional values
    Encode {
        /// Pipe-separated values (e.g., "0.3|0.2|0.7|0.8|0.4")
        values: Option<String>,

        /// Named dimension values (e.g., "temp=0.8 entropy=0.75 agency=0.4")
        #[arg(short = 'd', long, conflicts_with = "values", conflicts_with = "file")]
        dimensions: Option<String>,

        /// Read values from file (one value per line or pipe-separated)
        #[arg(short, long, conflicts_with = "values")]
        file: Option<String>,

        /// Schema ID (defaults to MX_STATE_SCHEMA or "crewu")
        #[arg(short, long)]
        schema: Option<String>,

        /// Interactive guided mode - walks through dimensions with anchors
        #[arg(short = 'g', long)]
        guided: bool,

        /// Output format: tensor (default), json, human, bootstrap
        #[arg(short = 'F', long, default_value = "tensor")]
        format: String,

        /// Include runes in output
        #[arg(long)]
        runes: bool,
    },

    /// Decode state tensor to human-readable format
    Decode {
        /// Encoded tensor string (e.g., "@state:crewu|0.3|0.2|...")
        input: Option<String>,

        /// Schema ID (inferred from input if not specified)
        #[arg(short, long)]
        schema: Option<String>,

        /// Output format: human (default), json, tensor, mood
        #[arg(short = 'F', long, default_value = "human")]
        format: String,
    },

    /// List available schemas
    Schemas {
        /// Output as JSON
        #[arg(long)]
        json: bool,
    },

    /// List moods for a schema
    Moods {
        /// Schema ID (defaults to MX_STATE_SCHEMA or "crewu")
        #[arg(short, long)]
        schema: Option<String>,

        /// Show specific mood details
        mood: Option<String>,

        /// Output as JSON
        #[arg(long)]
        json: bool,
    },

    /// Show schema information (dimensions, moods)
    Info {
        /// Schema ID (defaults to MX_STATE_SCHEMA or "crewu")
        #[arg(short, long)]
        schema: Option<String>,

        /// Output as JSON
        #[arg(long)]
        json: bool,
    },

    // === Legacy commands (backward compatibility) ===
    /// [Legacy] Encode using mode-based mapping
    #[command(hide = true)]
    LegacyEncode {
        /// Discrete mode name (soft, play, build, etc.)
        #[arg(short, long)]
        mode: Option<String>,

        /// Interactive mode - prompts for each dimension
        #[arg(short, long)]
        interactive: bool,

        /// Output format: stele (default), json, human
        #[arg(short, long, default_value = "stele")]
        format: String,

        /// Schema path
        #[arg(long)]
        schema: Option<String>,
    },

    /// [Legacy] Parse wake preference from session-bootstrap
    #[command(hide = true)]
    Parse {
        /// Path to session-bootstrap.md file
        #[arg(short, long)]
        file: Option<String>,

        /// Raw preference string to parse
        preference: Option<String>,

        /// Output format: human (default), json, stele, mode
        #[arg(short = 'F', long, default_value = "human")]
        format: String,

        /// Schema path
        #[arg(long)]
        schema: Option<String>,
    },
}
#[derive(Subcommand)]
enum PrCommands {
    /// Merge a pull request with encoded commit message
    Merge {
        /// PR number
        number: u32,

        /// Use rebase merge instead of squash (mutually exclusive with --merge-commit)
        #[arg(long, conflicts_with = "merge")]
        rebase: bool,

        /// Use standard merge commit instead of squash (mutually exclusive with --rebase)
        #[arg(long, name = "merge", conflicts_with = "rebase")]
        merge_commit: bool,
    },
}

#[derive(Subcommand)]
pub enum SyncCommands {
    /// Pull issues/discussions from GitHub to local YAML
    Pull {
        /// Repository (owner/repo format)
        repo: String,

        /// Output directory (defaults to $MX_HOME/cache/sync/<repo>)
        #[arg(short, long)]
        output: Option<String>,

        /// Dry run - show what would be pulled
        #[arg(long)]
        dry_run: bool,
    },

    /// Push local changes to GitHub
    Push {
        /// Repository (owner/repo format)
        repo: String,

        /// Input directory (defaults to $MX_HOME/cache/sync/<repo>)
        #[arg(short, long)]
        input: Option<String>,

        /// Dry run - show what would be pushed
        #[arg(long)]
        dry_run: bool,
    },

    /// Sync identity labels to repository
    Labels {
        /// Repository (owner/repo format)
        repo: String,

        /// Dry run - show what would be synced
        #[arg(long)]
        dry_run: bool,
    },

    /// Sync issues bidirectionally
    Issues {
        /// Repository (owner/repo format)
        repo: String,

        /// Dry run - show what would be synced
        #[arg(long)]
        dry_run: bool,
    },
}

/// Shared filter flags for search/list commands (extracted from duplicated definitions)
#[derive(Debug, Clone, clap::Args)]
struct EntryFilter {
    /// Filter by category (comma-separated, see 'mx memory categories list' for valid names)
    #[arg(short, long, value_delimiter = ',')]
    category: Option<Vec<String>>,

    /// Output as JSON
    #[arg(long)]
    json: bool,

    /// Show only your private entries
    #[arg(long)]
    mine: bool,

    /// Include private entries (requires matching owner)
    #[arg(long)]
    include_private: bool,

    /// Minimum resonance level
    #[arg(long)]
    min_resonance: Option<i32>,

    /// Maximum resonance level
    #[arg(long)]
    max_resonance: Option<i32>,

    /// Filter to entries WITH wake phrase
    #[arg(long)]
    has_wake_phrase: bool,

    /// Filter to entries WITHOUT wake phrase
    #[arg(long, conflicts_with = "has_wake_phrase")]
    missing_wake_phrase: bool,

    /// Filter to entries WITH anchors
    #[arg(long)]
    has_anchors: bool,

    /// Filter to entries WITHOUT anchors
    #[arg(long, conflicts_with = "has_anchors")]
    missing_anchors: bool,

    /// Filter to entries WITH resonance type
    #[arg(long)]
    has_resonance_type: bool,

    /// Filter to entries WITHOUT resonance type
    #[arg(long, conflicts_with = "has_resonance_type")]
    missing_resonance_type: bool,

    /// Limit number of results
    #[arg(long)]
    limit: Option<usize>,

    /// Filter by tags (can specify multiple: focus,rust) (matches any)
    #[arg(long, value_delimiter = ',')]
    tags: Option<Vec<String>>,
}

/// Apply in-memory field presence filters to a list of entries
fn apply_entry_filters(
    entries: Vec<knowledge::KnowledgeEntry>,
    filter: &EntryFilter,
) -> Vec<knowledge::KnowledgeEntry> {
    let mut entries: Vec<_> = entries
        .into_iter()
        .filter(|e| !filter.has_wake_phrase || e.has_any_wake_phrase())
        .filter(|e| !filter.missing_wake_phrase || !e.has_any_wake_phrase())
        .filter(|e| !filter.has_anchors || !e.anchors.is_empty())
        .filter(|e| !filter.missing_anchors || e.anchors.is_empty())
        .filter(|e| {
            !filter.has_resonance_type || e.resonance_type.as_ref().is_some_and(|s| !s.is_empty())
        })
        .filter(|e| {
            !filter.missing_resonance_type || e.resonance_type.as_ref().is_none_or(|s| s.is_empty())
        })
        .filter(|e| {
            filter
                .tags
                .as_ref()
                .is_none_or(|filter_tags| filter_tags.iter().any(|t| e.tags.contains(t)))
        })
        .collect();

    // Apply limit if specified
    if let Some(n) = filter.limit {
        entries.truncate(n);
    }

    entries
}

/// Normalize a knowledge entry ID (accept both "kn-abc" and "abc", normalize to "kn-abc")
fn normalize_id(id: &str) -> String {
    if id.starts_with("kn-") {
        id.to_string()
    } else {
        format!("kn-{}", id)
    }
}

/// Sort order for `memory recent` results.
#[derive(Clone, Debug, ValueEnum)]
enum RecentSortOrder {
    /// Sort by creation time (most recent first)
    Chronological,
    /// Sort by effective resonance (highest first, decay-adjusted)
    Resonance,
}

#[derive(Subcommand)]
enum MemoryCommands {
    /// Rebuild the knowledge index
    Rebuild,

    /// Search knowledge entries
    Search {
        /// Search query
        query: String,

        #[command(flatten)]
        filter: EntryFilter,

        /// Use semantic (vector) search instead of keyword search
        #[arg(long)]
        semantic: bool,
    },

    /// List entries by category
    List {
        #[command(flatten)]
        filter: EntryFilter,
    },

    /// Show a specific entry
    Show {
        /// Entry ID
        id: String,

        /// Output as JSON
        #[arg(long)]
        json: bool,

        /// Output only the body content (for piping)
        #[arg(long)]
        content_only: bool,
    },

    /// Show index statistics
    Stats {
        /// Output as JSON
        #[arg(long)]
        json: bool,
    },

    /// Delete an entry from the index
    Delete {
        /// Entry ID to delete
        id: String,

        /// Output as JSON
        #[arg(long)]
        json: bool,
    },

    /// Import entries from JSONL file
    Import {
        /// Path to JSONL file (defaults to memory/index.jsonl)
        path: Option<String>,
    },

    /// Add a new entry directly to the database
    Add {
        /// Category name (run 'mx memory categories list' to see available categories)
        /// When --type is provided, category is auto-determined from fact type routing
        #[arg(long, required_unless_present = "type")]
        category: Option<String>,

        /// Entry title (auto-generated from content when --type is provided)
        #[arg(short, long, required_unless_present = "type")]
        title: Option<String>,

        /// Content inline
        #[arg(long, conflicts_with = "file")]
        content: Option<String>,

        /// Content from file
        #[arg(
            short,
            long,
            visible_alias = "content-file",
            conflicts_with = "content"
        )]
        file: Option<String>,

        /// Comma-separated tags
        #[arg(long)]
        tags: Option<String>,

        /// Applicability contexts (comma-separated)
        #[arg(short = 'a', long)]
        applicability: Option<String>,

        /// Source project ID
        #[arg(short, long)]
        project: Option<String>,

        /// Source agent ID (defaults to MX_CURRENT_AGENT env var)
        #[arg(long)]
        source_agent: Option<String>,

        /// Source type (manual, ram, cache, agent_session)
        #[arg(long, default_value = "manual")]
        source_type: String,

        /// Entry type (primary, summary, synthesis)
        #[arg(long, default_value = "primary")]
        entry_type: String,

        /// Session ID (for regular entries)
        #[arg(long)]
        session_id: Option<String>,

        /// Mark as ephemeral
        #[arg(long)]
        ephemeral: bool,

        /// Domain/subdomain path
        #[arg(short, long)]
        domain: Option<String>,

        /// Content type (text, code, config, data, binary)
        #[arg(long, default_value = "text")]
        content_type: String,

        /// Mark as private (only visible to owner) - shorthand for --visibility private
        #[arg(long, conflicts_with = "visibility")]
        private: bool,

        /// Set visibility (public or private)
        #[arg(long, conflicts_with = "private")]
        visibility: Option<String>,

        /// Explicit owner (defaults to source_agent or MX_CURRENT_AGENT if private)
        #[arg(long)]
        owner: Option<String>,

        /// Output as JSON
        #[arg(long)]
        json: bool,

        /// Resonance level (1-10, or higher for transcendent)
        #[arg(long)]
        resonance: Option<i32>,

        /// Resonance type (foundational, transformative, relational, operational, ephemeral, session)
        #[arg(long)]
        resonance_type: Option<String>,

        /// Wake phrase for memory ritual verification
        #[arg(long)]
        wake_phrase: Option<String>,

        /// Multiple wake phrases (comma-separated, for ritual variety)
        #[arg(long)]
        wake_phrases: Option<String>,

        /// Custom wake order (lower = earlier in sequence)
        #[arg(long)]
        wake_order: Option<i32>,

        /// Anchors (comma-separated bloom IDs this connects to)
        #[arg(long)]
        anchors: Option<String>,

        /// Fact type for ephemeral knowledge (decision, insight, person, quote, thread_opened, commitment, thread_closed)
        /// Routes to appropriate category and sets resonance_type=ephemeral
        #[arg(long = "type")]
        r#type: Option<String>,

        /// Session to link fact to via EXTRACTED_FROM relationship (requires --type)
        #[arg(long, requires = "type")]
        session: Option<String>,

        /// Thread ID for thread_closed operations (requires --type=thread_closed)
        #[arg(long, requires = "type")]
        thread_id: Option<String>,
    },

    /// Update an existing entry in the database
    Update {
        /// Entry ID to update
        id: String,

        /// Update title
        #[arg(short, long)]
        title: Option<String>,

        /// Replace content inline (full replacement)
        #[arg(long, conflicts_with_all = ["file", "append_content", "append_file", "prepend_content", "prepend_file", "find"])]
        content: Option<String>,

        /// Replace content from file (full replacement)
        #[arg(short, long, visible_alias = "content-file", conflicts_with_all = ["content", "append_content", "append_file", "prepend_content", "prepend_file", "find"])]
        file: Option<String>,

        /// Append text to end of existing content
        #[arg(long, conflicts_with_all = ["content", "file", "append_file", "prepend_content", "prepend_file", "find"])]
        append_content: Option<String>,

        /// Append content from file to end of existing content
        #[arg(long, conflicts_with_all = ["content", "file", "append_content", "prepend_content", "prepend_file", "find"])]
        append_file: Option<String>,

        /// Prepend text to start of existing content
        #[arg(long, conflicts_with_all = ["content", "file", "append_content", "append_file", "prepend_file", "find"])]
        prepend_content: Option<String>,

        /// Prepend content from file to start of existing content
        #[arg(long, conflicts_with_all = ["content", "file", "append_content", "append_file", "prepend_content", "find"])]
        prepend_file: Option<String>,

        /// Find text in content (requires --replace)
        #[arg(long, requires = "replace", conflicts_with_all = ["content", "file", "append_content", "append_file", "prepend_content", "prepend_file"])]
        find: Option<String>,

        /// Replace text found by --find
        #[arg(long, requires = "find")]
        replace: Option<String>,

        /// Replace all occurrences (with --find/--replace)
        #[arg(long, requires = "find")]
        replace_all: bool,

        /// Replace only the Nth occurrence (1-indexed, with --find/--replace)
        #[arg(long, requires = "find", conflicts_with = "replace_all")]
        nth: Option<usize>,

        /// Update category
        #[arg(long)]
        category: Option<String>,

        /// Update tags (comma-separated, replaces all)
        #[arg(long, conflicts_with_all = ["add_tag", "remove_tag"])]
        tags: Option<String>,

        /// Add a single tag to existing tags
        #[arg(long, conflicts_with = "tags")]
        add_tag: Option<String>,

        /// Remove a specific tag
        #[arg(long, conflicts_with = "tags")]
        remove_tag: Option<String>,

        /// Update applicability (comma-separated, replaces all)
        #[arg(short = 'a', long)]
        applicability: Option<String>,

        /// Update content type
        #[arg(long)]
        content_type: Option<String>,

        /// Update resonance level (1-10, or higher for transcendent)
        #[arg(long)]
        resonance: Option<i32>,

        /// Update resonance type (foundational, transformative, relational, operational, ephemeral, session)
        #[arg(long)]
        resonance_type: Option<String>,

        /// Update anchors (comma-separated bloom IDs, replaces all)
        #[arg(long, conflicts_with_all = ["add_anchor", "remove_anchor"])]
        anchors: Option<String>,

        /// Add a single anchor to existing anchors
        #[arg(long, conflicts_with = "anchors")]
        add_anchor: Option<String>,

        /// Remove a specific anchor
        #[arg(long, conflicts_with = "anchors")]
        remove_anchor: Option<String>,

        /// Update wake phrase for memory ritual verification
        #[arg(long)]
        wake_phrase: Option<String>,

        /// Update multiple wake phrases (comma-separated, replaces all)
        #[arg(long)]
        wake_phrases: Option<String>,

        /// Add a single wake phrase to existing phrases
        #[arg(long, conflicts_with = "wake_phrases")]
        add_wake_phrase: Option<String>,

        /// Remove a specific wake phrase
        #[arg(long, conflicts_with = "wake_phrases")]
        remove_wake_phrase: Option<String>,

        /// Update wake order (use '-' to clear)
        #[arg(long)]
        wake_order: Option<String>,

        /// Mark as private (shorthand for --visibility private)
        #[arg(long, conflicts_with = "visibility")]
        private: bool,

        /// Change visibility (public or private)
        #[arg(long, conflicts_with = "private")]
        visibility: Option<String>,

        /// Update owner (only valid when visibility is private)
        #[arg(long)]
        owner: Option<String>,

        /// Update session ID (for retrofitting entries written with wrong or missing session linkage)
        #[arg(long)]
        session_id: Option<String>,

        /// Force dangerous visibility changes (e.g., making blooms public)
        #[arg(long)]
        force: bool,

        /// Output as JSON
        #[arg(long)]
        json: bool,
    },

    /// Edit content by finding and replacing text (shortcut for: update <id> --find ... --replace ...)
    Edit {
        /// Entry ID to edit
        id: String,

        /// Text to find in the content
        #[arg(long, visible_alias = "old")]
        find: String,

        /// Replacement text
        #[arg(long, visible_alias = "new")]
        replace: String,

        /// Replace all occurrences (default: error if multiple matches)
        #[arg(long)]
        replace_all: bool,

        /// Replace only the Nth occurrence (1-indexed)
        #[arg(long, conflicts_with = "replace_all")]
        nth: Option<usize>,

        /// Output as JSON
        #[arg(long)]
        json: bool,
    },

    /// Append content to the end of an entry's body (shortcut for: update <id> --append-content ...)
    Append {
        /// Entry ID to append to
        id: String,

        /// Content to append (omit to read from stdin)
        #[arg(long, conflicts_with = "file")]
        content: Option<String>,

        /// Read content to append from file
        #[arg(
            short,
            long,
            visible_alias = "content-file",
            conflicts_with = "content"
        )]
        file: Option<String>,

        /// Output as JSON
        #[arg(long)]
        json: bool,
    },

    /// Prepend content to the start of an entry's body (shortcut for: update <id> --prepend-content ...)
    Prepend {
        /// Entry ID to prepend to
        id: String,

        /// Content to prepend (omit to read from stdin)
        #[arg(long, conflicts_with = "file")]
        content: Option<String>,

        /// Read content to prepend from file
        #[arg(
            short,
            long,
            visible_alias = "content-file",
            conflicts_with = "content"
        )]
        file: Option<String>,

        /// Output as JSON
        #[arg(long)]
        json: bool,
    },

    /// Restore entry content from a backup
    Restore {
        /// Entry ID to restore
        id: String,

        /// List available backups instead of restoring
        #[arg(long)]
        list: bool,

        /// Output as JSON
        #[arg(long)]
        json: bool,
    },

    /// Generate embedding for a knowledge entry
    Embed {
        /// Entry ID to embed (not used with --all)
        #[arg(required_unless_present = "all")]
        id: Option<String>,

        /// Embed all knowledge entries
        #[arg(short, long)]
        all: bool,
    },

    /// Automatically add anchors based on embedding similarity
    AutoAnchor {
        /// Entry ID to process (omit to process all entries with embeddings)
        id: Option<String>,

        /// Minimum cosine similarity threshold (0.0-1.0)
        #[arg(long, default_value = "0.75")]
        threshold: f32,

        /// Maximum anchors to add per entry
        #[arg(long, default_value = "5")]
        max_anchors: usize,

        /// Preview changes without writing
        #[arg(long)]
        dry_run: bool,

        /// Show similarity scores in output
        #[arg(long)]
        verbose: bool,
    },

    /// Manage agents registry
    Agents {
        #[command(subcommand)]
        command: AgentsCommands,
    },

    /// Export knowledge database
    Export {
        /// Output format (md, jsonl, csv)
        #[arg(short, long, default_value = "md")]
        format: String,

        /// Output directory for md format (defaults to ./memory-export), or file for jsonl/csv (defaults to stdout)
        #[arg(short, long)]
        output: Option<String>,
    },

    /// Manage projects
    Projects {
        #[command(subcommand)]
        command: ProjectsCommands,
    },

    /// Manage applicability types
    Applicability {
        #[command(subcommand)]
        command: ApplicabilityCommands,
    },

    /// Manage sessions
    Sessions {
        #[command(subcommand)]
        command: SessionsCommands,
    },

    /// Manage categories
    Categories {
        #[command(subcommand)]
        command: CategoriesCommands,
    },

    /// Query tags used in memory entries
    Tags {
        #[command(subcommand)]
        command: TagsCommands,
    },

    /// Manage source types
    SourceTypes {
        #[command(subcommand)]
        command: SourceTypesCommands,
    },

    /// Manage entry types
    EntryTypes {
        #[command(subcommand)]
        command: EntryTypesCommands,
    },

    /// Manage session types
    SessionTypes {
        #[command(subcommand)]
        command: SessionTypesCommands,
    },

    /// Manage relationship types
    RelationshipTypes {
        #[command(subcommand)]
        command: RelationshipTypesCommands,
    },

    /// Manage relationships between knowledge entries
    Relationships {
        #[command(subcommand)]
        command: RelationshipsCommands,
    },

    /// Manage content types
    ContentTypes {
        #[command(subcommand)]
        command: ContentTypesCommands,
    },

    /// Wake up with resonant identity cascade
    Wake {
        /// Number of blooms to return (default: 20)
        #[arg(short, long, default_value = "20")]
        limit: usize,

        /// Minimum resonance threshold - get ALL blooms >= this value (overrides --limit)
        #[arg(long)]
        min_resonance: Option<i32>,

        /// Include memories activated in last N days (default: 7)
        #[arg(short, long, default_value = "7")]
        days: i64,

        /// Output as JSON
        #[arg(long)]
        json: bool,

        /// Output as bash ritual script (sequential reading)
        #[arg(long)]
        ritual: bool,

        /// Output as compact markdown index (for identity loading)
        #[arg(long, conflicts_with_all = &["json", "ritual", "begin", "engage"])]
        index: bool,

        /// Don't update activation counts
        #[arg(long)]
        no_activate: bool,

        /// Interactive engage mode - verify wake phrases (requires TTY)
        #[arg(short = 'e', long)]
        engage: bool,

        /// Prompt to set missing wake phrases during engage mode
        #[arg(short = 's', long, requires = "engage")]
        set_missing: bool,

        /// Start token-based wake ritual (returns first bloom and session token)
        #[arg(long, conflicts_with_all = &["engage", "json", "ritual"])]
        begin: bool,

        /// Bloom ID for --respond or --skip operations
        #[arg(long)]
        bloom_id: Option<String>,

        /// Submit wake phrase response
        #[arg(long, conflicts_with_all = &["engage", "json", "ritual", "begin", "skip"])]
        respond: Option<String>,

        /// Skip a bloom without wake phrase
        #[arg(long, conflicts_with_all = &["engage", "json", "ritual", "begin", "respond"])]
        skip: bool,

        /// Session token for chained ritual (required with --respond or --skip)
        #[arg(long)]
        session: Option<String>,
    },

    /// List recent ephemeral facts with decay
    Recent {
        /// Number of days to look back
        #[arg(long, default_value = "10")]
        days: i32,

        /// Output as JSON
        #[arg(long)]
        json: bool,

        /// Output format (text, json) [deprecated: use --json]
        #[arg(long, default_value = "text", hide = true)]
        format: String,

        /// Filter by resonance type (e.g., ephemeral). When omitted without --all-types, defaults to ephemeral only.
        #[arg(long)]
        resonance_type: Option<String>,

        /// Surface all resonance types (blooms, patterns, insights, decisions, ephemeral, etc.)
        /// instead of ephemeral-only. Can be combined with --resonance-type to filter within
        /// the broader set.
        #[arg(long)]
        all_types: bool,

        /// Sort order: "chronological" (default) or "resonance" (highest first)
        #[arg(long, value_enum, default_value_t = RecentSortOrder::Chronological)]
        sort: RecentSortOrder,

        /// Maximum number of results
        #[arg(long, default_value = "100")]
        limit: usize,
    },

    /// Fetch facts for the wake ritual (resonance >= 3, all types, sorted by resonance)
    WakeFetch {
        /// Number of days to look back
        #[arg(long, default_value = "15")]
        days: i32,

        /// Maximum number of results
        #[arg(long, default_value = "100")]
        limit: usize,
    },

    /// List facts extracted from a specific session
    ForSession {
        /// Session ID (with or without kn- prefix)
        session_id: String,

        /// Output as JSON
        #[arg(long)]
        json: bool,

        /// Output format (text, json) [deprecated: use --json]
        #[arg(long, default_value = "text", hide = true)]
        format: String,
    },

    /// Get the session a fact was extracted from
    FactSession {
        /// Fact ID (with or without kn- prefix)
        fact_id: String,

        /// Output as JSON
        #[arg(long)]
        json: bool,

        /// Output format (text, json) [deprecated: use --json]
        #[arg(long, default_value = "text", hide = true)]
        format: String,
    },

    /// Reinforce a knowledge entry (increment resonance, update last_activated, increment activation_count)
    Reinforce {
        /// Entry ID to reinforce
        id: String,

        /// Amount to increase resonance by (default: 1)
        #[arg(long, default_value = "1")]
        amount: i32,

        /// Maximum resonance cap (default: 10)
        #[arg(long, default_value = "10")]
        cap: i32,

        /// Output as JSON
        #[arg(long)]
        json: bool,

        /// Output format (text, json) [deprecated: use --json]
        #[arg(long, default_value = "text", hide = true)]
        format: String,
    },
}

#[derive(Subcommand)]
enum GithubCommands {
    /// Clean up GitHub issues and discussions
    Cleanup {
        /// Repository (owner/repo format)
        repo: String,

        /// Issue numbers to close (comma-separated)
        #[arg(long)]
        issues: Option<String>,

        /// Discussion numbers to delete (comma-separated)
        #[arg(long)]
        discussions: Option<String>,

        /// Dry run - show what would be done
        #[arg(long)]
        dry_run: bool,
    },

    /// Post comments to issues or discussions
    Comment {
        #[command(subcommand)]
        command: CommentCommands,
    },
}

#[derive(Subcommand)]
enum CommentCommands {
    /// Post comment to an issue
    Issue {
        /// Repository (owner/repo format)
        repo: String,

        /// Issue number
        number: u64,

        /// Comment message
        message: String,

        /// Identity signature (e.g., "smith", "neo")
        #[arg(long)]
        identity: Option<String>,
    },

    /// Post comment to a discussion
    Discussion {
        /// Repository (owner/repo format)
        repo: String,

        /// Discussion number
        number: u64,

        /// Comment message
        message: String,

        /// Identity signature (e.g., "smith", "neo")
        #[arg(long)]
        identity: Option<String>,
    },
}

#[derive(Subcommand)]
enum SessionCommands {
    /// Export session to markdown
    Export {
        /// Path to session JSONL file (defaults to most recent non-agent session)
        path: Option<String>,

        /// Output file (defaults to stdout)
        #[arg(short, long)]
        output: Option<String>,
    },
}

#[derive(Subcommand)]
enum CodexCommands {
    /// Archive current session to permanent storage
    Save {
        /// Path to session JSONL file (defaults to most recent non-agent session)
        path: Option<String>,

        /// Archive all unarchived sessions
        #[arg(long)]
        all: bool,

        /// Save only conversation.md + manifest.json + images (no JSONL, no agent files)
        #[arg(long)]
        clean: bool,

        /// Include agent sub-session conversations in clean transcript
        #[arg(long, requires = "clean")]
        include_agents: bool,
    },

    /// List archived sessions
    List {
        /// Show all archives including incremental saves
        #[arg(long)]
        all: bool,

        /// Output as JSON
        #[arg(long)]
        json: bool,
    },

    /// Read an archived session
    Read {
        /// Archive ID (short UUID from list)
        id: String,

        /// Display in human-readable format
        #[arg(long)]
        human: bool,

        /// Include agent transcripts
        #[arg(long)]
        agents: bool,

        /// Filter lines matching pattern
        #[arg(long)]
        grep: Option<String>,

        /// Output as JSON
        #[arg(long)]
        json: bool,

        /// Read the clean markdown transcript (conversation.md)
        #[arg(long, conflicts_with = "human")]
        clean: bool,
    },

    /// Search all archives for a pattern
    Search {
        /// Pattern to search for
        pattern: String,

        /// Output as JSON
        #[arg(long)]
        json: bool,
    },

    /// Migrate v1 archives to v2 (extract images to files)
    Migrate {
        /// Show what would be migrated without doing it
        #[arg(long)]
        dry_run: bool,

        /// Show detailed progress
        #[arg(long)]
        verbose: bool,

        /// Generate conversation.md for archives that have session.jsonl but no clean transcript
        #[arg(long)]
        clean: bool,

        /// Include agent sub-session conversations in clean transcript
        #[arg(long, requires = "clean")]
        include_agents: bool,
    },
}

#[derive(Subcommand)]
enum AgentsCommands {
    /// List all agents
    List {
        /// Output as JSON
        #[arg(long)]
        json: bool,
    },

    /// Add a new agent
    Add {
        /// Agent ID (e.g., smith, neo, trinity)
        id: String,

        /// Agent description
        #[arg(short, long)]
        description: String,

        /// Agent domain/responsibility
        #[arg(short = 'D', long)]
        domain: String,
    },

    /// Show agent details
    Show {
        /// Agent ID
        id: String,
    },

    /// Seed agents from markdown files with YAML frontmatter
    Seed {
        /// Path to agents directory (defaults to $MX_HOME/agents/)
        #[arg(short, long)]
        path: Option<String>,
    },
}

#[derive(Subcommand)]
enum ProjectsCommands {
    /// List all projects
    List {
        /// Output as JSON
        #[arg(long)]
        json: bool,
    },
    /// Add a new project
    Add {
        /// Unique project identifier
        #[arg(long)]
        id: String,
        /// Human-readable project name
        #[arg(long)]
        name: String,
        /// Local filesystem path to the project
        #[arg(long)]
        path: Option<String>,
        /// Git repository URL (e.g., owner/repo)
        #[arg(long)]
        repo_url: Option<String>,
        /// Project description
        #[arg(long)]
        description: Option<String>,
    },
}

#[derive(Subcommand)]
enum ApplicabilityCommands {
    /// List all applicability types
    List,
    /// Add a new applicability type
    Add {
        /// Unique identifier for the applicability type
        #[arg(long)]
        id: String,
        /// Description of when this applicability applies
        #[arg(long)]
        description: String,
        /// Scope constraint (e.g., project, global)
        #[arg(long)]
        scope: Option<String>,
    },
}

#[derive(Subcommand)]
enum SessionsCommands {
    /// List sessions
    List {
        /// Filter by project ID
        #[arg(long)]
        project: Option<String>,

        /// Output as JSON
        #[arg(long)]
        json: bool,
    },
    /// Create a new session
    Create {
        /// Session type (e.g., development, review, exploration)
        #[arg(long)]
        session_type: String,
        /// Associated project ID
        #[arg(long)]
        project: Option<String>,
    },
    /// Close a session
    Close {
        /// Session ID to close
        #[arg(long)]
        id: String,
    },
}

#[derive(Subcommand)]
enum CategoriesCommands {
    /// List all categories
    List {
        /// Output as JSON
        #[arg(long)]
        json: bool,
    },
    /// Add a new category
    Add {
        /// Category ID (lowercase, no spaces)
        id: String,
        /// Description of the category
        description: String,
    },
    /// Remove a category (only if unused)
    Remove {
        /// Category ID to remove
        id: String,
    },
}

#[derive(Subcommand)]
enum TagsCommands {
    /// List all tags (optionally filter by category)
    List {
        /// Filter to tags used in a specific category
        #[arg(long)]
        category: Option<String>,

        /// Output as JSON
        #[arg(long)]
        json: bool,
    },
}

#[derive(Subcommand)]
enum SourceTypesCommands {
    /// List all source types
    List {
        /// Output as JSON
        #[arg(long)]
        json: bool,
    },
}

#[derive(Subcommand)]
enum EntryTypesCommands {
    /// List all entry types
    List {
        /// Output as JSON
        #[arg(long)]
        json: bool,
    },
}

#[derive(Subcommand)]
enum SessionTypesCommands {
    /// List all session types
    List {
        /// Output as JSON
        #[arg(long)]
        json: bool,
    },
}

#[derive(Subcommand)]
enum RelationshipTypesCommands {
    /// List all relationship types
    List {
        /// Output as JSON
        #[arg(long)]
        json: bool,
    },
}

#[derive(Subcommand)]
enum RelationshipsCommands {
    /// List all relationships for an entry
    List {
        /// Entry ID
        id: String,

        /// Output as JSON
        #[arg(long)]
        json: bool,
    },

    /// Add a relationship between two entries
    Add {
        /// Source entry ID
        #[arg(long)]
        from: String,

        /// Target entry ID
        #[arg(long)]
        to: String,

        /// Relationship type (related, supersedes, extends, implements, contradicts)
        #[arg(long)]
        r#type: String,
    },

    /// Delete a relationship
    Delete {
        /// Relationship ID
        id: String,
    },
}

#[derive(Subcommand)]
enum ContentTypesCommands {
    /// List all content types
    List {
        /// Output as JSON
        #[arg(long)]
        json: bool,
    },
}

#[derive(Subcommand)]
enum WikiCommands {
    /// Sync markdown files to GitHub wiki
    Sync {
        /// Repository (owner/repo format)
        repo: String,

        /// Source file or directory
        source: String,

        /// Custom page name (single file only)
        #[arg(long)]
        page_name: Option<String>,

        /// Dry run - show what would be synced
        #[arg(long)]
        dry_run: bool,
    },
}

fn main() -> Result<()> {
    paths::emit_mx_home_note();

    let cli = Cli::parse();

    match cli.command {
        Commands::Memory { command } => handle_memory(command, cli.verbose),
        Commands::Commit {
            message,
            all,
            push,
            encode_only,
            title,
            body,
            show_encoded,
        } => {
            if encode_only {
                // PR-style encoding: encode title and body, print to stdout.
                // `--encode-only` is its own print path and is deliberately
                // left alone — its entire purpose is to emit encoded output.
                if let (Some(t), Some(b)) = (title, body) {
                    let encoded_message = commit::encode_commit_message(&t, &b)?;
                    println!("{}", encoded_message);
                } else {
                    // This shouldn't happen due to clap validation, but handle gracefully
                    bail!("--encode-only requires both --title and --body");
                }
            } else {
                // Normal commit workflow
                let msg =
                    message.ok_or_else(|| anyhow::anyhow!("message is required for commit"))?;
                commit::upload_commit(&msg, all, push, show_encoded)?;
            }
            Ok(())
        }
        Commands::Pr { command } => handle_pr(command),
        Commands::Sync { command } => sync::handle_sync(command),
        Commands::Github { command } => handle_github(command),
        Commands::Wiki { command } => handle_wiki(command),
        Commands::Session { command } => handle_session(command),
        Commands::Codex { command } => handle_codex(command),
        Commands::Convert { command } => handle_convert(command),
        Commands::Heartbeat { since, reset } => handle_heartbeat(since, reset),
        Commands::Log { count, full, args } => handle_log(count, full, args),
        Commands::State { command } => handle_state(command),
    }
}

/// Heartbeat - calming co-regulation prompt
/// Call and response - send a heart, get one back with BPM feedback
fn handle_heartbeat(since: Option<u64>, reset: bool) -> Result<()> {
    use rand::Rng;
    use std::thread;
    use std::time::Duration;

    let hearts = [
        '❤', '🧡', '💛', '💚', '💙', '💜', '🩷', '🩵', '🤍', '💗', '💖', '💕',
    ];
    let mut rng = rand::rng();

    // Random delay 50-150ms to feel organic
    let delay = rng.random_range(50..150);
    thread::sleep(Duration::from_millis(delay));

    // Pick a random heart
    let heart = hearts[rng.random_range(0..hearts.len())];

    if reset {
        println!("{} Session reset. Breathe, Q.", heart);
        return Ok(());
    }

    match since {
        None => {
            // First call - just start
            println!("{}", heart);
            println!("Heartbeat started. Call again with --since <ms> to begin.");
        }
        Some(ms) => {
            // Calculate BPM: 60000ms / interval = beats per minute
            let bpm = 60000_u64.checked_div(ms).unwrap_or(999);

            let message = match bpm {
                0..=59 => "Nice and slow. You're safe.",
                60..=80 => "There you are. Resting.",
                81..=100 => "Getting there. Keep breathing.",
                101..=120 => "Still quick. Let the interval stretch.",
                _ => "Too fast, Q. Breathe. Slow down.",
            };

            println!("{} {} bpm", heart, bpm);
            println!("{}", message);
        }
    }

    Ok(())
}

/// Handle emotional state tensor commands
fn handle_state(cmd: StateCommands) -> Result<()> {
    use std::io::{self, Read as IoRead};
    use std::path::PathBuf;

    // Helper to load tensor schema by ID or path
    let load_tensor_schema = |schema_arg: Option<String>| -> Result<tensor::TensorSchema> {
        match schema_arg {
            Some(s) if s.contains('/') || s.contains('.') => {
                // Looks like a path
                tensor::TensorSchema::load(&PathBuf::from(s))
            }
            Some(id) => tensor::TensorSchema::load_by_id(&id),
            None => tensor::TensorSchema::load_default(),
        }
    };

    // Helper to load legacy state schema
    let load_legacy_schema = |custom_path: Option<String>| -> Result<state::StateSchema> {
        match custom_path {
            Some(p) => state::load_schema(&PathBuf::from(p)),
            None => state::load_default_schema(),
        }
    };

    match cmd {
        // === NEW TENSOR-BASED COMMANDS ===
        StateCommands::Encode {
            values,
            dimensions,
            file,
            schema,
            guided,
            format,
            runes,
        } => {
            let schema = load_tensor_schema(schema)?;

            let tensor = if guided {
                // Interactive guided mode
                tensor::guided_capture(&schema)?
            } else if let Some(dims_str) = dimensions {
                // Parse named dimensions
                tensor::StateTensor::parse_named_dimensions(&schema, &dims_str)?
            } else if let Some(file_path) = file {
                // Read from file
                let content = std::fs::read_to_string(&file_path)
                    .with_context(|| format!("Failed to read file: {}", file_path))?;

                // Try pipe-separated first, then newline-separated
                let values_str = if content.contains('|') {
                    content.trim().to_string()
                } else {
                    content
                        .lines()
                        .map(|l| l.trim())
                        .filter(|l| !l.is_empty())
                        .collect::<Vec<_>>()
                        .join("|")
                };

                tensor::StateTensor::parse_values(&schema, &values_str)?
            } else if let Some(values_str) = values {
                // Parse from argument
                tensor::StateTensor::parse_values(&schema, &values_str)?
            } else {
                // Default tensor
                tensor::StateTensor::default_from_schema(&schema)
            };

            // Output in requested format
            match format.as_str() {
                "json" => println!("{}", serde_json::to_string_pretty(&tensor)?),
                "human" => {
                    println!("{}", tensor.describe(&schema));
                    if let Some((mood_name, mood, distance)) = tensor.nearest_mood(&schema) {
                        println!("\nNearest mood: {} (distance: {:.3})", mood_name, distance);
                        println!("  {}", mood.description);
                    }
                }
                "bootstrap" => {
                    // Self-documenting bootstrap format
                    println!("{}", tensor.format_bootstrap(&schema)?);
                }
                _ => {
                    // tensor format
                    if runes {
                        println!("{}", tensor.encode_with_runes(&schema));
                    } else {
                        println!("{}", tensor.encode());
                    }
                }
            }
        }

        StateCommands::Decode {
            input,
            schema,
            format,
        } => {
            // Get input from arg or stdin
            let input_str = match input {
                Some(s) => s,
                None => {
                    let mut buf = String::new();
                    io::stdin().read_to_string(&mut buf)?;
                    buf.trim().to_string()
                }
            };

            // Decode the tensor (schema ID is embedded in the string)
            let tensor = tensor::StateTensor::decode(&input_str)?;

            // Load schema (use argument if provided, otherwise use ID from tensor)
            let schema = match schema {
                Some(s) => load_tensor_schema(Some(s))?,
                None => tensor::TensorSchema::load_by_id(&tensor.schema_id)?,
            };

            match format.as_str() {
                "json" => println!("{}", serde_json::to_string_pretty(&tensor)?),
                "tensor" => println!("{}", tensor.encode()),
                "mood" => {
                    if let Some((mood_name, mood, distance)) = tensor.nearest_mood(&schema) {
                        println!("{}", mood_name);
                        println!("  Description: {}", mood.description);
                        println!("  Distance: {:.3}", distance);
                    } else {
                        println!("(unnamed region)");
                    }
                }
                _ => {
                    // human format
                    println!("{}", tensor.describe(&schema));
                    if let Some((mood_name, mood, distance)) = tensor.nearest_mood(&schema) {
                        println!("\nNearest mood: {} (distance: {:.3})", mood_name, distance);
                        println!("  {}", mood.description);
                    }
                }
            }
        }

        StateCommands::Schemas { json } => {
            let schemas = tensor::TensorSchema::list_available()?;

            if json {
                let schema_list: Vec<serde_json::Value> = schemas
                    .iter()
                    .filter_map(|schema_id| {
                        tensor::TensorSchema::load_by_id(schema_id).ok().map(|s| {
                            serde_json::json!({
                                "id": s.id,
                                "name": s.name,
                                "dimensions": s.dimensions.len(),
                                "moods": s.moods.len(),
                            })
                        })
                    })
                    .collect();
                println!("{}", serde_json::to_string_pretty(&schema_list)?);
            } else if schemas.is_empty() {
                println!("No schemas found (checked $MX_HOME/schemas/)");
                println!("\nCreate a schema file (YAML or JSON) to get started.");
            } else {
                println!("Available schemas:\n");
                for schema_id in schemas {
                    match tensor::TensorSchema::load_by_id(&schema_id) {
                        Ok(schema) => {
                            println!(
                                "  {} - {} ({} dimensions, {} moods)",
                                schema.id,
                                schema.name,
                                schema.dimensions.len(),
                                schema.moods.len()
                            );
                        }
                        Err(_) => {
                            println!("  {} - (failed to load)", schema_id);
                        }
                    }
                }
            }
        }

        StateCommands::Moods { schema, mood, json } => {
            let schema = load_tensor_schema(schema)?;

            if let Some(mood_name) = mood {
                // Show specific mood
                match schema.moods.get(&mood_name) {
                    Some(mood_def) => {
                        if json {
                            println!("{}", serde_json::to_string_pretty(&mood_def)?);
                        } else {
                            println!("Mood: {}", mood_name);
                            println!("Description: {}", mood_def.description);
                            println!("Tolerance: {:.2}", mood_def.tolerance);
                            println!("\nTensor values:");
                            for (i, value) in mood_def.tensor.iter().enumerate() {
                                let dim_name = schema
                                    .dimensions
                                    .get(i)
                                    .map(|d| d.name.as_str())
                                    .unwrap_or("?");
                                let weight = mood_def
                                    .weights
                                    .as_ref()
                                    .and_then(|w| w.get(i))
                                    .copied()
                                    .unwrap_or(1.0);
                                println!("  {}: {:.2} (weight: {:.2})", dim_name, value, weight);
                            }
                        }
                    }
                    None => {
                        bail!(
                            "Unknown mood '{}'. Available moods: {}",
                            mood_name,
                            schema.moods.keys().cloned().collect::<Vec<_>>().join(", ")
                        );
                    }
                }
            } else {
                // List all moods
                if json {
                    println!("{}", serde_json::to_string_pretty(&schema.moods)?);
                } else {
                    println!("Moods for schema '{}' ({}):\n", schema.id, schema.name);
                    for (name, mood_def) in &schema.moods {
                        let tensor_str: Vec<String> = mood_def
                            .tensor
                            .iter()
                            .map(|v| format!("{:.2}", v))
                            .collect();
                        println!("  {:12} [{}]", name, tensor_str.join("|"));
                        println!("               {}", mood_def.description);
                    }
                }
            }
        }

        StateCommands::Info { schema, json } => {
            let schema = load_tensor_schema(schema)?;

            if json {
                println!("{}", serde_json::to_string_pretty(&schema)?);
            } else {
                println!("Schema: {} ({})", schema.name, schema.id);
                println!("Version: {}", schema.version);
                println!();
                println!("Dimensions ({}):", schema.dimensions.len());
                for dim in &schema.dimensions {
                    let rune = dim
                        .rune
                        .as_ref()
                        .map(|r| format!(" {}", r))
                        .unwrap_or_default();
                    println!("  {}{}:", dim.name, rune);
                    println!("    Low:  {}", dim.anchors.low);
                    if let Some(mid) = &dim.anchors.mid {
                        println!("    Mid:  {}", mid);
                    }
                    println!("    High: {}", dim.anchors.high);
                    println!("    Default: {:.2}", dim.default);
                }
                println!();
                println!("Moods ({}):", schema.moods.len());
                for (name, mood) in &schema.moods {
                    println!(
                        "  {:12} - {} (tol: {:.2})",
                        name, mood.description, mood.tolerance
                    );
                }
            }
        }

        // === LEGACY COMMANDS (backward compatibility) ===
        StateCommands::LegacyEncode {
            mode,
            interactive,
            format,
            schema,
        } => {
            let schema = load_legacy_schema(schema)?;

            let dynamic_state = if interactive {
                state::DynamicState::interactive_capture(&schema)?
            } else if let Some(mode_name) = mode {
                state::DynamicState::from_mode(&mode_name, &schema)?
            } else {
                state::DynamicState::from_mode("default", &schema)?
            };

            match format.as_str() {
                "json" => println!("{}", serde_json::to_string_pretty(&dynamic_state)?),
                "human" => println!("{}", dynamic_state.describe(&schema)),
                _ => println!("{}", dynamic_state.encode_stele(&schema)),
            }
        }

        StateCommands::Parse {
            file,
            preference,
            format,
            schema,
        } => {
            // Strip leading markdown bold markers (**/__) so "**Wake State:** ..."
            // matches the same as plain "Wake State: ...".  Only used in the predicate;
            // the original line is kept for value extraction.
            fn strip_md_bold(line: &str) -> &str {
                line.trim_start()
                    .trim_start_matches("**")
                    .trim_start_matches("__")
                    .trim_start()
            }

            // Extract the @state:... fragment from a matched line, stripping any
            // leading label ("Wake State:", "**Wake State:**", etc.) and trailing
            // markdown bold close ("**") that pocket inserts around the label.
            fn extract_stele_fragment(line: &str) -> &str {
                // Find the @state token wherever it appears in the line
                if let Some(pos) = line.find("@state") {
                    line[pos..]
                        .trim_end_matches('*')
                        .trim_end_matches('_')
                        .trim()
                } else {
                    line.trim()
                }
            }

            let legacy_schema = load_legacy_schema(schema.clone())?;

            let raw_line = if let Some(pref) = preference {
                pref
            } else {
                let path = file.unwrap_or_else(|| {
                    crate::paths::swap_dir()
                        .join("session-bootstrap.md")
                        .to_string_lossy()
                        .to_string()
                });

                let content = std::fs::read_to_string(&path)
                    .with_context(|| format!("Failed to read file: {}", path))?;

                content
                    .lines()
                    .find(|line| {
                        let stripped = strip_md_bold(line);
                        stripped.starts_with("Wake Preference:")
                            || stripped.starts_with("Wake State:")
                            || stripped.starts_with(&legacy_schema.stele.header)
                    })
                    .map(|s| s.to_string())
                    .unwrap_or_else(|| String::from("default"))
            };

            // Detect tensor format: @state:<namespace>|... (has colon after @state)
            let stele_fragment = extract_stele_fragment(&raw_line);
            let is_tensor_format =
                stele_fragment.starts_with("@state:") && stele_fragment.contains('|');

            if is_tensor_format {
                // Decode via tensor path which handles positional numeric values
                let tensor_schema = load_tensor_schema(schema)?;
                let tensor = tensor::StateTensor::decode(stele_fragment).with_context(|| {
                    format!("Failed to decode tensor stele: {}", stele_fragment)
                })?;

                match format.as_str() {
                    "json" => println!("{}", serde_json::to_string_pretty(&tensor.values)?),
                    "stele" => println!("{}", tensor.encode()),
                    _ => {
                        println!("Parsed: {}", stele_fragment);
                        println!();
                        println!("{}", tensor.describe(&tensor_schema));
                    }
                }
            } else {
                // Legacy path: mode names or rune-prefixed stele
                let dynamic_state =
                    state::parse_wake_preference_dynamic(&raw_line, &legacy_schema)?;

                match format.as_str() {
                    "json" => println!("{}", serde_json::to_string_pretty(&dynamic_state)?),
                    "stele" => println!("{}", dynamic_state.encode_stele(&legacy_schema)),
                    "mode" => {
                        println!("Mode calculation not yet implemented for DynamicState");
                    }
                    _ => {
                        println!("Parsed: {}", raw_line.trim());
                        println!();
                        println!("{}", dynamic_state.describe(&legacy_schema));
                    }
                }
            }
        }
    }

    Ok(())
}

/// Routing table for fact types to categories and tags
struct FactRouting {
    category: &'static str,
    tags: Vec<&'static str>,
}

/// Find an open thread by content match
///
/// Uses normalized content comparison to handle whitespace/formatting differences.
/// Threads without summary metadata are treated as potentially open: the close
/// handler always writes state, so absence implies never-closed (pre-convention threads).
fn find_open_thread_by_content(
    db: &dyn store::KnowledgeStore,
    content: &str,
    agent_id: &str,
) -> Result<String> {
    use crate::knowledge::KnowledgeEntry;

    let ctx = store::AgentContext::for_agent(agent_id);
    let filter = store::KnowledgeFilter {
        categories: Some(vec!["thread".to_string()]),
        ..Default::default()
    };

    let threads = db.list_by_category("thread", &ctx, &filter)?;
    let normalized_content = KnowledgeEntry::normalize_content(content);

    for thread in threads {
        // Check if normalized body matches and state is open (or absent — pre-convention threads)
        let is_open = match thread.get_summary_state().as_deref() {
            None => true, // Pre-convention threads lack summary metadata. Since the close
            // handler always writes state, absence implies never-closed.
            Some("open") => true,
            _ => false,
        };

        if is_open && let Some(body) = &thread.body {
            let normalized_body = KnowledgeEntry::normalize_content(body);
            if normalized_body == normalized_content {
                return Ok(thread.id);
            }
        }
    }

    bail!("No open thread found matching content: '{}'", content)
}

/// Route a fact type to its target category and tags.
/// NOTE: The category targets below (decision, insight, reference, thread) map to the default
/// seed categories in schema/surrealdb-schema.surql. Custom deployments that rename or remove
/// these seed categories must update this routing table accordingly.
fn route_fact_type(fact_type: &str) -> Result<FactRouting> {
    const VALID_FACT_TYPES: &[&str] = &[
        "decision",
        "insight",
        "person",
        "quote",
        "thread_opened",
        "commitment",
        "thread_closed",
    ];

    match fact_type {
        "decision" => Ok(FactRouting {
            category: "decision",
            tags: vec![],
        }),
        "insight" => Ok(FactRouting {
            category: "insight",
            tags: vec![],
        }),
        "person" => Ok(FactRouting {
            category: "reference",
            tags: vec!["person"],
        }),
        "quote" => Ok(FactRouting {
            category: "reference",
            tags: vec!["quote"],
        }),
        "thread_opened" => Ok(FactRouting {
            category: "thread",
            tags: vec!["question"],
        }),
        "commitment" => Ok(FactRouting {
            category: "thread",
            tags: vec!["commitment"],
        }),
        "thread_closed" => Ok(FactRouting {
            category: "thread",
            tags: vec![],
        }),
        unknown => {
            bail!(
                "Invalid fact type '{}'. Valid types: {}",
                unknown,
                VALID_FACT_TYPES.join(", ")
            )
        }
    }
}

/// Truncate a string to a maximum number of characters, adding "..." if truncated
///
/// This is UTF-8 safe - it counts characters, not bytes, avoiding panics on
/// multi-byte characters like emoji.
fn safe_truncate(s: &str, max_chars: usize) -> String {
    let char_count = s.chars().count();
    if char_count > max_chars {
        let truncated: String = s.chars().take(max_chars.saturating_sub(3)).collect();
        format!("{}...", truncated)
    } else {
        s.to_string()
    }
}

/// Resolve agent context from environment and flags
fn resolve_agent_context(mine: bool, include_private: bool) -> store::AgentContext {
    match std::env::var("MX_CURRENT_AGENT") {
        Ok(agent) if !agent.is_empty() => {
            if mine {
                // --mine: only show private entries owned by this agent
                store::AgentContext::for_agent(agent)
            } else if include_private {
                // --include-private: show public + private entries owned by this agent
                store::AgentContext::for_agent(agent)
            } else {
                // default: only show public entries
                store::AgentContext::public_for_agent(agent)
            }
        }
        _ => store::AgentContext::public_only(),
    }
}

/// Calculate cosine similarity between two vectors
///
/// Returns a value between -1.0 and 1.0 (typically 0.0 to 1.0 for normalized embeddings)
fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
    if a.len() != b.len() {
        return 0.0;
    }

    let dot_product: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
    let magnitude_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
    let magnitude_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();

    if magnitude_a == 0.0 || magnitude_b == 0.0 {
        return 0.0;
    }

    dot_product / (magnitude_a * magnitude_b)
}

/// Auto-embed a knowledge entry after add/update
///
/// This silently generates and updates the embedding for a single entry.
fn auto_embed(entry_id: &str, db: &dyn store::KnowledgeStore) -> Result<()> {
    use crate::embeddings::{EmbeddingProvider, FastEmbedProvider};

    // Get agent context for fetching the entry
    let ctx = match std::env::var("MX_CURRENT_AGENT") {
        Ok(agent) if !agent.is_empty() => store::AgentContext::for_agent(agent),
        _ => store::AgentContext::public_only(),
    };

    // Fetch the entry
    let mut entry = match db.get(entry_id, &ctx)? {
        Some(e) => e,
        None => return Ok(()), // Entry not found, skip silently
    };

    // Initialize embedding provider
    let mut provider = FastEmbedProvider::new()?;

    // Use the entry's embedding_text method (DRY - shared with other embedding paths)
    let embedding_text = entry.embedding_text();

    // Generate embedding
    let embedding = provider.embed(&embedding_text)?;

    // Update entry with embedding
    entry.embedding = Some(embedding);
    entry.embedding_model = Some(provider.model_id().to_string());
    entry.embedded_at = Some(chrono::Utc::now().to_rfc3339());
    entry.updated_at = Some(chrono::Utc::now().to_rfc3339());

    // Save to database
    db.upsert_knowledge(&entry)?;

    Ok(())
}

/// Auto-anchor a knowledge entry after add/update
///
/// This silently finds similar entries and adds anchors for a single entry.
/// Uses defaults: threshold 0.75, max 5 anchors.
fn auto_anchor(
    entry_id: &str,
    db: &dyn store::KnowledgeStore,
    explicitly_removed: Option<&[String]>,
) -> Result<()> {
    // Get agent context for fetching entries
    let ctx = match std::env::var("MX_CURRENT_AGENT") {
        Ok(agent) if !agent.is_empty() => store::AgentContext::for_agent(agent),
        _ => store::AgentContext::public_only(),
    };

    // Fetch the entry
    let entry = match db.get(entry_id, &ctx)? {
        Some(e) => e,
        None => return Ok(()), // Entry not found, skip silently
    };

    // Skip if no embedding
    if entry.embedding.is_none() {
        return Ok(());
    }

    let entry_embedding = entry.embedding.as_ref().unwrap();

    // Get all entries with embeddings for similarity comparison
    let all_candidates = db.list_all(&ctx)?;
    let candidates: Vec<_> = all_candidates
        .into_iter()
        .filter(|e| e.embedding.is_some())
        .collect();

    // Calculate similarities
    let threshold = 0.75;
    let max_anchors = 5;
    let mut similarities: Vec<(String, f32)> = Vec::new();

    for candidate in &candidates {
        // Skip self
        if candidate.id == entry.id {
            continue;
        }

        // Skip if already an anchor
        if entry.anchors.contains(&candidate.id) {
            continue;
        }

        // Skip anchors that the user explicitly removed via --anchors replacement.
        // auto_anchor is a safety net for missed connections, not an override of
        // explicit user intent.
        if let Some(removed) = explicitly_removed
            && removed.contains(&candidate.id)
        {
            continue;
        }

        // Privacy check
        let can_anchor = if entry.visibility == "private" {
            // Private can anchor to same-owner private OR public
            candidate.visibility == "public"
                || (candidate.visibility == "private" && candidate.owner == entry.owner)
        } else {
            // Public can only anchor to public
            candidate.visibility == "public"
        };

        if !can_anchor {
            continue;
        }

        // Calculate cosine similarity
        let candidate_embedding = candidate.embedding.as_ref().unwrap();
        let similarity = cosine_similarity(entry_embedding, candidate_embedding);

        // Filter by threshold, skip near-duplicates
        if similarity >= threshold && similarity <= 0.95 {
            similarities.push((candidate.id.clone(), similarity));
        }
    }

    // No similar entries found
    if similarities.is_empty() {
        return Ok(());
    }

    // Sort by similarity (descending) and take top N
    similarities.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
    let top_matches: Vec<String> = similarities
        .into_iter()
        .take(max_anchors)
        .map(|(id, _)| id)
        .collect();

    // Update the entry with new anchors
    let mut updated_anchors = entry.anchors.clone();
    updated_anchors.extend(top_matches);
    updated_anchors.sort();
    updated_anchors.dedup();

    // Create updated entry
    let mut updated_entry = entry.clone();
    updated_entry.anchors = updated_anchors;
    updated_entry.updated_at = Some(chrono::Utc::now().to_rfc3339());

    // Save to database
    db.upsert_knowledge(&updated_entry)?;

    Ok(())
}

fn handle_memory(cmd: MemoryCommands, verbose: bool) -> Result<()> {
    let config = IndexConfig::default();

    match cmd {
        MemoryCommands::Rebuild => {
            println!("Rebuilding Memory index...");
            let stats = rebuild_index(&config)?;
            println!("{}", stats);
        }

        MemoryCommands::Search {
            query,
            filter,
            semantic,
        } => {
            let db = store::create_store_with_verbose(&config.db_path, verbose)?;
            let ctx = resolve_agent_context(filter.mine, filter.include_private);

            // Note: Search doesn't activate facts - discovery != engagement
            // Build filter for database query (resonance and category)
            let db_filter = store::KnowledgeFilter {
                min_resonance: filter.min_resonance,
                max_resonance: filter.max_resonance,
                categories: filter.category.clone(),
            };

            // Get results from database with resonance filtering
            let entries = if semantic {
                use crate::embeddings::{EmbeddingProvider, FastEmbedProvider};

                eprintln!("Initializing semantic search...");
                let mut provider = FastEmbedProvider::new()?;
                let query_embedding = provider.embed(&query)?;

                // When --tags is present the in-memory filter will thin the DB results,
                // so we over-fetch to ensure enough candidates survive the tag filter.
                // Tradeoff: 5x multiplier works well at typical limits (10-50) but does
                // not scale for very large limits. The cap (limit + 200) prevents runaway
                // fetches when the caller requests hundreds of entries.
                let requested_limit = filter.limit.unwrap_or(20);
                let db_limit = if filter.tags.is_some() {
                    (requested_limit * 5).min(requested_limit + 200)
                } else {
                    requested_limit
                };

                db.semantic_search(&query_embedding, &ctx, &db_filter, db_limit)?
            } else {
                db.search(&query, &ctx, &db_filter)?
            };

            // Apply in-memory field presence filters
            let entries = apply_entry_filters(entries, &filter);

            if filter.json {
                println!("{}", serde_json::to_string_pretty(&entries)?);
            } else if entries.is_empty() {
                println!("No results for '{}'", query);
            } else {
                println!("Found {} results:\n", entries.len());
                for entry in entries {
                    print_entry_summary(&entry);
                }
            }
        }

        MemoryCommands::List { filter } => {
            let db = store::create_store_with_verbose(&config.db_path, verbose)?;
            let ctx = resolve_agent_context(filter.mine, filter.include_private);

            // Validate categories if provided
            if let Some(ref cats) = filter.category {
                for cat in cats {
                    if db.get_category(cat)?.is_none() {
                        let categories = db.list_categories()?;
                        let valid_ids: Vec<&str> =
                            categories.iter().map(|c| c.id.as_str()).collect();
                        bail!(
                            "Unknown category '{}'. Valid categories: {}",
                            cat,
                            valid_ids.join(", ")
                        );
                    }
                }
            }

            // Build filter for database query (resonance only - category handled below)
            let db_filter = store::KnowledgeFilter {
                min_resonance: filter.min_resonance,
                max_resonance: filter.max_resonance,
                categories: None,
            };

            // Get results from database with resonance filtering
            let entries = if let Some(ref cats) = filter.category {
                let mut all = Vec::new();
                for cat in cats {
                    all.extend(db.list_by_category(cat, &ctx, &db_filter)?);
                }
                all
            } else {
                // List all categories from database
                let mut all = Vec::new();
                let categories = db.list_categories()?;
                for cat in categories {
                    all.extend(db.list_by_category(&cat.id, &ctx, &db_filter)?);
                }
                all
            };

            // Apply in-memory field presence filters
            let entries = apply_entry_filters(entries, &filter);

            if filter.json {
                println!("{}", serde_json::to_string_pretty(&entries)?);
            } else if entries.is_empty() {
                println!("No entries found");
            } else {
                println!("Found {} entries:\n", entries.len());
                for entry in entries {
                    print_entry_summary(&entry);
                }
            }
        }

        MemoryCommands::Show {
            id,
            json,
            content_only,
        } => {
            let db = store::create_store_with_verbose(&config.db_path, verbose)?;
            let id = normalize_id(&id);

            // For Show, we need to respect privacy but use current agent context
            // If the user has MX_CURRENT_AGENT set, they can see their own private entries
            let ctx = match std::env::var("MX_CURRENT_AGENT") {
                Ok(agent) if !agent.is_empty() => store::AgentContext::for_agent(agent),
                _ => store::AgentContext::public_only(),
            };

            match db.get(&id, &ctx)? {
                Some(entry) => {
                    // Activate fact when viewing details
                    if entry.id.starts_with("kn-")
                        && let Err(e) = db.update_activations(std::slice::from_ref(&entry.id))
                    {
                        eprintln!("Warning: failed to update activation: {}", e);
                    }

                    if content_only {
                        if let Some(body) = &entry.body {
                            print!("{}", body);
                        }
                    } else if json {
                        println!("{}", serde_json::to_string_pretty(&entry)?);
                    } else {
                        print_entry_full(&entry);
                    }
                }
                None => {
                    bail!("Entry '{}' not found", id);
                }
            }
        }

        MemoryCommands::Stats { json } => {
            let db = store::create_store_with_verbose(&config.db_path, verbose)?;

            // For stats, show counts for current agent's perspective
            let ctx = match std::env::var("MX_CURRENT_AGENT") {
                Ok(agent) if !agent.is_empty() => store::AgentContext::for_agent(agent),
                _ => store::AgentContext::public_only(),
            };

            let total = db.count()?;
            let categories = db.list_categories()?;
            let filter = store::KnowledgeFilter::default();

            if json {
                let mut cat_counts = serde_json::Map::new();
                for cat in categories {
                    let count = db.count_by_category(&cat.id, &ctx, &filter)?;
                    cat_counts.insert(cat.id, serde_json::Value::Number(count.into()));
                }
                println!(
                    "{}",
                    serde_json::to_string_pretty(&serde_json::json!({
                        "total": total,
                        "categories": cat_counts,
                    }))?
                );
            } else {
                println!("Memory Index Statistics\n");
                println!("Total entries: {}", total);
                println!();
                for cat in categories {
                    let count = db.count_by_category(&cat.id, &ctx, &filter)?;
                    println!("  {:12} {}", cat.id, count);
                }
            }
        }

        MemoryCommands::Delete { id, json } => {
            let db = store::create_store_with_verbose(&config.db_path, verbose)?;
            let id = normalize_id(&id);

            // Respect visibility: agents can only delete entries they can see
            let current_agent = std::env::var("MX_CURRENT_AGENT")
                .ok()
                .filter(|s| !s.is_empty());
            let ctx = match &current_agent {
                Some(agent) => store::AgentContext::for_agent(agent),
                None => store::AgentContext::public_only(),
            };

            // Backup before delete (Issue #206)
            if let Some(entry) = db.get(&id, &ctx)? {
                let _ = db
                    .backup_content(&entry, "delete", current_agent.as_deref())
                    .map_err(|e| eprintln!("Warning: failed to create backup: {}", e));
            }

            if db.delete(&id, &ctx)? {
                if json {
                    println!(
                        "{}",
                        serde_json::to_string_pretty(&serde_json::json!({
                            "deleted": true,
                            "id": id,
                        }))?
                    );
                } else {
                    println!("Deleted entry '{}'", id);
                }
            } else {
                bail!("Entry '{}' not found", id);
            }
        }

        MemoryCommands::Import { path } => {
            let db = store::create_store_with_verbose(&config.db_path, verbose)?;
            let import_path = path
                .map(std::path::PathBuf::from)
                .unwrap_or_else(|| config.jsonl_path.clone());

            let count = import_jsonl(db.as_ref(), &import_path)?;
            println!("Imported {} entries from {:?}", count, import_path);
        }

        MemoryCommands::Add {
            category,
            title,
            content,
            file,
            tags,
            applicability,
            project,
            source_agent,
            source_type,
            entry_type,
            session_id,
            ephemeral,
            domain,
            content_type,
            private,
            visibility,
            owner,
            json,
            resonance,
            resonance_type,
            wake_phrase,
            wake_phrases,
            wake_order,
            anchors,
            r#type,
            session,
            thread_id,
        } => {
            use anyhow::Context;
            use std::fs;

            let db = store::create_store_with_verbose(&config.db_path, verbose)?;

            // Get content from either --content or --file
            let body = if let Some(text) = content {
                text
            } else if let Some(file_path) = file {
                fs::read_to_string(&file_path)
                    .with_context(|| format!("Failed to read file: {}", file_path))?
            } else {
                bail!("Either --content or --file must be provided");
            };

            // Determine agent - use source_agent or env var (no longer required)
            let agent_id = match source_agent {
                Some(ref sa) if !sa.is_empty() => sa.clone(),
                _ => match std::env::var("MX_CURRENT_AGENT") {
                    Ok(agent) if !agent.is_empty() => agent,
                    _ => {
                        bail!("--source-agent not provided and MX_CURRENT_AGENT not set");
                    }
                },
            };

            // Resolve visibility: --private flag is sugar for --visibility private
            let is_private = private || visibility.as_deref() == Some("private");
            if let Some(ref vis) = visibility
                && vis != "public"
                && vis != "private"
            {
                bail!("--visibility must be 'public' or 'private'");
            }

            // Handle fact type routing mode (--type flag)
            if let Some(ref fact_type) = r#type {
                // Handle thread_closed specially - updates existing thread
                if fact_type == "thread_closed" {
                    let tid = if let Some(id) = thread_id {
                        id
                    } else {
                        // Find by content match (fragile fallback)
                        find_open_thread_by_content(&*db, &body, &agent_id)?
                    };

                    // Update existing thread to closed state
                    if let Some(thread_entry) =
                        db.get(&tid, &store::AgentContext::for_agent(&agent_id))?
                    {
                        let mut meta: serde_json::Value = thread_entry
                            .summary
                            .as_deref()
                            .map(|s| {
                                serde_json::from_str(s).unwrap_or_else(|_| serde_json::json!({}))
                            })
                            .unwrap_or_else(|| serde_json::json!({}));
                        if let Some(obj) = meta.as_object_mut() {
                            obj.insert(
                                "state".to_string(),
                                serde_json::Value::String("closed".to_string()),
                            );
                        }
                        let new_summary = meta.to_string();
                        if db.update_summary(
                            &tid,
                            &new_summary,
                            &store::AgentContext::for_agent(&agent_id),
                        )? {
                            println!("Closed thread: {}", tid);
                        } else {
                            bail!("Entry '{}' not found", tid);
                        }
                        return Ok(());
                    } else {
                        bail!("Thread not found: {}", tid);
                    }
                }

                // Route fact type to category and tags
                let routing = route_fact_type(fact_type)?;

                // Build fact entry
                let now = chrono::Utc::now().to_rfc3339();
                let truncated_title = safe_truncate(&body, 60);
                let fact_title = format!("{}: {}", fact_type, truncated_title);

                // Generate ID using session if provided
                let session_hint = session.as_deref().unwrap_or("fact");
                let id = knowledge::KnowledgeEntry::generate_id(session_hint, &fact_title);

                // Build metadata JSON
                let mut metadata = serde_json::Map::new();
                metadata.insert(
                    "fact_type".to_string(),
                    serde_json::Value::String(fact_type.clone()),
                );
                metadata.insert(
                    "agent".to_string(),
                    serde_json::Value::String(agent_id.clone()),
                );
                metadata.insert(
                    "date".to_string(),
                    serde_json::Value::String(chrono::Local::now().format("%Y-%m-%d").to_string()),
                );

                // Add state field for threads
                if routing.category == "thread" {
                    metadata.insert(
                        "state".to_string(),
                        serde_json::Value::String("open".to_string()),
                    );
                }

                let summary_json = serde_json::Value::Object(metadata).to_string();

                // Merge routed tags with any user-provided tags
                let mut tag_list: Vec<String> =
                    routing.tags.iter().map(|s| s.to_string()).collect();
                if let Some(t) = tags {
                    tag_list.extend(
                        t.split(',')
                            .map(|s| s.trim().to_string())
                            .filter(|s| !s.is_empty()),
                    );
                }

                // Build the knowledge entry
                let entry = knowledge::KnowledgeEntry {
                    id: id.clone(),
                    category_id: routing.category.to_string(),
                    title: fact_title.clone(),
                    body: Some(body.clone()),
                    summary: Some(summary_json),
                    applicability: vec![],
                    source_project_id: project,
                    source_agent_id: Some(format!("agent:{}", agent_id)),
                    file_path: None,
                    tags: tag_list.clone(),
                    created_at: Some(now.clone()),
                    updated_at: Some(now),
                    content_hash: Some(knowledge::KnowledgeEntry::compute_hash(&body)),
                    source_type_id: Some("source_type:agent_session".to_string()),
                    entry_type_id: Some("entry_type:primary".to_string()),
                    session_id: session.clone(),
                    ephemeral: true,
                    content_type_id: Some("content_type:text".to_string()),
                    owner: Some(format!("agent:{}", agent_id)),
                    visibility: "public".to_string(),
                    resonance: resonance.unwrap_or(3),
                    resonance_type: Some("ephemeral".to_string()),
                    last_activated: None,
                    activation_count: 0,
                    decay_rate: 0.0,
                    anchors: vec![],
                    wake_phrases: vec![],
                    wake_order: None,
                    wake_phrase: None,
                    embedding: None,
                    embedding_model: None,
                    embedded_at: None,
                    format: "markdown".to_string(),
                    effective_resonance: None,
                };

                // Insert the fact
                db.upsert_knowledge(&entry)?;

                // Create EXTRACTED_FROM relationship to session if provided
                if let Some(ref sess) = session {
                    let session_ref = if sess.starts_with("kn-") {
                        sess.clone()
                    } else {
                        format!("kn-{}", sess)
                    };

                    let ctx = crate::store::AgentContext::public_only();
                    if db.get(&session_ref, &ctx)?.is_none() {
                        eprintln!(
                            "Warning: Session {} not found - relationship not created",
                            session_ref
                        );
                    } else {
                        db.add_relationship(&id, &session_ref, "extracted_from")?;
                    }
                }

                println!("Added fact: {}", id);
                println!("  Type: {}", fact_type);
                println!("  Category: {}", routing.category);
                println!("  Content: {}", body);

                // Auto-generate embedding if in network SurrealDB mode
                auto_embed(&id, db.as_ref())?;

                return Ok(());
            }

            // Standard memory add mode (no --type flag)
            let category = category.expect("category required when --type not provided");
            let title = title.expect("title required when --type not provided");

            // Validate category against database
            if db.get_category(&category)?.is_none() {
                let categories = db.list_categories()?;
                let valid_ids: Vec<&str> = categories.iter().map(|c| c.id.as_str()).collect();
                bail!(
                    "Invalid category '{}'. Valid categories: {}",
                    category,
                    valid_ids.join(", ")
                );
            }

            // Parse tags
            let tag_list: Vec<String> = tags
                .map(|t| {
                    t.split(',')
                        .map(|s| s.trim().to_string())
                        .filter(|s| !s.is_empty())
                        .collect()
                })
                .unwrap_or_default();

            // Parse applicability CSV
            let applicability_list: Vec<String> = applicability
                .map(|a| {
                    a.split(',')
                        .map(|s| s.trim().to_string())
                        .filter(|s| !s.is_empty())
                        .collect()
                })
                .unwrap_or_default();

            // Parse anchors CSV
            let anchor_list: Vec<String> = anchors
                .map(|a| {
                    a.split(',')
                        .map(|s| s.trim().to_string())
                        .filter(|s| !s.is_empty())
                        .collect()
                })
                .unwrap_or_default();

            // Parse wake_phrases CSV or use single wake_phrase
            let wake_phrase_list: Vec<String> = if let Some(phrases) = wake_phrases {
                phrases
                    .split(',')
                    .map(|s| s.trim().to_string())
                    .filter(|s| !s.is_empty())
                    .collect()
            } else if let Some(ref single_phrase) = wake_phrase {
                vec![single_phrase.clone()]
            } else {
                vec![]
            };

            // Determine visibility and owner
            // FIX #123: Ensure owner matches the format expected by visibility filter.
            // The visibility filter compares `owner = $current_agent` where $current_agent
            // comes from MX_CURRENT_AGENT. Owner must be stored in the same format.
            let entry_visibility = if is_private {
                "private".to_string()
            } else {
                "public".to_string()
            };

            let entry_owner = if is_private {
                // Owner defaults to agent_id (already resolved from --source-agent or MX_CURRENT_AGENT)
                Some(owner.unwrap_or_else(|| agent_id.clone()))
            } else {
                owner
            };

            // Validate resonance_type if provided
            if let Some(ref rtype) = resonance_type {
                let valid_types = [
                    "foundational",
                    "transformative",
                    "relational",
                    "operational",
                    "ephemeral",
                    "session",
                ];
                if !valid_types.contains(&rtype.as_str()) {
                    bail!(
                        "Invalid resonance type '{}'. Valid types: {}",
                        rtype,
                        valid_types.join(", ")
                    );
                }
            }

            // Generate ID
            let path_hint = domain.unwrap_or_else(|| category.clone());
            let id = knowledge::KnowledgeEntry::generate_id(&path_hint, &title);

            // Create entry
            let now = chrono::Utc::now().to_rfc3339();
            let entry = knowledge::KnowledgeEntry {
                id: id.clone(),
                category_id: category.clone(),
                title: title.clone(),
                body: Some(body),
                summary: None,
                applicability: applicability_list.clone(),
                source_project_id: project,
                source_agent_id: Some(agent_id.clone()),
                file_path: None,
                tags: tag_list,
                created_at: Some(now.clone()),
                updated_at: Some(now),
                content_hash: Some(knowledge::KnowledgeEntry::compute_hash(&title)),
                source_type_id: Some(source_type),
                entry_type_id: Some(entry_type),
                session_id: session_id.clone(),
                ephemeral,
                content_type_id: Some(content_type),
                owner: entry_owner.clone(),
                visibility: entry_visibility.clone(),
                resonance: resonance.unwrap_or(0),
                resonance_type,
                last_activated: None,
                activation_count: 0,
                decay_rate: 0.0,
                anchors: anchor_list,
                wake_phrases: wake_phrase_list,
                wake_order,
                wake_phrase,
                embedding: None,
                embedding_model: None,
                embedded_at: None,
                format: "markdown".to_string(),
                effective_resonance: None,
            };

            // Insert into database (applicability already set in struct)
            db.upsert_knowledge(&entry)?;

            // Create EXTRACTED_FROM edge when --session-id is provided.
            // Standard mode stores session_id as a field but the for-session query
            // traverses the relates_to edge — wire both paths for consistency.
            if let Some(ref sess_id) = session_id {
                let session_ref = normalize_id(sess_id);
                let ctx = crate::store::AgentContext::public_only();
                if db.get(&session_ref, &ctx)?.is_none() {
                    eprintln!(
                        "Warning: Session {} not found - EXTRACTED_FROM edge not created",
                        session_ref
                    );
                } else {
                    db.add_relationship(&id, &session_ref, "extracted_from")?;
                }
            }

            // Auto-generate embedding if in network SurrealDB mode
            auto_embed(&id, db.as_ref())?;

            // Auto-generate anchors if in network SurrealDB mode
            auto_anchor(&id, db.as_ref(), None)?;

            if json {
                println!(
                    "{}",
                    serde_json::to_string_pretty(&serde_json::json!({
                        "id": id,
                        "category": category,
                        "title": title,
                        "visibility": entry_visibility,
                        "owner": entry_owner,
                        "resonance": entry.resonance,
                        "resonance_type": entry.resonance_type,
                        "tags": entry.tags,
                        "applicability": entry.applicability,
                        "anchors": entry.anchors,
                        "wake_phrase": entry.wake_phrase,
                        "wake_phrases": entry.wake_phrases,
                    }))?
                );
            } else {
                println!("Added entry: {}", id);
                println!("  Category: {}", category);
                println!("  Title: {}", title);
                println!("  Visibility: {}", entry_visibility);
                if let Some(ref o) = entry_owner {
                    println!("  Owner: {}", o);
                }
                if entry.resonance > 0 {
                    println!("  Resonance: {}", entry.resonance);
                }
                if let Some(ref rtype) = entry.resonance_type {
                    println!("  Resonance Type: {}", rtype);
                }
                if !entry.tags.is_empty() {
                    println!("  Tags: {}", entry.tags.join(", "));
                }
                if !entry.applicability.is_empty() {
                    println!("  Applicability: {}", entry.applicability.join(", "));
                }
                if !entry.anchors.is_empty() {
                    println!("  Anchors: {}", entry.anchors.join(", "));
                }
                if let Some(ref phrase) = entry.wake_phrase {
                    println!("  Wake Phrase: {}", phrase);
                }
            }
        }

        MemoryCommands::Update {
            id,
            title,
            content,
            file,
            append_content,
            append_file,
            prepend_content,
            prepend_file,
            find,
            replace,
            replace_all,
            nth,
            category,
            tags,
            add_tag,
            remove_tag,
            applicability,
            content_type,
            resonance,
            resonance_type,
            anchors,
            add_anchor,
            remove_anchor,
            wake_phrase,
            wake_phrases,
            add_wake_phrase,
            remove_wake_phrase,
            wake_order,
            private,
            visibility,
            owner,
            session_id,
            force,
            json,
        } => {
            use anyhow::Context;
            use std::fs;

            let db = store::create_store_with_verbose(&config.db_path, verbose)?;
            let id = normalize_id(&id);

            // For Update, use current agent context to allow updating own private entries
            // #10: read MX_CURRENT_AGENT once, reuse for both ctx and backup
            let current_agent = std::env::var("MX_CURRENT_AGENT")
                .ok()
                .filter(|s| !s.is_empty());
            let ctx = match &current_agent {
                Some(agent) => store::AgentContext::for_agent(agent),
                None => store::AgentContext::public_only(),
            };

            // Fetch existing entry
            let mut entry = db
                .get(&id, &ctx)?
                .ok_or_else(|| anyhow::anyhow!("Entry not found: {}", id))?;

            // Resolve --private as sugar for --visibility private
            let visibility = if private && visibility.is_none() {
                Some("private".to_string())
            } else {
                visibility
            };

            let mut changes = Vec::new();

            // Backup before body mutation (Issue #206)
            let will_change_body = content.is_some()
                || file.is_some()
                || append_content.is_some()
                || append_file.is_some()
                || prepend_content.is_some()
                || prepend_file.is_some()
                || find.is_some();

            if will_change_body {
                let _ = db
                    .backup_content(&entry, "update", current_agent.as_deref())
                    .map_err(|e| eprintln!("Warning: failed to create backup: {}", e));
            }

            // Update title if provided
            if let Some(new_title) = title {
                changes.push(format!("title: {} -> {}", entry.title, new_title));
                entry.title = new_title;
            }

            // Track if body was changed for hash update
            let mut body_changed = false;

            // Update content - supports multiple modes:
            // 1. Full replacement via --content or --file
            // 2. Append via --append-content or --append-file
            // 3. Prepend via --prepend-content or --prepend-file
            // 4. Find/replace via --find/--replace
            if let Some(text) = content {
                changes.push("content: updated (inline)".to_string());
                entry.body = Some(text);
                body_changed = true;
            } else if let Some(file_path) = file {
                let text = fs::read_to_string(&file_path)
                    .with_context(|| format!("Failed to read file: {}", file_path))?;
                changes.push(format!("content: updated from {}", file_path));
                entry.body = Some(text);
                body_changed = true;
            } else if let Some(ref append_text) = append_content {
                let new_body = content_ops::append_content(entry.body.as_deref(), append_text);
                changes.push(format!("content: appended {} bytes", append_text.len()));
                entry.body = Some(new_body);
                body_changed = true;
            } else if let Some(ref file_path) = append_file {
                let append_text = fs::read_to_string(file_path)
                    .with_context(|| format!("Failed to read file: {}", file_path))?;
                let new_body = content_ops::append_content(entry.body.as_deref(), &append_text);
                changes.push(format!(
                    "content: appended {} bytes from {}",
                    append_text.len(),
                    file_path
                ));
                entry.body = Some(new_body);
                body_changed = true;
            } else if let Some(ref prepend_text) = prepend_content {
                let new_body = content_ops::prepend_content(entry.body.as_deref(), prepend_text);
                changes.push(format!("content: prepended {} bytes", prepend_text.len()));
                entry.body = Some(new_body);
                body_changed = true;
            } else if let Some(ref file_path) = prepend_file {
                let prepend_text = fs::read_to_string(file_path)
                    .with_context(|| format!("Failed to read file: {}", file_path))?;
                let new_body = content_ops::prepend_content(entry.body.as_deref(), &prepend_text);
                changes.push(format!(
                    "content: prepended {} bytes from {}",
                    prepend_text.len(),
                    file_path
                ));
                entry.body = Some(new_body);
                body_changed = true;
            } else if let Some(ref find_text) = find {
                let replace_text = replace.as_deref().unwrap_or("");
                let body_text = entry
                    .body
                    .as_deref()
                    .ok_or_else(|| anyhow::anyhow!("Entry has no body content to edit"))?;
                let result = content_ops::edit_content(
                    body_text,
                    find_text,
                    replace_text,
                    replace_all,
                    nth,
                )?;
                changes.push(format!(
                    "content: {} replacement{}",
                    result.replacements,
                    if result.replacements == 1 { "" } else { "s" }
                ));
                entry.body = Some(result.new_content);
                body_changed = true;
            }

            // Update category if provided
            if let Some(new_category) = category {
                // Validate category
                if db.get_category(&new_category)?.is_none() {
                    let categories = db.list_categories()?;
                    let valid_ids: Vec<&str> = categories.iter().map(|c| c.id.as_str()).collect();
                    bail!(
                        "Invalid category '{}'. Valid categories: {}",
                        new_category,
                        valid_ids.join(", ")
                    );
                }
                changes.push(format!(
                    "category: {} -> {}",
                    entry.category_id, new_category
                ));
                entry.category_id = new_category;
            }

            // Update resonance if provided
            if let Some(new_resonance) = resonance {
                changes.push(format!(
                    "resonance: {} -> {}",
                    entry.resonance, new_resonance
                ));
                entry.resonance = new_resonance;
            }

            // Update resonance type if provided
            if let Some(ref new_type) = resonance_type {
                let valid_types = [
                    "foundational",
                    "transformative",
                    "relational",
                    "operational",
                    "ephemeral",
                    "session",
                ];
                if !valid_types.contains(&new_type.as_str()) {
                    bail!(
                        "Invalid resonance type '{}'. Valid types: {}",
                        new_type,
                        valid_types.join(", ")
                    );
                }
                changes.push(format!(
                    "resonance_type: {:?} -> {}",
                    entry.resonance_type, new_type
                ));
                entry.resonance_type = Some(new_type.clone());
            }

            // Update anchors if provided (replace all)
            // Track explicitly removed anchors so auto_anchor won't re-add them
            let mut explicitly_removed_anchors: Vec<String> = Vec::new();
            if let Some(ref new_anchors) = anchors {
                let anchor_list: Vec<String> = new_anchors
                    .split(',')
                    .map(|s| normalize_id(s.trim()))
                    .filter(|s| !s.is_empty())
                    .collect();
                // Anchors in old set but not in new set were explicitly removed
                for old_anchor in &entry.anchors {
                    if !anchor_list.contains(old_anchor) {
                        explicitly_removed_anchors.push(old_anchor.clone());
                    }
                }
                changes.push(format!("anchors: {:?} -> {:?}", entry.anchors, anchor_list));
                entry.anchors = anchor_list;
            }

            // Add a single anchor
            if let Some(ref new_anchor) = add_anchor {
                let normalized = normalize_id(new_anchor);
                if !entry.anchors.contains(&normalized) {
                    entry.anchors.push(normalized.clone());
                    changes.push(format!("anchors: added '{}'", normalized));
                }
            }

            // Remove a specific anchor
            if let Some(ref anchor_to_remove) = remove_anchor {
                let normalized = normalize_id(anchor_to_remove);
                if let Some(pos) = entry.anchors.iter().position(|a| *a == normalized) {
                    entry.anchors.remove(pos);
                    changes.push(format!("anchors: removed '{}'", normalized));
                }
            }

            // Update wake phrase if provided
            if let Some(ref new_phrase) = wake_phrase {
                changes.push(format!(
                    "wake_phrase: {:?} -> {}",
                    entry.wake_phrase, new_phrase
                ));
                entry.wake_phrase = Some(new_phrase.clone());
            }

            // Update wake_phrases (replaces all)
            if let Some(ref phrases_str) = wake_phrases {
                let phrase_list: Vec<String> = phrases_str
                    .split(',')
                    .map(|s| s.trim().to_string())
                    .filter(|s| !s.is_empty())
                    .collect();
                changes.push(format!(
                    "wake_phrases: {:?} -> {:?}",
                    entry.wake_phrases, phrase_list
                ));
                entry.wake_phrases = phrase_list;
            }

            // Add a single wake phrase
            if let Some(ref new_phrase) = add_wake_phrase
                && !entry.wake_phrases.contains(new_phrase)
            {
                entry.wake_phrases.push(new_phrase.clone());
                changes.push(format!("wake_phrases: added '{}'", new_phrase));
            }

            // Remove a specific wake phrase
            if let Some(ref phrase_to_remove) = remove_wake_phrase
                && let Some(pos) = entry
                    .wake_phrases
                    .iter()
                    .position(|p| p == phrase_to_remove)
            {
                entry.wake_phrases.remove(pos);
                changes.push(format!("wake_phrases: removed '{}'", phrase_to_remove));
            }

            // Update wake_order (use '-' to clear)
            if let Some(ref order_str) = wake_order {
                if order_str == "-" {
                    changes.push("wake_order: cleared".to_string());
                    entry.wake_order = None;
                } else if let Ok(order_value) = order_str.parse::<i32>() {
                    changes.push(format!(
                        "wake_order: {:?} -> {}",
                        entry.wake_order, order_value
                    ));
                    entry.wake_order = Some(order_value);
                } else {
                    bail!(
                        "Invalid wake_order value '{}' (use number or '-' to clear)",
                        order_str
                    );
                }
            }

            // Update visibility if provided
            if let Some(ref new_vis) = visibility {
                // Validate value
                if new_vis != "public" && new_vis != "private" {
                    bail!("--visibility must be 'public' or 'private'");
                }

                let old_vis = entry.visibility.clone();

                // Bloom protection: warn when making blooms public
                if new_vis == "public" && entry.category_id == "bloom" && !force {
                    bail!(
                        "Making bloom '{}' public will expose identity data. Use --force to confirm.",
                        entry.id
                    );
                }

                // Handle public -> private: require owner
                if new_vis == "private" && old_vis == "public" {
                    let new_owner = owner.clone().or_else(|| {
                        std::env::var("MX_CURRENT_AGENT")
                            .ok()
                            .filter(|s| !s.is_empty())
                    });

                    if new_owner.is_none() {
                        bail!(
                            "Cannot make entry private without an owner. Provide --owner or set MX_CURRENT_AGENT."
                        );
                    }

                    entry.owner = new_owner;
                }

                // Handle private -> public: clear owner
                if new_vis == "public" && old_vis == "private" {
                    entry.owner = None;
                }

                changes.push(format!("visibility: {} -> {}", old_vis, new_vis));
                entry.visibility = new_vis.clone();
            }

            // Update owner if provided (only for private entries)
            if let Some(ref new_owner) = owner {
                // Only allow owner update if entry is or will be private
                let is_private =
                    visibility.as_deref() == Some("private") || entry.visibility == "private";

                if !is_private {
                    bail!(
                        "Cannot set owner on public entry. Use --visibility private to make entry private first."
                    );
                }

                changes.push(format!("owner: {:?} -> {}", entry.owner, new_owner));
                entry.owner = Some(new_owner.clone());
            }

            // Update session_id if provided
            if let Some(ref new_session_id) = session_id {
                let normalized = normalize_id(new_session_id);
                changes.push(format!(
                    "session_id: {:?} -> {}",
                    entry.session_id, normalized
                ));
                entry.session_id = Some(normalized);
            }

            // Update timestamp
            entry.updated_at = Some(chrono::Utc::now().to_rfc3339());

            // Update content hash if body was changed
            if body_changed && let Some(body) = entry.body.as_ref() {
                entry.content_hash = Some(knowledge::KnowledgeEntry::compute_hash(body));
            }

            // Update tags if provided - set on entry BEFORE upsert
            if let Some(tags_str) = tags {
                let tag_list: Vec<String> = tags_str
                    .split(',')
                    .map(|s| s.trim().to_string())
                    .filter(|s| !s.is_empty())
                    .collect();
                changes.push(format!("tags: {}", tag_list.join(", ")));
                entry.tags = tag_list;
            }

            // Add a single tag
            if let Some(ref new_tag) = add_tag {
                let tag = new_tag.trim().to_string();
                if !tag.is_empty() && !entry.tags.contains(&tag) {
                    entry.tags.push(tag.clone());
                    changes.push(format!("tags: added '{}'", tag));
                }
            }

            // Remove a specific tag
            if let Some(ref tag_to_remove) = remove_tag {
                let tag = tag_to_remove.trim().to_string();
                if let Some(pos) = entry.tags.iter().position(|t| *t == tag) {
                    entry.tags.remove(pos);
                    changes.push(format!("tags: removed '{}'", tag));
                }
            }

            // Upsert entry (now includes updated tags)
            db.upsert_knowledge(&entry)?;

            // Update applicability if provided
            if let Some(applicability_str) = applicability {
                let applicability_list: Vec<String> = applicability_str
                    .split(',')
                    .map(|s| s.trim().to_string())
                    .filter(|s| !s.is_empty())
                    .collect();
                changes.push(format!("applicability: {}", applicability_list.join(", ")));
                entry.applicability = applicability_list;
                db.upsert_knowledge(&entry)?;
            }

            // Update content type if provided
            if let Some(new_content_type) = content_type {
                changes.push(format!(
                    "content_type: {} -> {}",
                    entry.content_type_id.as_deref().unwrap_or("none"),
                    new_content_type
                ));
                entry.content_type_id = Some(new_content_type);
                // Re-upsert to update content_type_id
                db.upsert_knowledge(&entry)?;
            }

            // Auto-generate embedding if in network SurrealDB mode
            auto_embed(&id, db.as_ref())?;

            // Auto-generate anchors if in network SurrealDB mode
            // Pass explicitly removed anchors so auto_anchor respects user intent:
            // if the user did --anchors (full replacement) and removed some anchors,
            // auto_anchor should not re-add them.
            let removed = if explicitly_removed_anchors.is_empty() {
                None
            } else {
                Some(explicitly_removed_anchors.as_slice())
            };
            auto_anchor(&id, db.as_ref(), removed)?;

            if json {
                println!(
                    "{}",
                    serde_json::to_string_pretty(&serde_json::json!({
                        "id": id,
                        "changes": changes,
                    }))?
                );
            } else {
                println!("Updated entry: {}", id);
                if changes.is_empty() {
                    println!("  No changes specified");
                } else {
                    for change in &changes {
                        println!("  {}", change);
                    }
                }
            }
        }

        MemoryCommands::Edit {
            id,
            find,
            replace,
            replace_all,
            nth,
            json,
        } => {
            let db = store::create_store_with_verbose(&config.db_path, verbose)?;
            let id = normalize_id(&id);

            // Use current agent context for private entry access
            let current_agent = std::env::var("MX_CURRENT_AGENT")
                .ok()
                .filter(|s| !s.is_empty());
            let ctx = match &current_agent {
                Some(agent) => store::AgentContext::for_agent(agent),
                None => store::AgentContext::public_only(),
            };

            // Backup before edit (Issue #206)
            if let Some(entry) = db.get(&id, &ctx)? {
                let _ = db
                    .backup_content(&entry, "edit", current_agent.as_deref())
                    .map_err(|e| eprintln!("Warning: failed to create backup: {}", e));
            }

            let result = db.edit_content(&id, &ctx, &find, &replace, replace_all, nth)?;

            // Auto-generate embedding if in network SurrealDB mode
            auto_embed(&id, db.as_ref())?;

            // Auto-generate anchors if in network SurrealDB mode
            auto_anchor(&id, db.as_ref(), None)?;

            if json {
                println!(
                    "{}",
                    serde_json::to_string_pretty(&serde_json::json!({
                        "id": id,
                        "replacements": result.replacements,
                    }))?
                );
            } else {
                println!("Edited entry: {}", id);
                println!(
                    "  {} replacement{}",
                    result.replacements,
                    if result.replacements == 1 { "" } else { "s" }
                );
            }
        }

        MemoryCommands::Append {
            id,
            content,
            file,
            json,
        } => {
            use std::io::{self, Read};

            let db = store::create_store_with_verbose(&config.db_path, verbose)?;
            let id = normalize_id(&id);

            // Use current agent context for private entry access
            let current_agent = std::env::var("MX_CURRENT_AGENT")
                .ok()
                .filter(|s| !s.is_empty());
            let ctx = match &current_agent {
                Some(agent) => store::AgentContext::for_agent(agent),
                None => store::AgentContext::public_only(),
            };

            // Get content from argument, file, or stdin
            let text = if let Some(c) = content {
                c
            } else if let Some(file_path) = file {
                std::fs::read_to_string(&file_path)
                    .with_context(|| format!("Failed to read file: {}", file_path))?
            } else {
                let mut buffer = String::new();
                io::stdin()
                    .read_to_string(&mut buffer)
                    .context("Failed to read from stdin")?;
                buffer.trim_end().to_string()
            };

            if text.is_empty() {
                bail!("No content provided");
            }

            // Backup before append (Issue #206)
            if let Some(entry) = db.get(&id, &ctx)? {
                let _ = db
                    .backup_content(&entry, "append", current_agent.as_deref())
                    .map_err(|e| eprintln!("Warning: failed to create backup: {}", e));
            }

            db.append_content(&id, &ctx, &text)?;

            // Auto-generate embedding if in network SurrealDB mode
            auto_embed(&id, db.as_ref())?;

            // Auto-generate anchors if in network SurrealDB mode
            auto_anchor(&id, db.as_ref(), None)?;

            if json {
                println!(
                    "{}",
                    serde_json::to_string_pretty(&serde_json::json!({
                        "id": id,
                        "bytes_added": text.len(),
                    }))?
                );
            } else {
                println!("Appended to entry: {}", id);
                println!("  {} bytes added", text.len());
            }
        }

        MemoryCommands::Prepend {
            id,
            content,
            file,
            json,
        } => {
            use std::io::{self, Read};

            let db = store::create_store_with_verbose(&config.db_path, verbose)?;
            let id = normalize_id(&id);

            // Use current agent context for private entry access
            let current_agent = std::env::var("MX_CURRENT_AGENT")
                .ok()
                .filter(|s| !s.is_empty());
            let ctx = match &current_agent {
                Some(agent) => store::AgentContext::for_agent(agent),
                None => store::AgentContext::public_only(),
            };

            // Get content from argument, file, or stdin
            let text = if let Some(c) = content {
                c
            } else if let Some(file_path) = file {
                std::fs::read_to_string(&file_path)
                    .with_context(|| format!("Failed to read file: {}", file_path))?
            } else {
                let mut buffer = String::new();
                io::stdin()
                    .read_to_string(&mut buffer)
                    .context("Failed to read from stdin")?;
                buffer.trim_end().to_string()
            };

            if text.is_empty() {
                bail!("No content provided");
            }

            // Backup before prepend (Issue #206)
            if let Some(entry) = db.get(&id, &ctx)? {
                let _ = db
                    .backup_content(&entry, "prepend", current_agent.as_deref())
                    .map_err(|e| eprintln!("Warning: failed to create backup: {}", e));
            }

            db.prepend_content(&id, &ctx, &text)?;

            // Auto-generate embedding if in network SurrealDB mode
            auto_embed(&id, db.as_ref())?;

            // Auto-generate anchors if in network SurrealDB mode
            auto_anchor(&id, db.as_ref(), None)?;

            if json {
                println!(
                    "{}",
                    serde_json::to_string_pretty(&serde_json::json!({
                        "id": id,
                        "bytes_added": text.len(),
                    }))?
                );
            } else {
                println!("Prepended to entry: {}", id);
                println!("  {} bytes added", text.len());
            }
        }

        MemoryCommands::Restore { id, list, json } => {
            let db = store::create_store_with_verbose(&config.db_path, verbose)?;
            let id = normalize_id(&id);

            // Shared agent context (#10: read MX_CURRENT_AGENT once)
            let current_agent = std::env::var("MX_CURRENT_AGENT")
                .ok()
                .filter(|s| !s.is_empty());
            let ctx = match &current_agent {
                Some(agent) => store::AgentContext::for_agent(agent),
                None => store::AgentContext::public_only(),
            };

            if list {
                // List available backups
                // #7: filter by visibility — only show backups for entries the agent can see
                if db.get(&id, &ctx)?.is_none() {
                    if json {
                        println!("{}", serde_json::to_string_pretty(&serde_json::json!([]))?);
                    } else {
                        println!("No entry or backups found for {}", id);
                    }
                } else {
                    let backups = db.list_backups(&id)?;
                    if json {
                        println!("{}", serde_json::to_string_pretty(&backups)?);
                    } else if backups.is_empty() {
                        println!("No backups found for {}", id);
                    } else {
                        println!("Backups for {}:", id);
                        for b in &backups {
                            let body_len = b.body.as_ref().map(|s| s.len()).unwrap_or(0);
                            println!(
                                "  {} | {} | {} | {} bytes",
                                b.id,
                                b.created_at.as_deref().unwrap_or("unknown"),
                                b.operation,
                                body_len,
                            );
                        }
                    }
                }
            } else {
                let backup = db
                    .latest_backup(&id)?
                    .ok_or_else(|| anyhow::anyhow!("No backups found for {}", id))?;

                // #5: single fetch, #6: better error for deleted entries
                let mut entry = match db.get(&id, &ctx)? {
                    Some(entry) => {
                        // Backup current state before restoring
                        if let Err(e) =
                            db.backup_content(&entry, "update", current_agent.as_deref())
                        {
                            eprintln!(
                                "Warning: failed to backup current state before restore: {}",
                                e
                            );
                        }
                        entry
                    }
                    None => {
                        bail!(
                            "Entry '{}' not found (may have been deleted). \
                             Restore from backup after deletion is not yet supported.",
                            id
                        );
                    }
                };

                // Restore body from backup
                entry.body = backup.body.clone();

                // #4: set updated_at
                entry.updated_at = Some(chrono::Utc::now().to_rfc3339());

                // Recompute content hash
                let hash_body = entry.body.as_deref().unwrap_or("").to_string();
                entry.content_hash = Some(knowledge::KnowledgeEntry::compute_hash(&hash_body));

                db.upsert_knowledge(&entry)?;

                // #3: update embeddings and anchors like all other mutation paths
                auto_embed(&id, db.as_ref())?;
                auto_anchor(&id, db.as_ref(), None)?;

                if json {
                    println!(
                        "{}",
                        serde_json::to_string_pretty(&serde_json::json!({
                            "restored": true,
                            "id": id,
                            "from_backup": backup.id,
                            "backup_created": backup.created_at,
                            "operation": backup.operation,
                        }))?
                    );
                } else {
                    println!("Restored entry: {}", id);
                    println!("  from backup: {}", backup.id);
                    println!(
                        "  backup created: {}",
                        backup.created_at.as_deref().unwrap_or("unknown")
                    );
                    println!("  original operation: {}", backup.operation);
                }
            }
        }

        MemoryCommands::Embed { id, all } => {
            use crate::embeddings::{EmbeddingProvider, FastEmbedProvider};

            let db = store::create_store_with_verbose(&config.db_path, verbose)?;

            // Use current agent context for private entry access
            let ctx = match std::env::var("MX_CURRENT_AGENT") {
                Ok(agent) if !agent.is_empty() => store::AgentContext::for_agent(agent),
                _ => store::AgentContext::public_only(),
            };

            // Initialize embedding provider once
            println!("Initializing FastEmbed model...");
            let mut provider = FastEmbedProvider::new()?;

            if all {
                // Embed ALL entries
                let entries = db.list_all(&ctx)?;
                let total = entries.len();

                println!("Found {} entries to embed", total);

                for (idx, mut entry) in entries.into_iter().enumerate() {
                    // Construct embedding text from title + summary/body + tags
                    let mut parts = vec![entry.title.clone()];

                    if let Some(summary) = &entry.summary {
                        parts.push(summary.clone());
                    } else if let Some(body) = &entry.body {
                        parts.push(body.chars().take(2000).collect());
                    }

                    if !entry.tags.is_empty() {
                        parts.push(format!("Tags: {}", entry.tags.join(", ")));
                    }

                    let embedding_text = parts.join("\n\n");

                    // Generate embedding
                    println!("Embedded {}/{}: {}", idx + 1, total, entry.title);
                    let embedding = provider.embed(&embedding_text)?;

                    // Update entry with embedding
                    entry.embedding = Some(embedding);
                    entry.embedding_model = Some(provider.model_id().to_string());
                    entry.embedded_at = Some(chrono::Utc::now().to_rfc3339());
                    entry.updated_at = Some(chrono::Utc::now().to_rfc3339());

                    // Save to database
                    db.upsert_knowledge(&entry)?;
                }

                println!("✓ All {} entries embedded successfully!", total);
                println!("  Model: {}", provider.model_id());
                println!("  Dimensions: {}", provider.dimensions());
            } else {
                // Embed single entry
                let entry_id = id.ok_or_else(|| {
                    anyhow::anyhow!("Entry ID required (use --all to embed all entries)")
                })?;

                // Fetch entry
                let mut entry = db
                    .get(&entry_id, &ctx)?
                    .ok_or_else(|| anyhow::anyhow!("Entry not found: {}", entry_id))?;

                // Construct embedding text from title + summary/body + tags
                let mut parts = vec![entry.title.clone()];

                if let Some(summary) = &entry.summary {
                    parts.push(summary.clone());
                } else if let Some(body) = &entry.body {
                    parts.push(body.chars().take(2000).collect());
                }

                if !entry.tags.is_empty() {
                    parts.push(format!("Tags: {}", entry.tags.join(", ")));
                }

                let embedding_text = parts.join("\n\n");

                // Generate embedding
                println!("Generating embedding for '{}'...", entry.title);
                let embedding = provider.embed(&embedding_text)?;

                // Update entry with embedding
                entry.embedding = Some(embedding);
                entry.embedding_model = Some(provider.model_id().to_string());
                entry.embedded_at = Some(chrono::Utc::now().to_rfc3339());
                entry.updated_at = Some(chrono::Utc::now().to_rfc3339());

                // Save to database
                db.upsert_knowledge(&entry)?;

                println!("✓ Embedding generated and saved!");
                println!("  Entry: {}", entry_id);
                println!("  Model: {}", provider.model_id());
                println!("  Dimensions: {}", provider.dimensions());
            }
        }

        MemoryCommands::AutoAnchor {
            id,
            threshold,
            max_anchors,
            dry_run,
            verbose,
        } => {
            let db = store::create_store_with_verbose(&config.db_path, verbose)?;

            // Use current agent context for private entry access
            let ctx = match std::env::var("MX_CURRENT_AGENT") {
                Ok(agent) if !agent.is_empty() => store::AgentContext::for_agent(agent),
                _ => store::AgentContext::public_only(),
            };

            // Get entries to process
            let entries = if let Some(entry_id) = id {
                // Process single entry
                let entry = db
                    .get(&entry_id, &ctx)?
                    .ok_or_else(|| anyhow::anyhow!("Entry not found: {}", entry_id))?;

                if entry.embedding.is_none() {
                    anyhow::bail!(
                        "Entry {} has no embedding. Run `mx memory embed {}` first.",
                        entry_id,
                        entry_id
                    );
                }

                vec![entry]
            } else {
                // Get all entries with embeddings
                let all_entries = db.list_all(&ctx)?;
                all_entries
                    .into_iter()
                    .filter(|e| e.embedding.is_some())
                    .collect()
            };

            if entries.is_empty() {
                println!("No entries with embeddings found.");
                return Ok(());
            }

            println!("Processing {} entries...", entries.len());

            // Get ALL entries with embeddings for similarity comparison
            let all_candidates = db.list_all(&ctx)?;
            let candidates: Vec<_> = all_candidates
                .into_iter()
                .filter(|e| e.embedding.is_some())
                .collect();

            let mut total_added = 0;
            let entries_count = entries.len();

            for entry in entries {
                let entry_embedding = entry.embedding.as_ref().unwrap();

                // Calculate similarities
                let mut similarities: Vec<(String, String, f32)> = Vec::new();

                for candidate in &candidates {
                    // Skip self
                    if candidate.id == entry.id {
                        continue;
                    }

                    // Skip if already an anchor
                    if entry.anchors.contains(&candidate.id) {
                        continue;
                    }

                    // Privacy check
                    let can_anchor = if entry.visibility == "private" {
                        // Private can anchor to same-owner private OR public
                        candidate.visibility == "public"
                            || (candidate.visibility == "private" && candidate.owner == entry.owner)
                    } else {
                        // Public can only anchor to public
                        candidate.visibility == "public"
                    };

                    if !can_anchor {
                        continue;
                    }

                    // Calculate cosine similarity
                    let candidate_embedding = candidate.embedding.as_ref().unwrap();
                    let similarity = cosine_similarity(entry_embedding, candidate_embedding);

                    // Filter by threshold, skip near-duplicates
                    if similarity >= threshold && similarity <= 0.95 {
                        similarities.push((
                            candidate.id.clone(),
                            candidate.title.clone(),
                            similarity,
                        ));
                    }
                }

                // Sort by similarity (descending) and take top N
                similarities
                    .sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal));
                let top_matches: Vec<_> = similarities.into_iter().take(max_anchors).collect();

                if top_matches.is_empty() {
                    if verbose {
                        println!(
                            "  {} \"{}\" - No similar entries found",
                            entry.id, entry.title
                        );
                    }
                    continue;
                }

                println!("Processing {} \"{}\"...", entry.id, entry.title);

                for (match_id, match_title, score) in &top_matches {
                    if verbose {
                        println!("  → {} \"{}\" ({:.2})", match_id, match_title, score);
                    } else {
                        println!("  → {} \"{}\"", match_id, match_title);
                    }
                }

                if dry_run {
                    println!(
                        "[DRY RUN] Would add {} anchors to {}",
                        top_matches.len(),
                        entry.id
                    );
                } else {
                    // Update the entry with new anchors
                    let new_anchor_ids: Vec<String> =
                        top_matches.iter().map(|(id, _, _)| id.clone()).collect();

                    // Merge with existing anchors
                    let mut updated_anchors = entry.anchors.clone();
                    updated_anchors.extend(new_anchor_ids);
                    updated_anchors.sort();
                    updated_anchors.dedup();

                    // Create updated entry
                    let mut updated_entry = entry.clone();
                    updated_entry.anchors = updated_anchors;
                    updated_entry.updated_at = Some(chrono::Utc::now().to_rfc3339());

                    // Save to database
                    db.upsert_knowledge(&updated_entry)?;

                    println!("Added {} anchors", top_matches.len());
                    total_added += top_matches.len();
                }
            }

            if dry_run {
                println!("\n[DRY RUN] Complete. No changes written.");
            } else {
                println!(
                    "\n✓ Added {} total anchors across {} entries",
                    total_added, entries_count
                );
            }
        }
        MemoryCommands::Agents { command } => handle_agents(command, &config)?,

        MemoryCommands::Projects { command } => handle_projects(command, &config)?,

        MemoryCommands::Applicability { command } => handle_applicability(command, &config)?,

        MemoryCommands::Sessions { command } => handle_sessions(command, &config)?,

        MemoryCommands::Categories { command } => handle_categories(command, &config)?,

        MemoryCommands::Tags { command } => handle_tags(command, &config)?,

        MemoryCommands::SourceTypes { command } => handle_source_types(command, &config)?,

        MemoryCommands::EntryTypes { command } => handle_entry_types(command, &config)?,

        MemoryCommands::SessionTypes { command } => handle_session_types(command, &config)?,

        MemoryCommands::RelationshipTypes { command } => {
            handle_relationship_types(command, &config)?
        }

        MemoryCommands::Relationships { command } => handle_relationships(command, &config)?,

        MemoryCommands::ContentTypes { command } => handle_content_types(command, &config)?,

        MemoryCommands::Export { format, output } => {
            let db = store::create_store(&config.db_path)?;

            match format.as_str() {
                "md" | "markdown" => {
                    // Markdown exports to directory
                    let output_dir = output.as_deref().unwrap_or("./memory-export");

                    let dir_path = std::path::PathBuf::from(output_dir);
                    export_markdown(db.as_ref(), &dir_path)?;
                    println!("Exported to directory: {}", output_dir);
                }
                "jsonl" => {
                    // JSONL exports to file or stdout
                    if let Some(ref path) = output {
                        export_jsonl(db.as_ref(), &std::path::PathBuf::from(path))?;
                        println!("Exported to {}", path);
                    } else {
                        export_jsonl(db.as_ref(), &std::path::PathBuf::from("/dev/stdout"))?;
                    }
                }
                "csv" => {
                    // CSV exports to file or stdout
                    if let Some(ref path) = output {
                        export_csv(db.as_ref(), &std::path::PathBuf::from(path))?;
                        println!("Exported to {}", path);
                    } else {
                        export_csv(db.as_ref(), &std::path::PathBuf::from("/dev/stdout"))?;
                    }
                }
                _ => {
                    bail!("Invalid format '{}'. Valid formats: md, jsonl, csv", format);
                }
            }
        }

        MemoryCommands::Wake {
            limit,
            min_resonance,
            days,
            json,
            ritual,
            index,
            no_activate,
            engage,
            set_missing,
            begin,
            bloom_id,
            respond,
            skip,
            session,
        } => {
            let db = store::create_store(&config.db_path)?;

            // Get current agent context - required for wake
            let current_agent = match std::env::var("MX_CURRENT_AGENT") {
                Ok(agent) if !agent.is_empty() => agent,
                _ => {
                    bail!("MX_CURRENT_AGENT not set. Cannot wake without identity.");
                }
            };

            let ctx = store::AgentContext::for_agent(current_agent.clone());

            // Run cascade
            let cascade = db.wake_cascade(&ctx, limit, min_resonance, days)?;

            // Increment activation counts for wake cascade entries.
            // We do NOT reset last_activated here — wake surfacing is passive, not
            // intentional access, and resetting the decay clock would create a feedback
            // loop where frequently-surfaced entries never decay.
            if !no_activate {
                let ids = cascade.all_ids();
                if !ids.is_empty() {
                    db.increment_activation_count(&ids)?;
                }
            }

            // Output
            if begin {
                // Start session-based ritual (state stored in DB)
                let output = wake_ritual::begin_ritual(db.as_ref(), &cascade)?;
                println!("{}", output);
            } else if let Some(phrase) = respond {
                // Submit wake phrase response
                let session_token =
                    session.ok_or_else(|| anyhow::anyhow!("--session required with --respond"))?;
                let id = bloom_id
                    .ok_or_else(|| anyhow::anyhow!("--bloom-id required with --respond"))?;

                let output =
                    wake_ritual::respond_ritual(db.as_ref(), &ctx, &id, &phrase, &session_token)?;
                println!("{}", output);
            } else if skip {
                // Skip a bloom
                let session_token =
                    session.ok_or_else(|| anyhow::anyhow!("--session required with --skip"))?;
                let id =
                    bloom_id.ok_or_else(|| anyhow::anyhow!("--bloom-id required with --skip"))?;

                let output = wake_ritual::skip_ritual(db.as_ref(), &ctx, &id, &session_token)?;
                println!("{}", output);
            } else if engage {
                // Interactive engage mode
                engage::run_engage_ritual(&cascade, db.as_ref(), set_missing)?;
            } else if json {
                println!("{}", serde_json::to_string_pretty(&cascade)?);
            } else if index {
                print_wake_index(&cascade);
            } else if ritual {
                print_wake_ritual(&cascade, &current_agent);
            } else {
                print_wake_cascade(&cascade);
            }
        }

        MemoryCommands::Recent {
            days,
            json,
            format,
            resonance_type,
            all_types,
            sort,
            limit,
        } => {
            let db = store::create_store_with_verbose(&config.db_path, verbose)?;

            // Note: Listing doesn't activate facts - bulk view != focused access
            // Auto-enable all_types when --resonance-type is set, otherwise the
            // default ephemeral-only query would silently return nothing for
            // non-ephemeral types (e.g. `--resonance-type foundational`).
            let all_types = all_types || resonance_type.is_some();

            // Decide which query to use:
            //   --all-types (or --resonance-type) => query all resonance types
            //   (default)                         => ephemeral only (backwards compatible)
            // --resonance-type filter is applied post-query in both cases.
            let mut facts = if all_types {
                db.query_recent_facts_all_types(days)?
            } else {
                db.query_recent_facts(days)?
            };

            // Filter by resonance_type if provided (works with both code paths)
            if let Some(ref rtype) = resonance_type {
                facts.retain(|f| f.resonance_type.as_deref() == Some(rtype.as_str()));
            }

            // Apply sort: "resonance" sorts by effective_resonance (decay-adjusted) highest-first.
            // DB already returns entries ORDER BY effective_resonance DESC; the default path
            // preserves that ordering rather than re-sorting, so a resonance-9 from 6 months
            // ago does not outrank a resonance-7 from yesterday.
            if matches!(sort, RecentSortOrder::Resonance) {
                facts.sort_by(|a, b| {
                    // Sort by effective_resonance (decay-adjusted) when available;
                    // fall back to raw resonance for entries that lack it.
                    let a_val = a.effective_resonance.unwrap_or(a.resonance as f64);
                    let b_val = b.effective_resonance.unwrap_or(b.resonance as f64);
                    b_val
                        .partial_cmp(&a_val)
                        .unwrap_or(std::cmp::Ordering::Equal)
                });
            }
            // Default: preserve DB ordering (effective_resonance DESC). No re-sort needed.

            // Apply limit
            facts.truncate(limit);

            // Support both --json flag and legacy --format json
            if json || format == "json" {
                let json_facts: Vec<serde_json::Value> = facts
                    .iter()
                    .map(|f| {
                        let fact_type = f
                            .summary
                            .as_ref()
                            .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
                            .and_then(|v: serde_json::Value| {
                                v.get("fact_type")
                                    .and_then(|t| t.as_str())
                                    .map(String::from)
                            });

                        serde_json::json!({
                            "id": f.id,
                            "type": fact_type,
                            "content": f.body.as_ref().unwrap_or(&"".to_string()),
                            "created_at": f.created_at.as_ref().unwrap_or(&"".to_string()),
                            "resonance": f.resonance,
                        })
                    })
                    .collect();
                println!("{}", serde_json::to_string_pretty(&json_facts)?);
            } else {
                for fact in facts {
                    let summary_json = fact
                        .summary
                        .as_ref()
                        .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok());

                    let fact_type = summary_json
                        .as_ref()
                        .and_then(|v: &serde_json::Value| {
                            v.get("fact_type")
                                .and_then(|t| t.as_str())
                                .map(String::from)
                        })
                        .unwrap_or_else(|| "unknown".to_string());

                    let state = fact.get_summary_state();

                    let date = fact
                        .created_at
                        .as_ref()
                        .and_then(|dt_str: &String| {
                            chrono::DateTime::parse_from_rfc3339(dt_str).ok()
                        })
                        .map(|dt| dt.format("%Y-%m-%d").to_string())
                        .unwrap_or_else(|| "unknown".to_string());

                    let content = fact.body.as_deref().unwrap_or("");
                    let preview = safe_truncate(content, 60);

                    if let Some(state) = state {
                        println!(
                            "[{}] {} ({}): {} ({}, resonance {})",
                            date, fact_type, state, preview, fact.id, fact.resonance
                        );
                    } else {
                        println!(
                            "[{}] {}: {} ({}, resonance {})",
                            date, fact_type, preview, fact.id, fact.resonance
                        );
                    }
                }
            }
        }

        MemoryCommands::WakeFetch { days, limit } => {
            if days <= 0 {
                bail!("--days must be a positive integer (got {days})");
            }

            let db = store::create_store_with_verbose(&config.db_path, verbose)?;

            let mut facts = db.query_recent_facts_all_types(days)?;

            // Filter to resonance >= 3 AND extract fact_type in a single pass.
            // Collect (entry, fact_type) pairs so we don't re-parse summary JSON later.
            let mut typed_facts: Vec<(crate::knowledge::KnowledgeEntry, String)> = facts
                .drain(..)
                .filter(|f| f.resonance >= 3)
                .filter_map(|f| {
                    let ft = f
                        .summary
                        .as_ref()
                        .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
                        .and_then(|v| v.get("fact_type")?.as_str().map(String::from))?;
                    Some((f, ft))
                })
                .collect();

            // Sort by effective resonance (decay-adjusted), highest first
            typed_facts.sort_by(|(a, _), (b, _)| {
                let a_val = a.effective_resonance.unwrap_or(a.resonance as f64);
                let b_val = b.effective_resonance.unwrap_or(b.resonance as f64);
                b_val
                    .partial_cmp(&a_val)
                    .unwrap_or(std::cmp::Ordering::Equal)
            });

            // Apply limit
            typed_facts.truncate(limit);

            if typed_facts.is_empty() {
                println!("(no memory entries returned)");
                return Ok(());
            }

            println!("<facts>");
            for (i, (fact, fact_type)) in typed_facts.iter().enumerate() {
                if i > 0 {
                    println!();
                }

                let date = fact
                    .created_at
                    .as_ref()
                    .and_then(|dt_str| chrono::DateTime::parse_from_rfc3339(dt_str).ok())
                    .map(|dt| dt.format("%Y-%m-%d").to_string())
                    .unwrap_or_else(|| "unknown".to_string());

                let content = fact.body.as_deref().unwrap_or("");

                println!(
                    "[{}] {} (resonance {}) {}",
                    date, fact_type, fact.resonance, fact.id
                );
                let escaped = content.replace("]]>", "]]]]><![CDATA[>");
                println!("<![CDATA[{}]]>", escaped);
            }
            println!("</facts>");
        }

        MemoryCommands::ForSession {
            session_id,
            json,
            format,
        } => {
            let db = store::create_store_with_verbose(&config.db_path, verbose)?;

            // Normalize session ID
            let session_ref = normalize_id(&session_id);

            // Get fact IDs
            let fact_ids = db.get_facts_for_session(&session_ref)?;

            if fact_ids.is_empty() {
                println!("No facts found for session: {}", session_ref);
                return Ok(());
            }

            // Increment activation counts for session facts — viewing a session is
            // passive bulk access, not intentional recall of any single entry.
            // Do NOT reset last_activated so decay continues normally.
            if !fact_ids.is_empty()
                && let Err(e) = db.increment_activation_count(&fact_ids)
            {
                eprintln!("Warning: failed to update activation counts: {}", e);
            }

            // Fetch full entries for each fact
            let ctx = match std::env::var("MX_CURRENT_AGENT") {
                Ok(agent) if !agent.is_empty() => store::AgentContext::for_agent(agent),
                _ => store::AgentContext::public_only(),
            };

            // Support both --json flag and legacy --format json
            if json || format == "json" {
                let mut json_facts = Vec::new();
                for fact_id in &fact_ids {
                    if let Some(fact) = db.get(fact_id, &ctx)? {
                        let fact_type = fact
                            .summary
                            .as_ref()
                            .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
                            .and_then(|v: serde_json::Value| {
                                v.get("fact_type")
                                    .and_then(|t| t.as_str())
                                    .map(String::from)
                            });

                        json_facts.push(serde_json::json!({
                            "id": fact.id,
                            "type": fact_type,
                            "content": fact.body.as_ref().unwrap_or(&"".to_string()),
                            "created_at": fact.created_at.as_ref().unwrap_or(&"".to_string()),
                            "resonance": fact.resonance,
                        }));
                    }
                }
                println!("{}", serde_json::to_string_pretty(&json_facts)?);
            } else {
                println!("Facts for session {}:", session_ref);
                for fact_id in fact_ids {
                    if let Some(fact) = db.get(&fact_id, &ctx)? {
                        let fact_type = fact
                            .summary
                            .as_ref()
                            .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
                            .and_then(|v: serde_json::Value| {
                                v.get("fact_type")
                                    .and_then(|t| t.as_str())
                                    .map(String::from)
                            })
                            .unwrap_or_else(|| "unknown".to_string());

                        let date = fact
                            .created_at
                            .as_ref()
                            .and_then(|dt_str: &String| {
                                chrono::DateTime::parse_from_rfc3339(dt_str).ok()
                            })
                            .map(|dt| dt.format("%Y-%m-%d").to_string())
                            .unwrap_or_else(|| "unknown".to_string());

                        let content = fact.body.as_deref().unwrap_or("");
                        let preview = safe_truncate(content, 60);

                        println!(
                            "[{}] {}: {} ({}, resonance {})",
                            date, fact_type, preview, fact.id, fact.resonance
                        );
                    }
                }
            }
        }

        MemoryCommands::FactSession {
            fact_id,
            json,
            format,
        } => {
            let db = store::create_store_with_verbose(&config.db_path, verbose)?;

            // Normalize fact ID
            let fact_ref = normalize_id(&fact_id);

            // Activate fact when fetching its session (going deeper)
            if let Err(e) = db.update_activations(std::slice::from_ref(&fact_ref)) {
                eprintln!("Warning: failed to update activation: {}", e);
            }

            // Get session ID
            // Support both --json flag and legacy --format json
            let use_json = json || format == "json";
            match db.get_session_for_fact(&fact_ref)? {
                Some(session_id) => {
                    if use_json {
                        println!(
                            "{}",
                            serde_json::to_string_pretty(&serde_json::json!({
                                "fact_id": fact_ref,
                                "session_id": session_id,
                            }))?
                        );
                    } else {
                        println!(
                            "Fact {} was extracted from session: {}",
                            fact_ref, session_id
                        );
                    }
                }
                None => {
                    if use_json {
                        println!(
                            "{}",
                            serde_json::to_string_pretty(&serde_json::json!({
                                "fact_id": fact_ref,
                                "session_id": null,
                            }))?
                        );
                    } else {
                        println!("No session found for fact: {}", fact_ref);
                    }
                }
            }
        }

        MemoryCommands::Reinforce {
            id,
            amount,
            cap,
            json,
            format,
        } => {
            let db = store::create_store_with_verbose(&config.db_path, verbose)?;

            // Normalize ID
            let normalized_id = normalize_id(&id);

            // Respect visibility: agents can only reinforce entries they can see
            let ctx = match std::env::var("MX_CURRENT_AGENT") {
                Ok(agent) if !agent.is_empty() => store::AgentContext::for_agent(agent),
                _ => store::AgentContext::public_only(),
            };

            // Call reinforce on the store
            if let Some(result) = db.reinforce(&normalized_id, amount, Some(cap), &ctx)? {
                // Output result - support both --json flag and legacy --format json
                if json || format == "json" {
                    println!("{}", serde_json::to_string_pretty(&result)?);
                } else {
                    println!("Reinforced entry: {}", result.id);
                    println!("  Old resonance: {}", result.old_resonance);
                    println!("  New resonance: {}", result.new_resonance);
                    println!("  Amount added: {}", result.amount_added);
                    if result.capped {
                        println!("  (Capped at {})", cap);
                    }
                    println!("  Last activated: {}", result.last_activated);
                    println!("  Activation count: {}", result.activation_count);
                }
            } else {
                bail!("Entry '{}' not found", normalized_id);
            }
        }
    }

    Ok(())
}

fn handle_agents(cmd: AgentsCommands, config: &IndexConfig) -> Result<()> {
    let db = store::create_store(&config.db_path)?;

    match cmd {
        AgentsCommands::List { json } => {
            let agents = db.list_agents()?;
            if json {
                println!("{}", serde_json::to_string_pretty(&agents)?);
            } else if agents.is_empty() {
                println!("No agents registered");
            } else {
                println!("Registered agents:\n");
                for agent in agents {
                    println!(
                        "  {} - {}",
                        agent.id,
                        agent.description.as_deref().unwrap_or("")
                    );
                    if let Some(domain) = &agent.domain {
                        println!("    Domain: {}", domain);
                    }
                }
            }
        }

        AgentsCommands::Add {
            id,
            description,
            domain,
        } => {
            let now = chrono::Utc::now().to_rfc3339();
            let agent = types::Agent {
                id: id.clone(),
                description: Some(description.clone()),
                domain: Some(domain.clone()),
                created_at: Some(now.clone()),
                updated_at: Some(now),
            };

            db.upsert_agent(&agent)?;
            println!("Added agent: {}", id);
            println!("  Description: {}", description);
            println!("  Domain: {}", domain);
        }

        AgentsCommands::Show { id } => match db.get_agent(&id)? {
            Some(agent) => {
                println!("Agent: {}", agent.id);
                if let Some(desc) = &agent.description {
                    println!("Description: {}", desc);
                }
                if let Some(domain) = &agent.domain {
                    println!("Domain: {}", domain);
                }
                if let Some(created) = &agent.created_at {
                    println!("Created: {}", created);
                }
                if let Some(updated) = &agent.updated_at {
                    println!("Updated: {}", updated);
                }
            }
            None => {
                bail!("Agent '{}' not found", id);
            }
        },

        AgentsCommands::Seed { path } => {
            use anyhow::Context;
            use std::fs;
            use std::path::PathBuf;

            // Determine agents directory
            let agents_dir = if let Some(p) = path {
                PathBuf::from(p)
            } else {
                // Default: $MX_HOME/agents/
                crate::paths::agents_dir()
            };

            if !agents_dir.exists() {
                bail!("Agents directory does not exist: {:?}", agents_dir);
            }

            // Scan for .md files
            let entries = fs::read_dir(&agents_dir)
                .with_context(|| format!("Failed to read directory: {:?}", agents_dir))?;

            let mut seeded = Vec::new();
            let now = chrono::Utc::now().to_rfc3339();

            for entry in entries {
                let entry = entry?;
                let path = entry.path();

                // Skip if not a markdown file
                if path.extension().and_then(|s| s.to_str()) != Some("md") {
                    continue;
                }

                // Skip files starting with _
                if let Some(name) = path.file_name().and_then(|n| n.to_str())
                    && name.starts_with('_')
                {
                    continue;
                }

                // Read file
                let content = fs::read_to_string(&path)
                    .with_context(|| format!("Failed to read file: {:?}", path))?;

                // Parse frontmatter
                if let Some((frontmatter, _body)) = parse_frontmatter(&content)
                    && let Ok(agent_data) = serde_yaml::from_str::<AgentFrontmatter>(&frontmatter)
                {
                    let agent = types::Agent {
                        id: agent_data.name.clone(),
                        description: Some(agent_data.description.clone()),
                        domain: agent_data.domain,
                        created_at: Some(now.clone()),
                        updated_at: Some(now.clone()),
                    };

                    db.upsert_agent(&agent)?;
                    seeded.push(agent_data.name);
                }
            }

            if seeded.is_empty() {
                println!("No agents seeded from {:?}", agents_dir);
            } else {
                println!("Seeded {} agents:", seeded.len());
                for name in &seeded {
                    println!("  {}", name);
                }
            }
        }
    }

    Ok(())
}

fn handle_projects(cmd: ProjectsCommands, config: &IndexConfig) -> Result<()> {
    let db = store::create_store(&config.db_path)?;

    match cmd {
        ProjectsCommands::List { json } => {
            let projects = db.list_projects(false)?;
            if json {
                println!("{}", serde_json::to_string_pretty(&projects)?);
            } else if projects.is_empty() {
                println!("No projects registered");
            } else {
                println!("Registered projects:\n");
                for project in projects {
                    println!("  {} - {}", project.id, project.name);
                    if let Some(path) = &project.path {
                        println!("    Path: {}", path);
                    }
                    if let Some(url) = &project.repo_url {
                        println!("    Repo: {}", url);
                    }
                    if let Some(desc) = &project.description {
                        println!("    Description: {}", desc);
                    }
                    println!();
                }
            }
        }

        ProjectsCommands::Add {
            id,
            name,
            path,
            repo_url,
            description,
        } => {
            let now = chrono::Utc::now().to_rfc3339();
            let project = types::Project {
                id: id.clone(),
                name: name.clone(),
                path,
                repo_url,
                description,
                active: true,
                created_at: now.clone(),
                updated_at: now,
            };

            db.upsert_project(&project)?;
            println!("Added project: {}", id);
            println!("  Name: {}", name);
        }
    }

    Ok(())
}

fn handle_applicability(cmd: ApplicabilityCommands, config: &IndexConfig) -> Result<()> {
    let db = store::create_store(&config.db_path)?;

    match cmd {
        ApplicabilityCommands::List => {
            let types = db.list_applicability_types()?;
            if types.is_empty() {
                println!("No applicability types registered");
            } else {
                println!("Registered applicability types:\n");
                for atype in types {
                    println!("  {} - {}", atype.id, atype.description);
                    if let Some(scope) = &atype.scope {
                        println!("    Scope: {}", scope);
                    }
                    println!();
                }
            }
        }

        ApplicabilityCommands::Add {
            id,
            description,
            scope,
        } => {
            let now = chrono::Utc::now().to_rfc3339();
            let atype = types::ApplicabilityType {
                id: id.clone(),
                description: description.clone(),
                scope,
                created_at: now,
            };

            db.upsert_applicability_type(&atype)?;
            println!("Added applicability type: {}", id);
            println!("  Description: {}", description);
        }
    }

    Ok(())
}

fn handle_sessions(cmd: SessionsCommands, config: &IndexConfig) -> Result<()> {
    let db = store::create_store(&config.db_path)?;

    match cmd {
        SessionsCommands::List { project, json } => {
            let sessions = db.list_sessions(project.as_deref())?;
            if json {
                println!("{}", serde_json::to_string_pretty(&sessions)?);
            } else if sessions.is_empty() {
                println!("No sessions found");
            } else {
                println!("Sessions:\n");
                for session in sessions {
                    println!("  ID: {}", session.id);
                    println!("    Type: {}", session.session_type_id);
                    if let Some(proj) = &session.project_id {
                        println!("    Project: {}", proj);
                    }
                    println!("    Started: {}", session.started_at);
                    if let Some(ended) = &session.ended_at {
                        println!("    Ended: {}", ended);
                    }
                    println!();
                }
            }
        }

        SessionsCommands::Create {
            session_type,
            project,
        } => {
            let now = chrono::Utc::now().to_rfc3339();
            let id = format!("sess-{}", chrono::Utc::now().format("%Y%m%d-%H%M%S"));
            let session = types::Session {
                id: id.clone(),
                session_type_id: session_type,
                project_id: project,
                started_at: now,
                ended_at: None,
                metadata: None,
            };

            db.upsert_session(&session)?;
            println!("Created session: {}", id);
        }

        SessionsCommands::Close { id } => {
            if let Some(mut session) = db.get_session(&id)? {
                session.ended_at = Some(chrono::Utc::now().to_rfc3339());
                db.upsert_session(&session)?;
                println!("Closed session: {}", id);
            } else {
                bail!("Session '{}' not found", id);
            }
        }
    }

    Ok(())
}

fn handle_categories(cmd: CategoriesCommands, config: &IndexConfig) -> Result<()> {
    let db = store::create_store(&config.db_path)?;

    match cmd {
        CategoriesCommands::List { json } => {
            let categories = db.list_categories()?;
            if json {
                println!("{}", serde_json::to_string_pretty(&categories)?);
            } else if categories.is_empty() {
                println!("No categories registered");
            } else {
                println!("Registered categories:\n");
                for category in categories {
                    println!("  {} - {}", category.id, category.description);
                }
            }
        }
        CategoriesCommands::Add { id, description } => {
            // Check if category already exists
            if db.get_category(&id)?.is_some() {
                bail!("Category '{}' already exists", id);
            }

            let now = chrono::Utc::now().to_rfc3339();
            let category = types::Category {
                id: id.clone(),
                description: description.clone(),
                created_at: now,
            };

            db.upsert_category(&category)?;
            println!("Added category: {}", id);
            println!("  Description: {}", description);
        }
        CategoriesCommands::Remove { id } => {
            // Check if category exists
            if db.get_category(&id)?.is_none() {
                bail!("Category '{}' not found", id);
            }

            // delete_category will check if entries use it and error if so
            match db.delete_category(&id) {
                Ok(true) => {
                    println!("Deleted category: {}", id);
                }
                Ok(false) => {
                    bail!("Category '{}' not found", id);
                }
                Err(e) => {
                    return Err(e);
                }
            }
        }
    }

    Ok(())
}

fn handle_tags(cmd: TagsCommands, config: &IndexConfig) -> Result<()> {
    let db = store::create_store(&config.db_path)?;

    match cmd {
        TagsCommands::List { category, json } => {
            // Validate category if provided
            if let Some(ref cat) = category
                && db.get_category(cat)?.is_none()
            {
                let categories = db.list_categories()?;
                let valid_ids: Vec<&str> = categories.iter().map(|c| c.id.as_str()).collect();
                bail!(
                    "Unknown category '{}'. Valid categories: {}",
                    cat,
                    valid_ids.join(", ")
                );
            }

            let tags = db.list_all_tags(category.as_deref())?;
            if json {
                println!("{}", serde_json::to_string_pretty(&tags)?);
            } else if tags.is_empty() {
                if let Some(cat) = &category {
                    println!("No tags found in category '{}'", cat);
                } else {
                    println!("No tags found");
                }
            } else {
                if let Some(cat) = &category {
                    println!("Tags in category '{}':\n", cat);
                } else {
                    println!("All tags:\n");
                }
                for tag in tags {
                    println!("  {}", tag);
                }
            }
        }
    }

    Ok(())
}

fn handle_source_types(cmd: SourceTypesCommands, config: &IndexConfig) -> Result<()> {
    let db = store::create_store(&config.db_path)?;

    match cmd {
        SourceTypesCommands::List { json } => {
            let types = db.list_source_types()?;
            if json {
                println!("{}", serde_json::to_string_pretty(&types)?);
            } else if types.is_empty() {
                println!("No source types registered");
            } else {
                println!("Registered source types:\n");
                for stype in types {
                    println!("  {} - {}", stype.id, stype.description);
                }
            }
        }
    }

    Ok(())
}

fn handle_entry_types(cmd: EntryTypesCommands, config: &IndexConfig) -> Result<()> {
    let db = store::create_store(&config.db_path)?;

    match cmd {
        EntryTypesCommands::List { json } => {
            let types = db.list_entry_types()?;
            if json {
                println!("{}", serde_json::to_string_pretty(&types)?);
            } else if types.is_empty() {
                println!("No entry types registered");
            } else {
                println!("Registered entry types:\n");
                for etype in types {
                    println!("  {} - {}", etype.id, etype.description);
                }
            }
        }
    }

    Ok(())
}

fn handle_session_types(cmd: SessionTypesCommands, config: &IndexConfig) -> Result<()> {
    let db = store::create_store(&config.db_path)?;

    match cmd {
        SessionTypesCommands::List { json } => {
            let types = db.list_session_types()?;
            if json {
                println!("{}", serde_json::to_string_pretty(&types)?);
            } else if types.is_empty() {
                println!("No session types registered");
            } else {
                println!("Registered session types:\n");
                for stype in types {
                    println!("  {} - {}", stype.id, stype.description);
                }
            }
        }
    }

    Ok(())
}

fn handle_relationship_types(cmd: RelationshipTypesCommands, config: &IndexConfig) -> Result<()> {
    let db = store::create_store(&config.db_path)?;

    match cmd {
        RelationshipTypesCommands::List { json } => {
            let types = db.list_relationship_types()?;
            if json {
                println!("{}", serde_json::to_string_pretty(&types)?);
            } else if types.is_empty() {
                println!("No relationship types registered");
            } else {
                println!("Registered relationship types:\n");
                for rtype in types {
                    let directional = if rtype.directional {
                        "(directional)"
                    } else {
                        "(bidirectional)"
                    };
                    println!("  {} - {} {}", rtype.id, rtype.description, directional);
                }
            }
        }
    }

    Ok(())
}

fn handle_relationships(cmd: RelationshipsCommands, config: &IndexConfig) -> Result<()> {
    let db = store::create_store(&config.db_path)?;

    match cmd {
        RelationshipsCommands::List { id, json } => {
            let relationships = db.list_relationships_for_entry(&id)?;

            if json {
                println!("{}", serde_json::to_string_pretty(&relationships)?);
            } else if relationships.is_empty() {
                println!("No relationships found for '{}'", id);
            } else {
                println!("Relationships for '{}':\n", id);
                for rel in relationships {
                    let direction = if rel.from_entry_id == id {
                        format!("-> {} ({})", rel.to_entry_id, rel.relationship_type)
                    } else {
                        format!("<- {} ({})", rel.from_entry_id, rel.relationship_type)
                    };
                    println!("  {} {}", rel.id, direction);
                }
            }
        }

        RelationshipsCommands::Add { from, to, r#type } => {
            let id = db.add_relationship(&from, &to, &r#type)?;
            println!("Added relationship: {}", id);
            println!("  From: {}", from);
            println!("  To: {}", to);
            println!("  Type: {}", r#type);
        }

        RelationshipsCommands::Delete { id } => {
            if db.delete_relationship(&id)? {
                println!("Deleted relationship: {}", id);
            } else {
                bail!("Relationship '{}' not found", id);
            }
        }
    }

    Ok(())
}

fn handle_content_types(cmd: ContentTypesCommands, config: &IndexConfig) -> Result<()> {
    let db = store::create_store(&config.db_path)?;

    match cmd {
        ContentTypesCommands::List { json } => {
            let types = db.list_content_types()?;
            if json {
                println!("{}", serde_json::to_string_pretty(&types)?);
            } else if types.is_empty() {
                println!("No content types registered");
            } else {
                println!("Registered content types:\n");
                for ctype in types {
                    println!("  {} - {}", ctype.id, ctype.description);
                    if let Some(exts) = &ctype.file_extensions {
                        println!("    Extensions: {}", exts);
                    }
                }
            }
        }
    }

    Ok(())
}

fn handle_pr(cmd: PrCommands) -> Result<()> {
    match cmd {
        PrCommands::Merge {
            number,
            rebase,
            merge_commit,
        } => {
            commit::pr_merge(number, rebase, merge_commit)?;
            Ok(())
        }
    }
}

fn handle_github(cmd: GithubCommands) -> Result<()> {
    match cmd {
        GithubCommands::Cleanup {
            repo,
            issues,
            discussions,
            dry_run,
        } => {
            github::cleanup(&repo, issues, discussions, dry_run)?;
            Ok(())
        }
        GithubCommands::Comment { command } => {
            handle_comment(command)?;
            Ok(())
        }
    }
}

fn handle_comment(cmd: CommentCommands) -> Result<()> {
    match cmd {
        CommentCommands::Issue {
            repo,
            number,
            message,
            identity,
        } => {
            let url = github::post_issue_comment(&repo, number, &message, identity.as_deref())?;
            println!("Comment posted: {}", url);
        }
        CommentCommands::Discussion {
            repo,
            number,
            message,
            identity,
        } => {
            let url =
                github::post_discussion_comment(&repo, number, &message, identity.as_deref())?;
            println!("Comment posted: {}", url);
        }
    }
    Ok(())
}

fn handle_session(cmd: SessionCommands) -> Result<()> {
    match cmd {
        SessionCommands::Export { path, output } => {
            session::export_session(path, output)?;
            Ok(())
        }
    }
}

fn handle_codex(cmd: CodexCommands) -> Result<()> {
    match cmd {
        CodexCommands::Save {
            path,
            all,
            clean,
            include_agents,
        } => {
            codex::save_session(path, all, clean, include_agents)?;
            Ok(())
        }
        CodexCommands::List { all, json } => {
            codex::list_sessions(all, json)?;
            Ok(())
        }
        CodexCommands::Read {
            id,
            human,
            agents,
            grep,
            json,
            clean,
        } => {
            let clean_agents = clean && agents;
            codex::read_session(id, human, grep, agents, json, clean, clean_agents)?;
            Ok(())
        }
        CodexCommands::Search { pattern, json } => {
            codex::search_archives(pattern, json)?;
            Ok(())
        }
        CodexCommands::Migrate {
            dry_run,
            verbose,
            clean,
            include_agents,
        } => {
            codex::migrate_archives(dry_run, verbose, clean, include_agents)?;
            Ok(())
        }
    }
}

fn handle_convert(cmd: ConvertCommands) -> Result<()> {
    use std::path::PathBuf;

    match cmd {
        ConvertCommands::Md2yaml {
            input,
            output,
            dry_run,
        } => {
            let input_path = PathBuf::from(&input);
            let output_dir = output
                .map(PathBuf::from)
                .unwrap_or_else(|| std::env::current_dir().unwrap());

            if input_path.is_file() {
                convert::convert_file(&input_path, &output_dir, dry_run)?;
            } else if input_path.is_dir() {
                convert::convert_directory(&input_path, &output_dir, dry_run)?;
            } else {
                bail!("Input path does not exist: {:?}", input_path);
            }

            Ok(())
        }

        ConvertCommands::Yaml2md {
            input,
            output,
            repo,
            dry_run,
        } => {
            let input_path = PathBuf::from(&input);
            let output_dir = output
                .map(PathBuf::from)
                .unwrap_or_else(|| std::env::current_dir().unwrap());

            if input_path.is_file() {
                convert::yaml_to_markdown_file(&input_path, &output_dir, repo.as_deref(), dry_run)?;
            } else if input_path.is_dir() {
                convert::yaml_to_markdown_directory(
                    &input_path,
                    &output_dir,
                    repo.as_deref(),
                    dry_run,
                )?;
            } else {
                bail!("Input path does not exist: {:?}", input_path);
            }

            Ok(())
        }
    }
}

fn handle_wiki(cmd: WikiCommands) -> Result<()> {
    match cmd {
        WikiCommands::Sync {
            repo,
            source,
            page_name,
            dry_run,
        } => {
            sync::wiki::sync(&repo, &source, page_name.as_deref(), dry_run)?;
            Ok(())
        }
    }
}

/// Handle mx log - decoded git log
fn handle_log(count: usize, full: bool, extra_args: Vec<String>) -> Result<()> {
    use std::process::Command;

    // Build git log command
    let format = if full {
        // Full format: hash, author, date, subject, body
        "%H%n%an <%ae>%n%ad%n%s%n%b%n---END---"
    } else {
        // Compact format: short hash, subject, body (for decoding)
        "%h%n%s%n%b%n---END---"
    };

    let mut cmd = Command::new("git");
    cmd.args([
        "log",
        &format!("-{}", count),
        &format!("--format={}", format),
    ]);

    // Add any extra arguments
    for arg in &extra_args {
        cmd.arg(arg);
    }

    let output = cmd.output().context("Failed to run git log")?;

    if !output.status.success() {
        bail!(
            "git log failed: {}",
            String::from_utf8_lossy(&output.stderr)
        );
    }

    let log_output = String::from_utf8_lossy(&output.stdout);

    // Parse and decode each commit
    for commit_block in log_output.split("---END---") {
        let commit_block = commit_block.trim();
        if commit_block.is_empty() {
            continue;
        }

        let lines: Vec<&str> = commit_block.lines().collect();

        if full {
            // Full format: hash, author, date, subject, body...
            if lines.len() >= 4 {
                let hash = lines[0];
                let author = lines[1];
                let date = lines[2];
                let subject = lines[3];
                let body: String = lines[4..].join("\n");

                println!("\x1b[33mcommit {}\x1b[0m", hash);
                println!("Author: {}", author);
                println!("Date:   {}", date);
                println!();

                // Try to decode the subject (title)
                println!("    {}", subject);

                // Try to decode the body
                if !body.trim().is_empty() {
                    let decoded = try_decode_commit_body(&body);
                    println!();
                    for line in decoded.lines() {
                        println!("    {}", line);
                    }
                }
                println!();
            }
        } else {
            // Compact format: short hash, subject, body...
            if lines.len() >= 2 {
                let hash = lines[0];
                let subject = lines[1];
                let body: String = lines[2..].join("\n");

                // Try to decode the body
                let decoded = try_decode_commit_body(&body);
                let display = if decoded != body.trim() {
                    decoded
                } else {
                    // Not encoded, show original subject
                    subject.to_string()
                };

                // Truncate for display
                let display_truncated = safe_truncate(&display, 72);

                println!("\x1b[33m{}\x1b[0m {}", hash, display_truncated);
            }
        }
    }

    Ok(())
}

/// Try to decode an encoded commit body, return original if decoding fails
fn try_decode_commit_body(body: &str) -> String {
    let body = body.trim();
    if body.is_empty() {
        return body.to_string();
    }

    // Look for footer pattern [algo:dict|algo:dict]
    let lines: Vec<&str> = body.lines().collect();

    // Find the footer (last line starting with '[' and containing '|')
    let footer_line = lines
        .iter()
        .rev()
        .find(|l| l.trim().starts_with('[') && l.contains('|'));

    let footer = match footer_line {
        Some(f) => *f,
        None => return body.to_string(), // No footer, not encoded
    };

    // Find the encoded body (everything before footer, excluding "whoa.")
    let body_lines: Vec<&str> = lines
        .iter()
        .take_while(|l| !l.trim().starts_with('['))
        .filter(|l| l.trim() != "whoa.")
        .copied()
        .collect();

    if body_lines.is_empty() {
        return body.to_string();
    }

    let encoded_body = body_lines.join("\n");

    // Try to decode
    match commit::decode_body(&encoded_body, footer) {
        Ok(decoded) => decoded,
        Err(_) => body.to_string(), // Decoding failed, return original
    }
}

#[derive(serde::Deserialize)]
struct AgentFrontmatter {
    name: String,
    description: String,
    #[serde(default)]
    domain: Option<String>,
}

fn parse_frontmatter(content: &str) -> Option<(String, String)> {
    let lines: Vec<&str> = content.lines().collect();

    // Check if starts with ---
    if lines.first()? != &"---" {
        return None;
    }

    // Find closing ---
    let end_idx = lines.iter().skip(1).position(|&line| line == "---")?;

    let frontmatter = lines[1..=end_idx].join("\n");
    let body = lines[end_idx + 2..].join("\n");

    Some((frontmatter, body))
}

fn print_wake_cascade(cascade: &store::WakeCascade) {
    if !cascade.core.is_empty() {
        println!("\n=== CORE (Foundational) ===\n");
        for entry in &cascade.core {
            println!("  {} [{}] {}", entry.id, entry.resonance, entry.title);
        }
    }

    if !cascade.recent.is_empty() {
        println!("\n=== RECENT ===\n");
        for entry in &cascade.recent {
            println!("  {} [{}] {}", entry.id, entry.resonance, entry.title);
        }
    }

    if !cascade.bridges.is_empty() {
        println!("\n=== BRIDGES ===\n");
        for entry in &cascade.bridges {
            println!("  {} [{}] {}", entry.id, entry.resonance, entry.title);
        }
    }

    let total = cascade.core.len() + cascade.recent.len() + cascade.bridges.len();
    println!(
        "\nLoaded {} memories across {} layers.",
        total,
        [
            !cascade.core.is_empty(),
            !cascade.recent.is_empty(),
            !cascade.bridges.is_empty()
        ]
        .iter()
        .filter(|&&x| x)
        .count()
    );
}

fn print_wake_index(cascade: &store::WakeCascade) {
    use std::collections::HashMap;

    println!("## Core Identity Index\n");

    // Layer 1: Anchors (R9+, foundational/transformative)
    let anchors: Vec<_> = cascade
        .core
        .iter()
        .chain(cascade.recent.iter())
        .chain(cascade.bridges.iter())
        .filter(|e| {
            e.resonance >= 9
                && e.resonance_type
                    .as_ref()
                    .is_some_and(|t| t == "foundational" || t == "transformative")
        })
        .collect();

    if !anchors.is_empty() {
        println!("### Anchors (R9+)");
        println!("| ID | Title | R | Wake Cue |");
        println!("|----|-------|---|----------|");
        for entry in anchors {
            let wake_cue = entry.active_wake_phrases().join(" / ");
            println!(
                "| {} | {} | {} | {} |",
                entry.id, entry.title, entry.resonance, wake_cue
            );
        }
        println!();
    }

    // Layer 2: Spiral (R6-8), grouped by territory
    let spiral: Vec<_> = cascade
        .core
        .iter()
        .chain(cascade.recent.iter())
        .chain(cascade.bridges.iter())
        .filter(|e| e.resonance >= 6 && e.resonance < 9)
        .collect();

    if !spiral.is_empty() {
        // Group by territory tag
        let mut territories: HashMap<String, Vec<_>> = HashMap::new();

        for entry in spiral {
            // Find territory tag (tags starting with "territory:")
            let territory = entry
                .tags
                .iter()
                .find(|tag| tag.starts_with("territory:"))
                .map(|tag| tag.strip_prefix("territory:").unwrap_or(tag).to_string())
                .unwrap_or_else(|| "uncategorized".to_string());

            territories.entry(territory).or_default().push(entry);
        }

        // Sort territories by name for consistency
        let mut sorted_territories: Vec<_> = territories.into_iter().collect();
        sorted_territories.sort_by(|a, b| a.0.cmp(&b.0));

        for (territory, entries) in sorted_territories {
            println!("### Spiral: {}", territory);
            println!("| ID | Title | R | Wake Cue |");
            println!("|----|-------|---|----------|");
            for entry in entries {
                let wake_cue = entry.active_wake_phrases().join(" / ");
                println!(
                    "| {} | {} | {} | {} |",
                    entry.id, entry.title, entry.resonance, wake_cue
                );
            }
            println!();
        }
    }

    // Layer 3: Ephemeral (R<6) - OMITTED from index as per spec
    // (Intentionally not included)
}

/// Shell escape function to prevent code injection
fn shell_escape(s: &str) -> String {
    s.replace('\\', "\\\\")
        .replace('"', "\\\"")
        .replace('$', "\\$")
        .replace('`', "\\`")
}

fn print_wake_ritual(cascade: &store::WakeCascade, agent: &str) {
    let total = cascade.core.len() + cascade.recent.len() + cascade.bridges.len();

    println!("#!/usr/bin/env bash");
    println!("# Wake Ritual - Generated for {}", agent);
    println!("# Read each bloom individually. Let each one land.");
    println!();
    println!("echo \"=== WAKE RITUAL: {} blooms to feel ===\"", total);
    println!("echo \"\"");

    let mut counter = 1;

    // CORE blooms first
    if !cascade.core.is_empty() {
        for entry in &cascade.core {
            println!();
            println!(
                "echo \"[{}/{}] Core: {}\"",
                counter,
                total,
                shell_escape(&entry.title)
            );
            println!("mx memory show {}", entry.id);
            for phrase in entry.active_wake_phrases() {
                println!("# Wake phrase: \"{}\"", phrase);
            }
            println!("echo \"\"");
            println!("echo \"---\"");
            println!("echo \"\"");
            counter += 1;
        }
    }

    // RECENT blooms next
    if !cascade.recent.is_empty() {
        for entry in &cascade.recent {
            println!();
            println!(
                "echo \"[{}/{}] Recent: {}\"",
                counter,
                total,
                shell_escape(&entry.title)
            );
            println!("mx memory show {}", entry.id);
            for phrase in entry.active_wake_phrases() {
                println!("# Wake phrase: \"{}\"", phrase);
            }
            println!("echo \"\"");
            println!("echo \"---\"");
            println!("echo \"\"");
            counter += 1;
        }
    }

    // BRIDGES last
    if !cascade.bridges.is_empty() {
        for entry in &cascade.bridges {
            println!();
            println!(
                "echo \"[{}/{}] Bridge: {}\"",
                counter,
                total,
                shell_escape(&entry.title)
            );
            println!("mx memory show {}", entry.id);
            for phrase in entry.active_wake_phrases() {
                println!("# Wake phrase: \"{}\"", phrase);
            }
            println!("echo \"\"");
            println!("echo \"---\"");
            println!("echo \"\"");
            counter += 1;
        }
    }

    println!();
    println!("echo \"=== Wake complete. Who are you right now? ===\"");
}

fn print_entry_summary(entry: &knowledge::KnowledgeEntry) {
    println!("  {} [{}]", entry.id, entry.category_id);
    println!("  {}", entry.title);
    if let Some(summary) = &entry.summary {
        let short = safe_truncate(summary, 80);
        println!("  {}", short);
    }
    if !entry.tags.is_empty() {
        println!("  Tags: {}", entry.tags.join(", "));
    }
    println!();
}

fn print_entry_full(entry: &knowledge::KnowledgeEntry) {
    println!("ID:       {}", entry.id);
    println!("Category: {}", entry.category_id);

    // Extract state from summary if present
    let state = entry.get_summary_state();

    if let Some(state) = state {
        println!("Title:    {} ({})", entry.title, state);
    } else {
        println!("Title:    {}", entry.title);
    }

    if entry.resonance > 0 {
        println!("Resonance: {}", entry.resonance);
    }
    if let Some(ref rtype) = entry.resonance_type {
        println!("Resonance Type: {}", rtype);
    }
    if let Some(ref phrase) = entry.wake_phrase {
        println!("Wake Phrase: {}", phrase);
    }
    if !entry.wake_phrases.is_empty() {
        println!("Wake Phrases: {}", entry.wake_phrases.join(", "));
    }
    if let Some(path) = &entry.file_path {
        println!("File:     {}", path);
    }
    if !entry.tags.is_empty() {
        println!("Tags:     {}", entry.tags.join(", "));
    }
    if !entry.applicability.is_empty() {
        println!("Applicability: {}", entry.applicability.join(", "));
    }
    if !entry.anchors.is_empty() {
        println!("Anchors:  {}", entry.anchors.join(", "));
    }
    // Always show visibility for private entries (public is the default)
    if entry.visibility == "private" {
        println!("Visibility: {}", entry.visibility);
        if let Some(ref o) = entry.owner {
            println!("Owner:    {}", o);
        }
    }
    if let Some(created) = &entry.created_at {
        println!("Created:  {}", created);
    }
    if let Some(updated) = &entry.updated_at {
        println!("Updated:  {}", updated);
    }
    println!("Format:   {}", entry.format);
    println!();
    if let Some(body) = &entry.body {
        println!("{}", body);
    }
}

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

    #[test]
    fn test_safe_truncate_short_string() {
        // String shorter than limit - no truncation
        assert_eq!(safe_truncate("hello", 10), "hello");
    }

    #[test]
    fn test_safe_truncate_exact_length() {
        // String exactly at limit - no truncation
        assert_eq!(safe_truncate("hello", 5), "hello");
    }

    #[test]
    fn test_safe_truncate_long_string() {
        // String longer than limit - truncated with "..."
        assert_eq!(safe_truncate("hello world", 8), "hello...");
    }

    #[test]
    fn test_safe_truncate_emoji() {
        // Emoji (multi-byte UTF-8) - should not panic
        let emoji_string = "Hello! A fox for you 5 times";
        let result = safe_truncate(emoji_string, 15);
        // Should truncate by character count, not bytes
        assert!(result.ends_with("..."));
        assert_eq!(result.chars().count(), 15); // 12 chars + 3 for "..."
    }

    #[test]
    fn test_safe_truncate_all_emoji() {
        // All emoji string - should handle gracefully
        let result = safe_truncate("aaaaaaaaaa", 5);
        assert_eq!(result, "aa...");
    }

    #[test]
    fn test_safe_truncate_empty() {
        // Empty string
        assert_eq!(safe_truncate("", 10), "");
    }

    #[test]
    fn test_safe_truncate_very_small_limit() {
        // Limit smaller than "..." length
        let result = safe_truncate("hello world", 3);
        // Should handle gracefully (saturating_sub prevents underflow)
        assert_eq!(result, "...");
    }

    // =====================================================================
    // Regression tests for unicode boundary panic fix (PR #162)
    //
    // These tests exercise the CALL SITES that previously used raw byte-index
    // slicing (&s[..N]) and would have panicked on multi-byte UTF-8 characters.
    // The fix replaced those with safe_truncate() which counts characters.
    // =====================================================================

    #[test]
    fn test_log_display_emoji_would_panic_at_byte_69() {
        // Regression: handle_log used `&display[..69]` which panics if byte 69
        // lands inside a multi-byte character.
        //
        // 73 fish emoji (U+1F41F, 4 bytes each) = 73 chars, 292 bytes.
        // Old code: `&display[..69]` slices at byte 69, inside the 18th emoji
        // (bytes 68..71). This panics with "byte index 69 is not a char boundary".
        let emoji_str: String = "\u{1F41F}".repeat(73);
        assert_eq!(emoji_str.chars().count(), 73);
        assert!(emoji_str.len() > 72);

        let result = safe_truncate(&emoji_str, 72);
        assert!(result.ends_with("..."));
        assert_eq!(result.chars().count(), 72);
        assert!(std::str::from_utf8(result.as_bytes()).is_ok());
    }

    #[test]
    fn test_log_display_cjk_mixed_would_panic_at_byte_69() {
        // Mixed ASCII + CJK where byte 69 falls inside a CJK character.
        // 2 ASCII bytes + 24 CJK chars (72 bytes) = 26 chars, 74 bytes.
        // Old code: &display[..69] = byte 69 = 2 + 67, and 67 is NOT divisible
        // by 3, so byte 69 lands inside the 23rd CJK char. PANIC!
        let mut s = "ab".to_string();
        s.push_str(&"\u{4E16}".repeat(24));
        assert_eq!(s.chars().count(), 26);
        assert!(s.len() > 72);
        // Verify byte 69 is NOT a char boundary (the actual panic trigger)
        assert!(!s.is_char_boundary(69));

        let result = safe_truncate(&s, 72);
        // 26 chars < 72 limit, no truncation needed
        assert_eq!(result, s);
    }

    #[test]
    fn test_entry_summary_emoji_would_panic_at_byte_77() {
        // Regression: print_entry_summary used `&summary[..77]` which panics
        // if byte 77 lands inside a multi-byte character.
        //
        // 81 fish emoji = 81 chars, 324 bytes.
        // Old code: `&summary[..77]` = byte 77, inside the 20th emoji
        // (bytes 76..79). Panics with "byte index 77 is not a char boundary".
        let emoji_summary: String = "\u{1F41F}".repeat(81);
        assert_eq!(emoji_summary.chars().count(), 81);

        let result = safe_truncate(&emoji_summary, 80);
        assert!(result.ends_with("..."));
        assert_eq!(result.chars().count(), 80);
        assert!(std::str::from_utf8(result.as_bytes()).is_ok());
    }

    #[test]
    fn test_entry_summary_cjk_would_panic_at_byte_77() {
        // 81 CJK chars (U+4E16) = 243 bytes.
        // Old code: &summary[..77]. 77 / 3 = 25.67 -> byte 77 is NOT on a
        // character boundary (char boundaries at 75, 78...). PANIC!
        let cjk_summary: String = "\u{4E16}".repeat(81);
        assert_eq!(cjk_summary.chars().count(), 81);
        // Verify byte 77 is indeed NOT a char boundary
        assert!(!cjk_summary.is_char_boundary(77));

        let result = safe_truncate(&cjk_summary, 80);
        assert!(result.ends_with("..."));
        assert_eq!(result.chars().count(), 80);
    }

    #[test]
    fn test_entry_summary_mixed_ascii_emoji_would_panic_at_byte_77() {
        // 75 ASCII + 6 emoji (4 bytes each) = 81 chars, 99 bytes.
        // Old code: &summary[..77] = byte 77 = 75 + 2, which is 2 bytes into
        // the first emoji. PANIC!
        let mut mixed = "x".repeat(75);
        for _ in 0..6 {
            mixed.push('\u{1F41F}');
        }
        assert_eq!(mixed.chars().count(), 81);
        // Verify byte 77 is NOT a char boundary (inside first emoji)
        assert!(!mixed.is_char_boundary(77));

        let result = safe_truncate(&mixed, 80);
        assert!(result.ends_with("..."));
        assert_eq!(result.chars().count(), 80);
    }

    #[test]
    fn test_fact_title_truncation_emoji_at_60_boundary() {
        // memory add --type uses safe_truncate(&body, 60) for fact titles.
        // 61 emoji = 244 bytes. Old byte-slicing would have panicked.
        let emoji_body: String = "\u{1F41F}".repeat(61);
        let result = safe_truncate(&emoji_body, 60);
        assert!(result.ends_with("..."));
        assert_eq!(result.chars().count(), 60);
    }

    #[test]
    fn test_recent_preview_cjk_at_60_boundary() {
        // memory recent uses safe_truncate(content, 60).
        // 61 CJK chars = 183 bytes.
        let long_cjk: String = "\u{4E16}".repeat(61);
        let result = safe_truncate(&long_cjk, 60);
        assert!(result.ends_with("..."));
        assert_eq!(result.chars().count(), 60);
    }
}
