use crate::cli::args::{
    Cli, Command, CommonRouteArgs, ExplainArgs, GetArgs, HookArgs, InitArgs, McpArgs, McpCommand,
    MemoryActionArgs, MemoryArgs, MemoryCommand, MemoryConsolidateArgs, MemoryDedupArgs,
    MemoryFormatValue, MemoryImportArgs, MemoryImportGitArgs, MemoryLintArgs, MemoryListArgs,
    MemoryListViewValue, MemoryPruneArgs, MemoryRecordArgs, MemoryShowArgs, MemoryStatsArgs,
    MemorySyncIndexArgs, MemorySyncVaultArgs, StatusArgs, WakeupArgs,
};
use crate::cli::{hook, mcp_install};
use crate::daemon::{LifecycleReadOptions, read_history, read_record, read_workbench};
use crate::domain::{MemoryLifecycleState, OutputFormat, RouteInput};
use crate::lifecycle_service::{LifecycleAction, LifecycleService};
use crate::lifecycle_store::{
    LedgerEntry, LifecycleStore, ProposeMemoryRequest, RecordMemoryRequest, TransitionMetadata,
    latest_state_entries, lifecycle_root_from_config,
};
use crate::lifecycle_summary;
use crate::memory_gateway::{self, context_request, wakeup_request};
use crate::output;
use crate::vault_writer;
use clap::Parser;
use std::path::{Path, PathBuf};

pub fn run() -> anyhow::Result<()> {
    let cli = Cli::parse();
    match cli.command {
        Command::Get(args) => execute_get(args),
        Command::Explain(args) => execute_explain(args),
        Command::Wakeup(args) => execute_wakeup(args),
        Command::Memory(args) => execute_memory(args),
        Command::Mcp(args) => execute_mcp(args),
        Command::Hook(args) => execute_hook(args),
        Command::Init(args) => execute_init(args),
        Command::Status(args) => execute_status(args),
        #[cfg(feature = "embedding")]
        Command::Embedding(args) => execute_embedding(args),
        Command::Knowledge(args) => execute_knowledge(args),
    }
}

fn execute_hook(args: HookArgs) -> anyhow::Result<()> {
    hook::execute(args)
}

fn execute_mcp(args: McpArgs) -> anyhow::Result<()> {
    match args.command {
        McpCommand::Install(a) => mcp_install::execute_install(a),
        McpCommand::Update(a) => mcp_install::execute_update(a),
        McpCommand::Uninstall(a) => mcp_install::execute_uninstall(a),
        McpCommand::Doctor(a) => mcp_install::execute_doctor(a),
    }
}

fn execute_get(args: GetArgs) -> anyhow::Result<()> {
    let config_path = args.common.config.clone();
    let config = memory_gateway::load_config(&config_path)?;
    let requested_format = args.format.map(Into::into);
    let format = requested_format.unwrap_or_else(|| app_format(&config));
    let input = to_route_input(args.common, format);
    let response = memory_gateway::execute(&config_path, context_request(input), None)?;
    println!(
        "{}",
        output::render(&response.bundle, config.output.max_chars, format)
    );
    Ok(())
}

fn execute_explain(args: ExplainArgs) -> anyhow::Result<()> {
    let config_path = args.common.config.clone();
    let input = to_route_input(args.common, OutputFormat::Markdown);
    let response = memory_gateway::execute(&config_path, context_request(input), None)?;
    println!("{}", output::explain(&response.bundle));
    Ok(())
}

fn execute_wakeup(args: WakeupArgs) -> anyhow::Result<()> {
    let config_path = args.common.config.clone();
    let format = args.format.map(Into::into).unwrap_or(OutputFormat::Json);
    let input = to_route_input(args.common, format);
    let response = memory_gateway::execute(
        &config_path,
        wakeup_request(input, args.profile.into()),
        None,
    )?;
    println!(
        "{}",
        output::wakeup::render(response.wakeup_packet().unwrap(), format)
    );
    Ok(())
}

fn execute_memory(args: MemoryArgs) -> anyhow::Result<()> {
    match args.command {
        MemoryCommand::List(args) => execute_memory_list(args),
        MemoryCommand::Show(args) => execute_memory_show(args),
        MemoryCommand::History(args) => execute_memory_history(args),
        MemoryCommand::RecordManual(args) => execute_memory_record_manual(args),
        MemoryCommand::Propose(args) => execute_memory_propose(args),
        MemoryCommand::Accept(args) => execute_memory_action(args, LifecycleAction::Accept),
        MemoryCommand::Promote(args) => {
            execute_memory_action(args, LifecycleAction::PromoteToCanonical)
        }
        MemoryCommand::Archive(args) => execute_memory_action(args, LifecycleAction::Archive),
        MemoryCommand::Import(args) => execute_memory_import(args),
        MemoryCommand::ImportGit(args) => execute_memory_import_git(args),
        MemoryCommand::SyncVault(args) => execute_memory_sync_vault(args),
        MemoryCommand::Dedup(args) => execute_memory_dedup(args),
        MemoryCommand::Consolidate(args) => execute_memory_consolidate(args),
        MemoryCommand::Prune(args) => execute_memory_prune(args),
        MemoryCommand::Lint(args) => execute_memory_lint(args),
        MemoryCommand::SyncIndex(args) => execute_memory_sync_index(args),
        MemoryCommand::Stats(args) => execute_memory_stats(args),
    }
}

fn execute_memory_list(args: MemoryListArgs) -> anyhow::Result<()> {
    let snapshot = read_workbench(
        args.config.as_path(),
        &lifecycle_read_options(args.daemon_bin.as_deref()),
    )?;
    let entries = match args.view {
        MemoryListViewValue::PendingReview => snapshot.pending_review,
        MemoryListViewValue::WakeupReady => snapshot.wakeup_ready,
    };
    let heading = match args.view {
        MemoryListViewValue::PendingReview => "Pending review",
        MemoryListViewValue::WakeupReady => "Wakeup-ready",
    };

    match args.format.unwrap_or(MemoryFormatValue::Markdown) {
        MemoryFormatValue::Markdown => println!("{}", render_lifecycle_list(heading, &entries)),
        MemoryFormatValue::Json => println!("{}", serde_json::to_string_pretty(&entries)?),
    }
    Ok(())
}

fn execute_memory_show(args: MemoryShowArgs) -> anyhow::Result<()> {
    let entry = read_record(
        args.config.as_path(),
        &args.record_id,
        &lifecycle_read_options(args.daemon_bin.as_deref()),
    )?
    .ok_or_else(|| anyhow::anyhow!("memory record not found: {}", args.record_id))?;

    match args.format.unwrap_or(MemoryFormatValue::Markdown) {
        MemoryFormatValue::Markdown => println!("{}", render_lifecycle_detail(&entry)),
        MemoryFormatValue::Json => println!("{}", serde_json::to_string_pretty(&entry)?),
    }
    Ok(())
}

fn execute_memory_history(args: MemoryShowArgs) -> anyhow::Result<()> {
    let history = read_history(
        args.config.as_path(),
        &args.record_id,
        &lifecycle_read_options(args.daemon_bin.as_deref()),
    )?;

    // Every record has at least one create event (record_manual /
    // propose_ai), so an empty history list is equivalent to "record
    // doesn't exist". Aligning behavior with `memory show` and the
    // MCP `memory_history` tool keeps the three surfaces consistent.
    if history.is_empty() {
        anyhow::bail!("memory record not found: {}", args.record_id);
    }

    match args.format.unwrap_or(MemoryFormatValue::Markdown) {
        MemoryFormatValue::Markdown => {
            println!("{}", render_lifecycle_history(&args.record_id, &history))
        }
        MemoryFormatValue::Json => println!("{}", serde_json::to_string_pretty(&history)?),
    }
    Ok(())
}

fn lifecycle_read_options(daemon_bin: Option<&std::path::Path>) -> LifecycleReadOptions {
    daemon_bin
        .map(LifecycleReadOptions::with_daemon)
        .unwrap_or_default()
}

fn execute_memory_record_manual(args: MemoryRecordArgs) -> anyhow::Result<()> {
    let service = LifecycleService::new();
    let config_path = args.config.clone();
    let result = service.record_manual(config_path.as_path(), to_record_request(args))?;
    crate::vault_writer::writeback_from_config(config_path.as_path(), &result.entry);
    try_embedding_auto_append(&config_path, &result.entry);
    println!("{}", render_create_result("record_manual", &result.entry));
    Ok(())
}

fn execute_memory_propose(args: MemoryRecordArgs) -> anyhow::Result<()> {
    let service = LifecycleService::new();
    let config_path = args.config.clone();
    let result = service.propose_ai(config_path.as_path(), to_propose_request(args))?;
    crate::vault_writer::writeback_from_config(config_path.as_path(), &result.entry);
    println!("{}", render_create_result("propose", &result.entry));
    Ok(())
}

fn execute_memory_action(args: MemoryActionArgs, action: LifecycleAction) -> anyhow::Result<()> {
    let service = LifecycleService::new();
    let result = service.apply_action_with_metadata(
        args.config.as_path(),
        &args.record_id,
        action,
        transition_metadata(
            args.actor.clone(),
            args.reason.clone(),
            args.evidence_refs.clone(),
        ),
    )?;
    crate::vault_writer::writeback_from_config(args.config.as_path(), &result.entry);
    try_embedding_auto_append(&args.config, &result.entry);
    println!("{}", render_action_result(action, &result.entry));
    Ok(())
}

fn try_embedding_auto_append(config_path: &Path, entry: &LedgerEntry) {
    #[cfg(feature = "embedding")]
    {
        if !matches!(
            entry.record.state,
            MemoryLifecycleState::Accepted | MemoryLifecycleState::Canonical
        ) {
            return;
        }
        if let Ok(config) = crate::config::load_from_path(config_path) {
            crate::engine::embedding::try_append_record(
                &config.embedding,
                &entry.record_id,
                &entry.record,
            );
        }
    }
    #[cfg(not(feature = "embedding"))]
    {
        let _ = (config_path, entry);
    }
}

fn app_format(config: &crate::config::AppConfig) -> OutputFormat {
    crate::app::resolve_format(config, None)
}

fn to_route_input(args: CommonRouteArgs, format: OutputFormat) -> RouteInput {
    RouteInput {
        task: args.task,
        cwd: args.cwd,
        files: args.files,
        target: args.target.into(),
        format,
    }
}

fn render_lifecycle_list(title: &str, entries: &[LedgerEntry]) -> String {
    lifecycle_summary::render_queue_text(title, entries, true, true)
}

fn render_lifecycle_detail(entry: &LedgerEntry) -> String {
    lifecycle_summary::render_record_text(entry, true, true)
}

fn render_action_result(action: LifecycleAction, entry: &LedgerEntry) -> String {
    lifecycle_summary::render_action_text(action, entry)
}

fn render_lifecycle_history(record_id: &str, entries: &[LedgerEntry]) -> String {
    lifecycle_summary::render_history_text(record_id, entries, true)
}

fn render_create_result(kind: &str, entry: &LedgerEntry) -> String {
    lifecycle_summary::render_create_text(kind, entry)
}

fn to_record_request(args: MemoryRecordArgs) -> RecordMemoryRequest {
    RecordMemoryRequest {
        title: args.title,
        summary: args.summary,
        memory_type: args.memory_type,
        scope: args.scope.into(),
        source_ref: args.source_ref,
        project_id: args.project_id,
        user_id: args.user_id,
        sensitivity: args.sensitivity,
        metadata: transition_metadata(args.actor, args.reason, args.evidence_refs),
        entities: Vec::new(),
        tags: Vec::new(),
        triggers: Vec::new(),
        related_files: Vec::new(),
        related_records: Vec::new(),
        supersedes: None,
        applies_to: Vec::new(),
        valid_until: None,
    }
}

fn to_propose_request(args: MemoryRecordArgs) -> ProposeMemoryRequest {
    ProposeMemoryRequest {
        title: args.title,
        summary: args.summary,
        memory_type: args.memory_type,
        scope: args.scope.into(),
        source_ref: args.source_ref,
        project_id: args.project_id,
        user_id: args.user_id,
        sensitivity: args.sensitivity,
        metadata: transition_metadata(args.actor, args.reason, args.evidence_refs),
        entities: Vec::new(),
        tags: Vec::new(),
        triggers: Vec::new(),
        related_files: Vec::new(),
        related_records: Vec::new(),
        supersedes: None,
        applies_to: Vec::new(),
        valid_until: None,
    }
}

fn transition_metadata(
    actor: Option<String>,
    reason: Option<String>,
    evidence_refs: Vec<String>,
) -> TransitionMetadata {
    TransitionMetadata {
        actor,
        reason,
        evidence_refs,
    }
}

#[allow(dead_code)]
fn _config_path(path: &std::path::Path) -> &std::path::Path {
    path
}

fn execute_memory_import(args: MemoryImportArgs) -> anyhow::Result<()> {
    use crate::memory_importer::{ImportProvider, import_session};

    let provider = ImportProvider::parse(args.provider.as_str())?;
    let response = import_session(
        args.config.as_path(),
        provider,
        &args.session_id,
        args.apply,
        Some(args.actor.clone()),
    )?;

    match args.format {
        MemoryFormatValue::Json => println!("{}", serde_json::to_string_pretty(&response)?),
        MemoryFormatValue::Markdown => {
            println!("# Import preview · {}\n", response.session_ref);
            println!("- total messages scanned: {}", response.total_messages);
            println!("- candidates: {}", response.candidate_count);
            println!("- apply: {}", response.applied);
            if response.applied {
                println!(
                    "- applied record ids: {}",
                    response.applied_record_ids.join(", ")
                );
            }
            println!();
            for (idx, c) in response.candidates.iter().enumerate() {
                println!("## {}. [{}] {}", idx + 1, c.memory_type, c.title);
                println!("- scope: {:?}", c.scope);
                println!("- evidence: {}", c.evidence_refs.join(", "));
                println!();
                println!("{}", c.summary);
                println!();
            }
        }
    }

    if response.applied {
        eprintln!(
            "applied {} AI proposals to ledger",
            response.applied_record_ids.len()
        );
    }

    Ok(())
}

#[derive(Debug, Default)]
struct VaultSyncStats {
    created: usize,
    updated_all: usize,
    updated_preserve_body: usize,
    unchanged: usize,
    archived: usize,
    would_create: usize,
    would_update: usize,
    would_archive: usize,
    skipped_missing: usize,
    skipped_draft_or_candidate: usize,
    errors: Vec<(String, String)>,
}

impl VaultSyncStats {
    fn record_write_status(&mut self, status: crate::vault_writer::WriteStatus) {
        use crate::vault_writer::WriteStatus;
        match status {
            WriteStatus::Created => self.created += 1,
            WriteStatus::UpdatedAll => self.updated_all += 1,
            WriteStatus::UpdatedPreserveBody => self.updated_preserve_body += 1,
            WriteStatus::Unchanged => self.unchanged += 1,
        }
    }
}

fn execute_memory_import_git(args: MemoryImportGitArgs) -> anyhow::Result<()> {
    let repo_path = args
        .repo
        .unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
    let report = crate::git_importer::import_git_activity(
        &args.config,
        &repo_path,
        args.limit,
        args.dry_run,
    )?;

    println!(
        "# Git Import{}",
        if args.dry_run { " (dry-run)" } else { "" }
    );
    println!();
    println!("- commits scanned: {}", report.commits_scanned);
    println!("- candidates found: {}", report.candidates_found);
    println!("- persisted: {}", report.candidates_persisted.len());
    println!(
        "- duplicates dropped: {}",
        report.candidates_duplicate_dropped
    );
    if !report.candidates_persisted.is_empty() {
        println!();
        for id in &report.candidates_persisted {
            println!("  - `{id}`");
        }
    }
    Ok(())
}

fn execute_memory_dedup(args: MemoryDedupArgs) -> anyhow::Result<()> {
    let config_dir = args.config.parent().unwrap_or_else(|| Path::new("."));
    let lifecycle_root = lifecycle_root_from_config(config_dir);
    let store = LifecycleStore::new(&lifecycle_root);
    let entries = crate::lifecycle_store::wakeup_ready_entries(&store)?;
    let records: Vec<(String, crate::domain::MemoryRecord)> = entries
        .into_iter()
        .map(|e| (e.record_id, e.record))
        .collect();

    let suggestions = crate::contradiction::find_duplicates(&records, 0.5);

    if suggestions.is_empty() {
        println!("# 去重检查");
        println!();
        println!("未发现相似记忆对（阈值 50%）。");
        return Ok(());
    }

    println!("# 去重建议");
    println!();
    println!("发现 {} 对相似记忆:", suggestions.len());
    println!();
    for s in &suggestions {
        println!("  {}% 相似:", s.similarity);
        println!("    A: {} (`{}`)", s.title_a, s.record_id_a);
        println!("    B: {} (`{}`)", s.title_b, s.record_id_b);
        println!();
    }
    println!("使用 `spool memory archive --record-id <id>` 归档重复项。");
    Ok(())
}

fn execute_memory_consolidate(args: MemoryConsolidateArgs) -> anyhow::Result<()> {
    use crate::knowledge::cluster as consolidation;

    let entries = consolidation::load_entries(args.config.as_path())?;
    let suggestions = consolidation::detect_consolidation_candidates(&entries);

    if suggestions.is_empty() {
        println!("# 知识整合检查");
        println!();
        println!("未发现可合并的碎片记忆聚类（需要 3+ 条相关记录）。");
        return Ok(());
    }

    if !args.apply {
        println!("# 知识整合建议 (dry-run)");
        println!();
        println!("发现 {} 个可合并聚类:", suggestions.len());
        println!();
        for (idx, s) in suggestions.iter().enumerate() {
            println!("## 聚类 {}", idx + 1);
            println!("  建议标题: {}", s.suggested_title);
            println!("  共同 entities: {}", s.shared_entities.join(", "));
            println!("  共同 tags: {}", s.shared_tags.join(", "));
            println!("  包含记录 ({}):", s.cluster_records.len());
            for rid in &s.cluster_records {
                let title = entries
                    .iter()
                    .find(|e| &e.record_id == rid)
                    .map(|e| e.record.title.as_str())
                    .unwrap_or("?");
                println!("    - `{rid}` ({title})");
            }
            println!();
        }
        println!("使用 `--apply` 执行合并。");
        return Ok(());
    }

    println!("# 知识整合执行");
    println!();
    for (idx, s) in suggestions.iter().enumerate() {
        let result = consolidation::apply_consolidation(args.config.as_path(), s, &entries)?;
        println!("## 聚类 {} → 合并为 `{}`", idx + 1, result.merged_record_id);
        println!("  归档了 {} 条碎片记录", result.archived_record_ids.len());
        println!();
    }
    Ok(())
}

fn execute_memory_prune(args: MemoryPruneArgs) -> anyhow::Result<()> {
    use crate::knowledge::cluster as consolidation;

    let entries = consolidation::load_entries(args.config.as_path())?;
    let lifecycle_root = consolidation::resolve_lifecycle_root(args.config.as_path());
    let suggestions = consolidation::detect_prune_candidates(&entries, &lifecycle_root);

    if suggestions.is_empty() {
        println!("# 过时检测");
        println!();
        println!("未发现需要归档的过时记录。");
        return Ok(());
    }

    if !args.apply {
        println!("# 过时记录建议 (dry-run)");
        println!();
        println!("发现 {} 条待归档记录:", suggestions.len());
        println!();
        for s in &suggestions {
            let reason_text = match &s.reason {
                consolidation::PruneReason::Superseded { by } => {
                    format!("被 `{by}` 替代")
                }
                consolidation::PruneReason::Expired { valid_until } => {
                    format!("已过期 (valid_until: {valid_until})")
                }
                consolidation::PruneReason::Stale {
                    days_since_reference,
                } => {
                    format!("长期未引用 ({days_since_reference} 天)")
                }
            };
            println!("  - `{}` ({}) — {}", s.record_id, s.title, reason_text);
        }
        println!();
        println!("使用 `--apply` 执行归档。");
        return Ok(());
    }

    println!("# 过时记录归档");
    println!();
    let result = consolidation::apply_prune(args.config.as_path(), &suggestions)?;
    println!("已归档 {} 条记录:", result.archived_record_ids.len());
    for id in &result.archived_record_ids {
        println!("  - `{id}`");
    }
    Ok(())
}

fn execute_memory_lint(args: MemoryLintArgs) -> anyhow::Result<()> {
    let report = crate::wiki_lint::run_lint_from_config(args.config.as_path())?;
    if args.json {
        println!("{}", serde_json::to_string_pretty(&report)?);
    } else {
        println!("{}", crate::wiki_lint::render_lint_markdown(&report));
    }
    Ok(())
}

fn execute_memory_sync_index(args: MemorySyncIndexArgs) -> anyhow::Result<()> {
    if !args.apply {
        let preview = crate::wiki_index::render_index_from_config(args.config.as_path())?;
        println!("# sync-index (dry-run)\n");
        println!("{preview}");
        println!("Re-run with `--apply` to write the file.");
        return Ok(());
    }
    let result = crate::wiki_index::refresh_index_result(args.config.as_path())?;
    println!("# sync-index");
    println!();
    println!("- path: {}", result.path.display());
    println!("- status: {:?}", result.status);
    println!("- user_entries: {}", result.user_entries);
    println!("- project_entries: {}", result.project_entries);
    Ok(())
}

fn execute_memory_stats(args: MemoryStatsArgs) -> anyhow::Result<()> {
    let config_dir = args
        .config
        .parent()
        .map(Path::to_path_buf)
        .unwrap_or_else(|| PathBuf::from("."));
    let lifecycle_root = lifecycle_root_from_config(config_dir.as_path());
    let store = LifecycleStore::new(lifecycle_root.as_path());
    let entries = latest_state_entries(&store)?;

    let mut by_state: std::collections::HashMap<&str, usize> = std::collections::HashMap::new();
    let mut by_type: std::collections::HashMap<&str, usize> = std::collections::HashMap::new();

    for entry in &entries {
        *by_state
            .entry(crate::lifecycle_format::state_label(entry))
            .or_default() += 1;
        *by_type
            .entry(entry.record.memory_type.as_str())
            .or_default() += 1;
    }

    println!("# memory stats");
    println!();
    println!("total: {}", entries.len());
    println!();
    println!("## by state");
    let mut states: Vec<_> = by_state.iter().collect();
    states.sort_by(|a, b| b.1.cmp(a.1));
    for (state, count) in states {
        println!("  {state}: {count}");
    }
    println!();
    println!("## by type");
    let mut types: Vec<_> = by_type.iter().collect();
    types.sort_by(|a, b| b.1.cmp(a.1));
    for (memory_type, count) in types {
        println!("  {memory_type}: {count}");
    }
    Ok(())
}

fn execute_memory_sync_vault(args: MemorySyncVaultArgs) -> anyhow::Result<()> {
    let config = crate::app::load(args.config.as_path())?;
    let vault_root = crate::app::resolve_override_path(&config.vault.root, args.config.as_path())?;
    let config_dir = args
        .config
        .parent()
        .map(Path::to_path_buf)
        .unwrap_or_else(|| PathBuf::from("."));
    let lifecycle_root = lifecycle_root_from_config(config_dir.as_path());
    let store = LifecycleStore::new(lifecycle_root.as_path());
    let entries = latest_state_entries(&store)?;

    if args.enrich {
        return execute_enrich_pass(&entries, vault_root.as_path(), args.dry_run);
    }

    let mut stats = VaultSyncStats::default();
    for entry in &entries {
        match entry.record.state {
            MemoryLifecycleState::Accepted | MemoryLifecycleState::Canonical => {
                if args.dry_run {
                    let path =
                        vault_writer::memory_note_path(vault_root.as_path(), &entry.record_id);
                    if path.exists() {
                        stats.would_update += 1;
                    } else {
                        stats.would_create += 1;
                    }
                    continue;
                }
                match vault_writer::write_memory_note(
                    vault_root.as_path(),
                    &entry.record_id,
                    &entry.record,
                ) {
                    Ok(result) => stats.record_write_status(result.status),
                    Err(error) => stats
                        .errors
                        .push((entry.record_id.clone(), error.to_string())),
                }
            }
            MemoryLifecycleState::Archived => {
                if args.dry_run {
                    let path =
                        vault_writer::memory_note_path(vault_root.as_path(), &entry.record_id);
                    if path.exists() {
                        stats.would_archive += 1;
                    } else {
                        stats.skipped_missing += 1;
                    }
                    continue;
                }
                match vault_writer::archive_memory_note(vault_root.as_path(), &entry.record_id) {
                    Ok(Some(result)) => match result.status {
                        crate::vault_writer::WriteStatus::Unchanged => stats.unchanged += 1,
                        _ => stats.archived += 1,
                    },
                    Ok(None) => stats.skipped_missing += 1,
                    Err(error) => stats
                        .errors
                        .push((entry.record_id.clone(), error.to_string())),
                }
            }
            MemoryLifecycleState::Draft | MemoryLifecycleState::Candidate => {
                stats.skipped_draft_or_candidate += 1;
            }
        }
    }

    println!(
        "{}",
        render_vault_sync_summary(&stats, entries.len(), args.dry_run, vault_root.as_path())
    );
    Ok(())
}

fn execute_enrich_pass(
    entries: &[LedgerEntry],
    vault_root: &Path,
    dry_run: bool,
) -> anyhow::Result<()> {
    use crate::enrich;

    let mut enriched_count = 0_usize;
    let mut skipped_count = 0_usize;

    for entry in entries {
        // Only enrich accepted/canonical records
        if !matches!(
            entry.record.state,
            MemoryLifecycleState::Accepted | MemoryLifecycleState::Canonical
        ) {
            continue;
        }

        let patch = enrich::enrich_record(&entry.record);
        if patch.is_empty() {
            skipped_count += 1;
            continue;
        }

        if dry_run {
            println!(
                "would enrich `{}` ({}):",
                entry.record_id, entry.record.title
            );
            if !patch.entities.is_empty() {
                println!("  entities: {}", patch.entities.join(", "));
            }
            if !patch.tags.is_empty() {
                println!("  tags: {}", patch.tags.join(", "));
            }
            if !patch.triggers.is_empty() {
                println!("  triggers: {}", patch.triggers.join(", "));
            }
            println!();
            enriched_count += 1;
            continue;
        }

        // Apply enrichment: update vault note frontmatter directly
        let mut enriched_record = entry.record.clone();
        if enriched_record.entities.is_empty() {
            enriched_record.entities = patch.entities;
        }
        if enriched_record.tags.is_empty() {
            enriched_record.tags = patch.tags;
        }
        if enriched_record.triggers.is_empty() {
            enriched_record.triggers = patch.triggers;
        }

        match vault_writer::write_memory_note(vault_root, &entry.record_id, &enriched_record) {
            Ok(_) => enriched_count += 1,
            Err(error) => {
                eprintln!(
                    "[spool] enrich writeback failed for {}: {error}",
                    entry.record_id
                );
            }
        }
    }

    let mode = if dry_run { " (dry-run)" } else { "" };
    println!("# enrich summary{mode}");
    println!("enriched: {enriched_count}");
    println!("skipped (already has fields): {skipped_count}");
    Ok(())
}

fn render_vault_sync_summary(
    stats: &VaultSyncStats,
    total: usize,
    dry_run: bool,
    vault_root: &Path,
) -> String {
    let mode = if dry_run { " (dry-run)" } else { "" };
    let mut lines = Vec::new();
    lines.push(format!("# vault sync summary{mode}"));
    lines.push(format!("vault_root: {}", vault_root.display()));
    lines.push(format!("ledger_records: {total}"));
    if dry_run {
        lines.push(format!("would_create: {}", stats.would_create));
        lines.push(format!("would_update: {}", stats.would_update));
        lines.push(format!("would_archive: {}", stats.would_archive));
    } else {
        lines.push(format!("created: {}", stats.created));
        lines.push(format!("updated_all: {}", stats.updated_all));
        lines.push(format!(
            "updated_preserve_body: {}",
            stats.updated_preserve_body
        ));
        lines.push(format!("unchanged: {}", stats.unchanged));
        lines.push(format!("archived: {}", stats.archived));
    }
    lines.push(format!(
        "skipped_draft_or_candidate: {}",
        stats.skipped_draft_or_candidate
    ));
    lines.push(format!(
        "skipped_missing_archive_target: {}",
        stats.skipped_missing
    ));
    if !stats.errors.is_empty() {
        lines.push(format!("errors: {}", stats.errors.len()));
        for (record_id, msg) in &stats.errors {
            lines.push(format!("  - {record_id}: {msg}"));
        }
    }
    lines.join("\n")
}

fn execute_init(args: InitArgs) -> anyhow::Result<()> {
    let home = crate::support::home_dir().unwrap_or_else(|| std::path::PathBuf::from("/tmp"));
    let config_dir = home.join(".spool");
    let config_path = config_dir.join("config.toml");

    if config_path.exists() {
        println!("配置文件已存在: {}", config_path.display());
        println!("如需重新初始化，请先删除该文件。");
        return Ok(());
    }

    std::fs::create_dir_all(&config_dir)?;

    let vault = args
        .vault
        .map(|p| p.display().to_string())
        .unwrap_or_default();
    let repo = args
        .repo
        .or_else(|| std::env::current_dir().ok())
        .map(|p| p.display().to_string())
        .unwrap_or_default();
    let project_id = args.project_id.unwrap_or_else(|| {
        std::path::Path::new(&repo)
            .file_name()
            .and_then(|n| n.to_str())
            .unwrap_or("my-project")
            .to_string()
    });

    let config = format!(
        r#"[vault]
root = "{vault}"

[output]
default_format = "prompt"
max_chars = 12000
max_notes = 8
max_lifecycle = 5

[[projects]]
id = "{project_id}"
name = "{project_id}"
repo_paths = ["{repo}"]
note_roots = ["10-Projects", "20-Areas"]
"#
    );

    std::fs::write(&config_path, &config)?;

    println!("# spool 初始化完成");
    println!();
    println!("配置文件: {}", config_path.display());
    println!(
        "知识库路径: {}",
        if vault.is_empty() {
            "(未设置，请编辑 config.toml)"
        } else {
            &vault
        }
    );
    println!("项目: {project_id}");
    println!("仓库: {repo}");
    println!();
    println!("下一步:");
    println!("  1. 编辑 {} 设置 vault.root", config_path.display());
    println!(
        "  2. spool mcp install --client claude --config {}",
        config_path.display()
    );
    println!("  3. 开始新的 AI 会话，记忆将自动注入");

    Ok(())
}

fn execute_status(args: StatusArgs) -> anyhow::Result<()> {
    let home = crate::support::home_dir().unwrap_or_else(|| std::path::PathBuf::from("/tmp"));
    let config_path = args
        .config
        .unwrap_or_else(|| home.join(".spool/config.toml"));

    println!("# spool status");
    println!();

    // Config
    let config_exists = config_path.exists();
    println!(
        "  config: {} {}",
        if config_exists { "✓" } else { "✗" },
        config_path.display()
    );

    // Vault
    if config_exists {
        match crate::app::load(&config_path) {
            Ok(config) => {
                let vault_exists = config.vault.root.exists();
                println!(
                    "  vault:  {} {}",
                    if vault_exists { "✓" } else { "✗" },
                    config.vault.root.display()
                );
            }
            Err(e) => println!("  vault:  ✗ (config parse error: {e})"),
        }
    }

    // Lifecycle
    let lifecycle_root =
        lifecycle_root_from_config(config_path.parent().unwrap_or_else(|| Path::new(".")));
    let store = LifecycleStore::new(&lifecycle_root);
    let entries = latest_state_entries(&store).unwrap_or_default();
    let wakeup_ready = entries
        .iter()
        .filter(|e| {
            matches!(
                e.record.state,
                MemoryLifecycleState::Accepted | MemoryLifecycleState::Canonical
            )
        })
        .count();
    let pending = entries
        .iter()
        .filter(|e| e.record.state == MemoryLifecycleState::Candidate)
        .count();
    println!(
        "  memories: {} active, {} pending review, {} total",
        wakeup_ready,
        pending,
        entries.len()
    );

    // AI tools
    let tools = [
        ("claude", home.join(".claude")),
        ("codex", home.join(".codex")),
        ("cursor", home.join(".cursor")),
        ("opencode", home.join(".opencode")),
    ];
    let mut injected: Vec<&str> = Vec::new();
    for (name, dir) in &tools {
        if dir.is_dir() {
            let check_file = match *name {
                "claude" => dir.join("settings.json"),
                _ => dir.join("config.json"),
            };
            if std::fs::read_to_string(&check_file)
                .map(|c| c.contains("spool"))
                .unwrap_or(false)
            {
                injected.push(name);
            }
        }
    }
    if injected.is_empty() {
        println!("  clients: (none injected)");
    } else {
        println!("  clients: {}", injected.join(", "));
    }

    // Rules
    let rules = crate::rules::load(&lifecycle_root);
    let rule_count = rules.extraction.len() + rules.suppress.len();
    if rule_count > 0 {
        println!(
            "  rules:  {} extraction, {} suppress",
            rules.extraction.len(),
            rules.suppress.len()
        );
    }

    println!();
    Ok(())
}

#[cfg(feature = "embedding")]
fn execute_embedding(args: crate::cli::args::EmbeddingArgs) -> anyhow::Result<()> {
    use crate::cli::args::EmbeddingCommand;
    match args.command {
        EmbeddingCommand::Build(a) => execute_embedding_build(a),
        EmbeddingCommand::Status(a) => execute_embedding_status(a),
    }
}

#[cfg(feature = "embedding")]
fn execute_embedding_build(args: crate::cli::args::EmbeddingBuildArgs) -> anyhow::Result<()> {
    use crate::engine::embedding::{EmbeddingIndex, resolve_model_variant};

    let config = crate::config::load_from_path(&args.config)?;
    if !config.embedding.enabled {
        anyhow::bail!("embedding is disabled in config");
    }

    let config_dir = args.config.parent().unwrap_or_else(|| Path::new("."));
    let lifecycle_root = lifecycle_root_from_config(config_dir);
    let store = LifecycleStore::new(&lifecycle_root);
    let entries = latest_state_entries(&store)?;
    let wakeup_eligible: Vec<(String, &crate::domain::MemoryRecord)> = entries
        .iter()
        .filter(|e| {
            matches!(
                e.record.state,
                MemoryLifecycleState::Accepted | MemoryLifecycleState::Canonical
            )
        })
        .map(|e| (e.record_id.clone(), &e.record))
        .collect();

    let model_id = config.embedding.model_id.as_deref();
    let variant = resolve_model_variant(model_id);
    println!(
        "Building embedding index for {} records (model: {:?})...",
        wakeup_eligible.len(),
        model_id.unwrap_or("bge-small-zh-v1.5")
    );

    let model = fastembed::TextEmbedding::try_new(
        fastembed::InitOptions::new(variant).with_show_download_progress(true),
    )?;

    let index = EmbeddingIndex::build_from_records_with_model(&wakeup_eligible, &model)?;
    let index_path = config.embedding.resolved_index_path();
    index.save(&index_path)?;

    println!(
        "Done. {} records indexed ({} dim), saved to {}",
        index.len(),
        index.dim(),
        index_path.display()
    );
    Ok(())
}

#[cfg(feature = "embedding")]
fn execute_embedding_status(args: crate::cli::args::EmbeddingStatusArgs) -> anyhow::Result<()> {
    use crate::engine::embedding::EmbeddingIndex;

    let config = crate::config::load_from_path(&args.config)?;
    let index_path = config.embedding.resolved_index_path();

    println!("Embedding configuration:");
    println!("  enabled:    {}", config.embedding.enabled);
    println!(
        "  model_id:   {}",
        config
            .embedding
            .model_id
            .as_deref()
            .unwrap_or("(default: bge-small-zh-v1.5)")
    );
    println!("  index_path: {}", index_path.display());
    println!("  auto_index: {}", config.embedding.auto_index);
    println!();

    if index_path.exists() {
        match EmbeddingIndex::load(&index_path) {
            Ok(index) => {
                let meta = std::fs::metadata(&index_path)?;
                println!("Index status: BUILT");
                println!("  records:    {}", index.len());
                println!("  dimensions: {}", index.dim());
                println!("  file size:  {:.1} KB", meta.len() as f64 / 1024.0);

                let config_dir = args.config.parent().unwrap_or_else(|| Path::new("."));
                let lifecycle_root = lifecycle_root_from_config(config_dir);
                let store = LifecycleStore::new(&lifecycle_root);
                if let Ok(entries) = latest_state_entries(&store) {
                    let eligible = entries
                        .iter()
                        .filter(|e| {
                            matches!(
                                e.record.state,
                                MemoryLifecycleState::Accepted | MemoryLifecycleState::Canonical
                            )
                        })
                        .count();
                    let coverage = if eligible > 0 {
                        (index.len() as f64 / eligible as f64 * 100.0).min(100.0)
                    } else {
                        100.0
                    };
                    println!(
                        "  coverage:   {}/{} ({:.0}%)",
                        index.len(),
                        eligible,
                        coverage
                    );
                    if coverage < 90.0 {
                        println!();
                        println!(
                            "  Hint: coverage below 90%. Run `spool embedding build` to rebuild."
                        );
                    }
                }
            }
            Err(e) => {
                println!("Index status: CORRUPT ({})", e);
            }
        }
    } else {
        println!("Index status: NOT BUILT");
        println!("  Run `spool embedding build --config ...` to create the index.");
    }
    Ok(())
}

fn execute_knowledge(args: crate::cli::args::KnowledgeArgs) -> anyhow::Result<()> {
    use crate::cli::args::KnowledgeCommand;
    match args.command {
        KnowledgeCommand::Distill(a) => execute_knowledge_distill(a),
    }
}

fn execute_knowledge_distill(args: crate::cli::args::KnowledgeDistillArgs) -> anyhow::Result<()> {
    let drafts = crate::knowledge::detect_knowledge_clusters(&args.config)?;

    if drafts.is_empty() {
        println!("No knowledge clusters detected (need 3+ related fragments).");
        return Ok(());
    }

    println!("# Knowledge distillation\n");
    println!("Detected {} knowledge page draft(s):\n", drafts.len());

    for (i, draft) in drafts.iter().enumerate() {
        println!("## Draft {} — {}\n", i + 1, draft.title);
        println!("- domain: {}", draft.domain);
        println!("- tags: {}", draft.tags.join(", "));
        println!("- sources: {} fragments", draft.source_record_ids.len());
        if !draft.related_notes.is_empty() {
            println!("- related: {}", draft.related_notes.join(", "));
        }
        println!("\n### Preview:\n");
        for line in draft.summary.lines().take(20) {
            println!("  {}", line);
        }
        if draft.summary.lines().count() > 20 {
            println!("  ...(truncated)");
        }
        println!();
    }

    if args.apply {
        let ids = crate::knowledge::apply_distill(&args.config, &drafts, &args.actor)?;
        println!("Created {} knowledge page candidate(s):", ids.len());
        for id in &ids {
            println!("  - {}", id);
        }
        println!("\nUse `spool memory accept --record-id <id>` to promote to accepted.");
    } else if !args.dry_run {
        println!("Add --apply to create knowledge page candidates in the ledger.");
    }

    Ok(())
}
