use std::collections::HashMap;
use std::path::Path;
use std::process::ExitCode;

use anyhow::{anyhow, bail, Result};
use serde::Serialize;
use time::format_description::well_known::Rfc3339;
use time::{Duration, OffsetDateTime};

use crate::memory::entries::StructuredMemoryEntry;
use crate::memory::file::{remove_entry_blocks, render_entry_block, rewrite_entry_blocks};
use crate::memory::{governance, queue};
use crate::output::CommandReport;
use crate::paths::state::StateLayout;
use crate::profile;
use crate::repo::marker as repo_marker;
use crate::repo::registry as repo_registry;
use crate::state::protected_write::ExclusiveWriteOptions;
use crate::state::runtime::{
    self as runtime_state, LoadedRuntimeMemorySurface, RuntimeMemoryEntry,
};
use crate::state::session;
use crate::timestamps;

const PROMPT_BYTE_PRESSURE_THRESHOLD: usize = 2048;

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CompactScope {
    Profile,
    Repo,
    Branch,
    Clone,
}

impl CompactScope {
    fn label(self) -> &'static str {
        match self {
            Self::Profile => "profile",
            Self::Repo => "project",
            Self::Branch => "work_stream",
            Self::Clone => "workspace",
        }
    }

    fn surface_scope(self) -> &'static str {
        match self {
            Self::Profile => "profile_memory",
            Self::Repo => "project_memory",
            Self::Branch => "work_stream_memory",
            Self::Clone => "workspace_memory",
        }
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DecayClass {
    Permanent,
    Stable,
    Active,
}

impl DecayClass {
    pub fn as_str(self) -> &'static str {
        match self {
            Self::Permanent => "permanent",
            Self::Stable => "stable",
            Self::Active => "active",
        }
    }

    fn durability_rank(self) -> u8 {
        match self {
            Self::Permanent => 3,
            Self::Stable => 2,
            Self::Active => 1,
        }
    }

    fn review_floor_days(self) -> i64 {
        match self {
            Self::Active => 7,
            Self::Stable => 30,
            Self::Permanent => 90,
        }
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CompactReviewKind {
    Expired,
    Superseded,
    PromotionCandidate,
}

impl CompactReviewKind {
    pub fn as_str(self) -> &'static str {
        match self {
            Self::Expired => "expired",
            Self::Superseded => "superseded",
            Self::PromotionCandidate => "promotion-candidate",
        }
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ReviewOutcome {
    Surface,
    Remove,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CompactMode<'a> {
    Duplicate {
        entry_id: &'a str,
        keep_id: &'a str,
    },
    DecayClass {
        entry_id: &'a str,
        target: DecayClass,
    },
    Review {
        kind: CompactReviewKind,
        entry_id: Option<&'a str>,
        outcome: ReviewOutcome,
    },
}

pub struct CompactModeRequest<'a> {
    pub entry_id: Option<&'a str>,
    pub keep_id: Option<&'a str>,
    pub decay_class: Option<DecayClass>,
    pub review: Option<CompactReviewKind>,
    pub remove: bool,
    pub write: bool,
}

pub fn parse_mode(request: CompactModeRequest<'_>) -> Result<CompactMode<'_>> {
    let mut selected_modes = 0;
    if request.keep_id.is_some() {
        selected_modes += 1;
    }
    if request.decay_class.is_some() {
        selected_modes += 1;
    }
    if request.review.is_some() {
        selected_modes += 1;
    }

    if selected_modes == 0 {
        bail!(
            "`ccd memory compact` requires one of `--keep <id>`, `--decay-class <permanent|stable|active>`, or `--review <expired|superseded|promotion-candidate>`"
        );
    }
    if selected_modes > 1 {
        bail!(
            "`ccd memory compact` accepts exactly one of `--keep`, `--decay-class`, or `--review`"
        );
    }
    if request.remove && request.review.is_none() {
        bail!("`--remove` requires `--review <expired|superseded|promotion-candidate>`");
    }
    if request.write && request.review.is_some() && !request.remove {
        bail!("`--write` is only valid with `--remove` when using `--review`");
    }

    match (request.keep_id, request.decay_class, request.review) {
        (Some(keep_id), None, None) => Ok(CompactMode::Duplicate {
            entry_id: request
                .entry_id
                .ok_or_else(|| anyhow!("`--entry <id>` is required with `--keep <id>`"))?,
            keep_id,
        }),
        (None, Some(target), None) => Ok(CompactMode::DecayClass {
            entry_id: request.entry_id.ok_or_else(|| {
                anyhow!("`--entry <id>` is required with `--decay-class <permanent|stable|active>`")
            })?,
            target,
        }),
        (None, None, Some(kind)) => {
            if request.remove && request.entry_id.is_none() {
                bail!("`--entry <id>` is required when previewing or applying `--remove`");
            }
            Ok(CompactMode::Review {
                kind,
                entry_id: request.entry_id,
                outcome: if request.remove {
                    ReviewOutcome::Remove
                } else {
                    ReviewOutcome::Surface
                },
            })
        }
        _ => unreachable!("validated mutually-exclusive compact mode"),
    }
}

#[derive(Serialize)]
pub struct MemoryCompactReport {
    command: &'static str,
    ok: bool,
    path: String,
    profile: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    project_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    locality_id: Option<String>,
    scope: &'static str,
    mode: &'static str,
    memory_file: MemorySurfaceView,
    #[serde(skip_serializing_if = "Option::is_none")]
    selected_entry: Option<StructuredMemoryEntry>,
    #[serde(skip_serializing_if = "Option::is_none")]
    keep_entry: Option<StructuredMemoryEntry>,
    governance: governance::GovernanceDecisionView,
    #[serde(skip_serializing_if = "Option::is_none")]
    proposal: Option<CompactProposalView>,
    #[serde(skip_serializing_if = "Option::is_none")]
    review: Option<CompactReviewView>,
    #[serde(skip_serializing_if = "Option::is_none")]
    write_result: Option<WriteResultView>,
    #[serde(skip_serializing_if = "Option::is_none")]
    staged_write: Option<queue::StagedMemoryOpView>,
    #[serde(skip_serializing_if = "Option::is_none")]
    message: Option<String>,
    warnings: Vec<String>,
}

#[derive(Serialize)]
struct MemorySurfaceView {
    scope: &'static str,
    path: String,
    status: &'static str,
}

#[derive(Serialize)]
struct CompactProposalView {
    action: &'static str,
    match_rule: &'static str,
    selected_id: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    keep_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    target_decay_class: Option<&'static str>,
    diff_preview: String,
    note: &'static str,
}

#[derive(Serialize)]
struct CompactReviewView {
    review_type: &'static str,
    ordering_rule: &'static str,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pressure_signals: Vec<PressureSignalView>,
    candidates: Vec<ReviewCandidateView>,
    note: &'static str,
}

#[derive(Serialize)]
struct PressureSignalView {
    signal: &'static str,
    detail: String,
}

#[derive(Clone, Serialize)]
struct ReviewCandidateView {
    entry: StructuredMemoryEntry,
    eligibility_reason: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    age_basis: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    age_floor_days: Option<i64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    eligible_at: Option<String>,
    content_bytes: usize,
}

#[derive(Serialize)]
struct WriteResultView {
    file_action: &'static str,
    path: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    written_keep_entry: Option<StructuredMemoryEntry>,
    #[serde(skip_serializing_if = "Option::is_none")]
    written_selected_entry: Option<StructuredMemoryEntry>,
    #[serde(skip_serializing_if = "Option::is_none")]
    removed_entry: Option<StructuredMemoryEntry>,
}

#[derive(Clone)]
struct ReviewCandidate {
    entry: StructuredMemoryEntry,
    view: ReviewCandidateView,
    age_seconds: i64,
}

enum ReviewEligibility {
    Eligible {
        eligibility_reason: String,
        age_basis: Option<String>,
        age_floor_days: Option<i64>,
        eligible_at: Option<String>,
        age_seconds: i64,
    },
    Ineligible {
        reason: String,
    },
}

struct BuiltReview {
    view: CompactReviewView,
    candidates: Vec<ReviewCandidate>,
}

impl CommandReport for MemoryCompactReport {
    fn exit_code(&self) -> ExitCode {
        if self.ok {
            ExitCode::SUCCESS
        } else {
            ExitCode::from(1)
        }
    }

    fn render_text(&self) {
        if !self.ok {
            println!(
                "{}",
                self.message
                    .as_deref()
                    .unwrap_or("Queued memory compaction was rejected.")
            );
            return;
        }

        let scope_label = self.scope.replace('_', " ");
        if let Some(review) = &self.review {
            println!(
                "Prepared memory compaction review for {} entr{} in {}.",
                review.candidates.len(),
                if review.candidates.len() == 1 {
                    "y"
                } else {
                    "ies"
                },
                scope_label
            );
            println!(
                "Memory file: {} ({})",
                self.memory_file.path, self.memory_file.status
            );
            println!(
                "Governance: {} ({})",
                self.governance.action.as_str(),
                self.governance.status.as_str()
            );
            println!("Review: {} ({})", review.review_type, review.ordering_rule);
            for signal in &review.pressure_signals {
                println!("Pressure: {} ({})", signal.signal, signal.detail);
            }
            if review.candidates.is_empty() {
                println!("Eligible entries: none");
            } else {
                println!("Eligible entries:");
                for candidate in &review.candidates {
                    println!("- {}: {}", candidate.entry.id, candidate.eligibility_reason);
                }
            }
            for warning in &self.warnings {
                println!("Warning: {warning}");
            }
            println!();
            println!("{}", review.note);
            return;
        }

        let selected_entry = self
            .selected_entry
            .as_ref()
            .expect("proposal reports always include a selected entry");
        match &self.write_result {
            Some(write_result) if write_result.removed_entry.is_some() => println!(
                "Applied memory compaction removal for entry {} in {}.",
                selected_entry.id, self.scope
            ),
            Some(_) => println!(
                "Applied memory compaction for entry {} in {}.",
                selected_entry.id, self.scope
            ),
            None => println!(
                "Prepared memory compaction preview for entry {} in {}.",
                selected_entry.id, self.scope
            ),
        }
        println!(
            "Memory file: {} ({})",
            self.memory_file.path, self.memory_file.status
        );
        if let Some(keep_entry) = &self.keep_entry {
            println!("Keep entry: {}", keep_entry.id);
            println!("Selected duplicate: {}", selected_entry.id);
        } else {
            println!("Selected entry: {}", selected_entry.id);
            if let Some(target_decay_class) = self
                .proposal
                .as_ref()
                .and_then(|proposal| proposal.target_decay_class)
            {
                println!("Target decay class: {target_decay_class}");
            }
        }

        let proposal = self.proposal.as_ref().expect("proposal report");
        println!("Proposal: {} ({})", proposal.action, proposal.note);
        println!(
            "Governance: {} ({})",
            self.governance.action.as_str(),
            self.governance.status.as_str()
        );

        if let Some(write_result) = &self.write_result {
            println!(
                "Write: {} ({})",
                write_result.file_action, write_result.path
            );
        }
        if let Some(staged_write) = &self.staged_write {
            println!("Queued op: {}", staged_write.op_id);
        }

        for warning in &self.warnings {
            println!("Warning: {warning}");
        }

        println!();
        println!("Diff preview:");
        println!("{}", proposal.diff_preview);
        println!();
        println!("{}", proposal.note);
    }
}

pub fn run(
    repo_root: &Path,
    explicit_profile: Option<&str>,
    scope: CompactScope,
    mode: CompactMode<'_>,
    write: bool,
    write_options: ExclusiveWriteOptions,
    op_id_hint: Option<&str>,
) -> Result<MemoryCompactReport> {
    let profile = profile::resolve(explicit_profile)?;
    let layout = StateLayout::resolve(repo_root, profile.clone())?;
    ensure_profile_exists(&layout)?;
    let (locality_id, memory_file) = load_memory_surface(repo_root, &layout, scope)?;

    let staged_request = if should_stage_via_queue(write, scope, &layout)? {
        let request_key = CompactQueueRequestKey {
            command: "memory-compact",
            scope: scope.surface_scope(),
            mode: compact_mode_key(&mode),
        };
        let request_fingerprint = queue::fingerprint(&request_key)?;
        let op_id = queue::stable_op_id("compact", &request_fingerprint, op_id_hint);
        Some(QueuedWriteRequest {
            request_fingerprint,
            op_id,
        })
    } else {
        None
    };

    if !memory_file
        .entries
        .iter()
        .any(|entry| entry.as_structured_entry().is_some())
    {
        bail!(
            "no structured {}-memory entries are available at {}; add a `ccd-memory` block before running `ccd memory compact`",
            scope.label(),
            memory_file.source.path.display()
        );
    }

    match mode {
        CompactMode::Duplicate { entry_id, keep_id } => run_duplicate_flow(
            repo_root,
            &layout,
            scope,
            locality_id,
            &memory_file,
            entry_id,
            keep_id,
            write,
            staged_request.as_ref(),
            &write_options,
        ),
        CompactMode::DecayClass { entry_id, target } => run_decay_class_flow(
            repo_root,
            &layout,
            scope,
            locality_id,
            &memory_file,
            entry_id,
            target,
            write,
            staged_request.as_ref(),
            &write_options,
        ),
        CompactMode::Review {
            kind,
            entry_id,
            outcome,
        } => run_review_flow(
            repo_root,
            &layout,
            scope,
            locality_id,
            &memory_file,
            kind,
            entry_id,
            outcome,
            write,
            staged_request.as_ref(),
            &write_options,
        ),
    }
}

#[derive(Clone, Debug)]
struct QueuedWriteRequest {
    request_fingerprint: String,
    op_id: String,
}

#[derive(Serialize)]
struct CompactQueueRequestKey<'a> {
    command: &'static str,
    scope: &'static str,
    mode: CompactModeKey<'a>,
}

#[derive(Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
enum CompactModeKey<'a> {
    Duplicate {
        entry_id: &'a str,
        keep_id: &'a str,
    },
    DecayClass {
        entry_id: &'a str,
        target: &'static str,
    },
    Review {
        review: &'static str,
        entry_id: Option<&'a str>,
        outcome: &'static str,
    },
}

fn load_memory_surface(
    repo_root: &Path,
    layout: &StateLayout,
    scope: CompactScope,
) -> Result<(Option<String>, LoadedRuntimeMemorySurface)> {
    match scope {
        CompactScope::Profile => Ok((None, runtime_state::load_profile_memory_surface(layout)?)),
        CompactScope::Repo => {
            let marker = repo_marker::load(repo_root)?.ok_or_else(|| {
                anyhow!(
                    "repo is not linked: {} is missing; run `ccd attach --path {}` or `ccd link --path {}` first",
                    repo_root.join(repo_marker::MARKER_FILE).display(),
                    repo_root.display(),
                    repo_root.display()
                )
            })?;
            let locality_id = marker.locality_id;
            ensure_repo_registry_exists(layout, repo_root, &locality_id)?;
            ensure_repo_overlay_exists(layout, &locality_id)?;
            let file = runtime_state::load_locality_memory_surface(layout, &locality_id)?;
            Ok((Some(locality_id), file))
        }
        CompactScope::Branch => {
            let marker = repo_marker::load(repo_root)?.ok_or_else(|| {
                anyhow!(
                    "repo is not linked: {} is missing; run `ccd attach --path {}` or `ccd link --path {}` first",
                    repo_root.join(repo_marker::MARKER_FILE).display(),
                    repo_root.display(),
                    repo_root.display()
                )
            })?;
            let locality_id = marker.locality_id;
            ensure_repo_registry_exists(layout, repo_root, &locality_id)?;
            ensure_repo_overlay_exists(layout, &locality_id)?;
            if runtime_state::resolve_active_branch_memory_path(repo_root, layout, &locality_id)?
                .is_none()
            {
                bail!(
                    "work-stream memory is only available on active non-trunk named branches; switch to a feature branch before using `ccd memory compact --scope work-stream`"
                );
            }
            let file = runtime_state::load_branch_memory_surface(repo_root, layout, &locality_id)?;
            Ok((Some(locality_id), file))
        }
        CompactScope::Clone => {
            let marker = repo_marker::load(repo_root)?.ok_or_else(|| {
                anyhow!(
                    "repo is not linked: {} is missing; run `ccd attach --path {}` or `ccd link --path {}` first",
                    repo_root.join(repo_marker::MARKER_FILE).display(),
                    repo_root.display(),
                    repo_root.display()
                )
            })?;
            let locality_id = marker.locality_id;
            ensure_repo_registry_exists(layout, repo_root, &locality_id)?;
            let file = runtime_state::load_clone_memory_surface(layout)?;
            Ok((Some(locality_id), file))
        }
    }
}

#[allow(clippy::too_many_arguments)]
fn run_duplicate_flow(
    repo_root: &Path,
    layout: &StateLayout,
    scope: CompactScope,
    locality_id: Option<String>,
    memory_file: &LoadedRuntimeMemorySurface,
    entry_id: &str,
    keep_id: &str,
    write: bool,
    staged_request: Option<&QueuedWriteRequest>,
    write_options: &ExclusiveWriteOptions,
) -> Result<MemoryCompactReport> {
    if entry_id == keep_id {
        bail!("`--entry` and `--keep` must refer to different structured memory entries");
    }

    let (selected_runtime_entry, selected_entry) = load_selected_entry(entry_id, memory_file)?;
    let (keep_runtime_entry, keep_entry) = load_selected_entry(keep_id, memory_file)?;

    validate_duplicate_pair(&selected_entry, &keep_entry)?;

    let selected_superseded_at = timestamps::current_utc_rfc3339()?;
    let written_keep_runtime = updated_keep_entry(&keep_runtime_entry, &selected_entry.id)?;
    let written_selected_runtime =
        superseded_selected_entry(&selected_runtime_entry, selected_superseded_at);
    let written_keep_entry = written_keep_runtime
        .as_structured_entry()
        .expect("updated keep entry");
    let written_selected_entry = written_selected_runtime
        .as_structured_entry()
        .expect("superseded selected entry");
    let autonomous_session_id = current_autonomous_session_id(layout)?;
    let mut governance = governance::GovernanceDecisionView::new(
        governance::GovernanceDecisionInit {
            action: governance::GovernanceAction::AutoCompactDuplicate,
            status: governance::GovernanceStatus::Allowed,
            target_scope: scope.surface_scope().to_owned(),
            source_scope: Some(scope.surface_scope().to_owned()),
            entry_type: selected_entry.entry_type.clone(),
            rationale: "exact duplicates may compact automatically when the match is deterministic and reversible".to_owned(),
            evidence_summary: vec![
                "duplicate pair matched on exact type and normalized content".to_owned(),
                "the selected duplicate remains reversible through supersession metadata".to_owned(),
            ],
            pressure_signals: vec![governance::signal(
                "duplicate_match",
                format!(
                    "selected entry `{}` matches keep entry `{}` by normalized content",
                    selected_entry.id, keep_entry.id
                ),
            )],
            duplicate_digest: Some(governance::normalized_duplicate_digest(&selected_entry)),
            rate_limit: None,
        },
    );
    let diff_preview = duplicate_diff_preview(
        &keep_entry,
        &written_keep_entry,
        &selected_entry,
        &written_selected_entry,
    );

    let next_contents = if write {
        Some(rewrite_entry_blocks(
            &memory_file.source.content,
            &HashMap::from([
                (
                    keep_entry.id.clone(),
                    render_entry_block(&written_keep_entry),
                ),
                (
                    selected_entry.id.clone(),
                    render_entry_block(&written_selected_entry),
                ),
            ]),
        )?)
    } else {
        None
    };
    let write_result = write.then(|| WriteResultView {
        file_action: "rewrite_blocks",
        path: memory_file.source.path.display().to_string(),
        written_keep_entry: Some(written_keep_entry.clone()),
        written_selected_entry: Some(written_selected_entry.clone()),
        removed_entry: None,
    });

    let report = MemoryCompactReport {
        command: "memory-compact",
        ok: true,
        path: repo_root.display().to_string(),
        profile: layout.profile().to_string(),
        project_id: locality_id.clone(),
        locality_id: locality_id.clone(),
        scope: scope.label(),
        mode: if write {
            "exact_duplicate_supersede_write"
        } else {
            "exact_duplicate_supersede_preview"
        },
        memory_file: MemorySurfaceView {
            scope: scope.surface_scope(),
            path: memory_file.source.path.display().to_string(),
            status: memory_file.source.status.as_str(),
        },
        selected_entry: Some(selected_entry.clone()),
        keep_entry: Some(keep_entry.clone()),
        governance: governance.clone(),
        proposal: Some(CompactProposalView {
            action: "mark_selected_superseded",
            match_rule: "exact_type_and_normalized_content",
            selected_id: written_selected_entry.id.clone(),
            keep_id: Some(keep_id.to_owned()),
            target_decay_class: None,
            diff_preview,
            note: if write {
                "Applied with explicit approval. The selected duplicate was marked superseded and the kept entry now records the supersession link."
            } else {
                "Preview only. No files were changed. Rerun with `--write` to apply this duplicate compaction."
            },
        }),
        review: None,
        write_result,
        staged_write: None,
        message: None,
        warnings: Vec::new(),
    };

    let mut report = report;
    if write {
        let mutation_id = staged_request
            .map(|request| request.op_id.as_str())
            .unwrap_or("memory-compact-direct");
        governance = governance.with_rollback(
            mutation_id,
            write_options.actor_id.as_deref(),
            write_options
                .session_id
                .as_deref()
                .or(autonomous_session_id.as_deref()),
            vec![keep_entry.id.clone(), selected_entry.id.clone()],
            &memory_file.source.content,
            None,
        );
        report.governance = governance.clone();
    }

    finalize_compact_report(
        repo_root,
        layout,
        report,
        write.then(|| queue::MemoryOpPlan {
            target: queue::MemoryFileMutation {
                scope: scope.surface_scope().to_owned(),
                path: memory_file.source.path.display().to_string(),
                next_contents: next_contents.expect("write checked"),
            },
            source: None,
            refresh_locality_id: locality_id.clone(),
            authored_entry_ids: vec![keep_entry.id.clone(), selected_entry.id.clone()],
            governance: Some(governance),
        }),
        staged_request,
        write_options,
    )
}

#[allow(clippy::too_many_arguments)]
fn run_decay_class_flow(
    repo_root: &Path,
    layout: &StateLayout,
    scope: CompactScope,
    locality_id: Option<String>,
    memory_file: &LoadedRuntimeMemorySurface,
    entry_id: &str,
    target: DecayClass,
    write: bool,
    staged_request: Option<&QueuedWriteRequest>,
    write_options: &ExclusiveWriteOptions,
) -> Result<MemoryCompactReport> {
    let (selected_runtime_entry, selected_entry) = load_selected_entry(entry_id, memory_file)?;
    validate_decay_class_change(&selected_entry, target)?;

    let written_selected_runtime =
        selected_runtime_entry.with_decay_class(Some(target.as_str().to_owned()));
    let written_selected_entry = written_selected_runtime
        .as_structured_entry()
        .expect("decay-class updated entry");
    let autonomous_session_id = current_autonomous_session_id(layout)?;
    let mut governance =
        governance::GovernanceDecisionView::new(governance::GovernanceDecisionInit {
            action: governance::GovernanceAction::StageNarrowing,
            status: governance::GovernanceStatus::Allowed,
            target_scope: scope.surface_scope().to_owned(),
            source_scope: Some(scope.surface_scope().to_owned()),
            entry_type: selected_entry.entry_type.clone(),
            rationale:
                "durability downgrades are governed narrowing actions rather than silent deletion"
                    .to_owned(),
            evidence_summary: vec![
                format!(
                    "requested decay class `{}` narrows or backfills durability",
                    target.as_str()
                ),
                "the original entry remains present and reversible".to_owned(),
            ],
            pressure_signals: Vec::new(),
            duplicate_digest: Some(governance::normalized_duplicate_digest(&selected_entry)),
            rate_limit: None,
        });
    let diff_preview = single_entry_diff_preview(&selected_entry, &written_selected_entry);
    let backfill = selected_entry.decay_class.is_none();

    let next_contents = if write {
        Some(rewrite_entry_blocks(
            &memory_file.source.content,
            &HashMap::from([(
                selected_entry.id.clone(),
                render_entry_block(&written_selected_entry),
            )]),
        )?)
    } else {
        None
    };
    let write_result = write.then(|| WriteResultView {
        file_action: "rewrite_blocks",
        path: memory_file.source.path.display().to_string(),
        written_keep_entry: None,
        written_selected_entry: Some(written_selected_entry.clone()),
        removed_entry: None,
    });

    let report = MemoryCompactReport {
        command: "memory-compact",
        ok: true,
        path: repo_root.display().to_string(),
        profile: layout.profile().to_string(),
        project_id: locality_id.clone(),
        locality_id: locality_id.clone(),
        scope: scope.label(),
        mode: match (backfill, write) {
            (true, false) => "decay_class_backfill_preview",
            (true, true) => "decay_class_backfill_write",
            (false, false) => "decay_class_downgrade_preview",
            (false, true) => "decay_class_downgrade_write",
        },
        memory_file: MemorySurfaceView {
            scope: scope.surface_scope(),
            path: memory_file.source.path.display().to_string(),
            status: memory_file.source.status.as_str(),
        },
        selected_entry: Some(selected_entry.clone()),
        keep_entry: None,
        governance: governance.clone(),
        proposal: Some(CompactProposalView {
            action: "set_decay_class",
            match_rule: if backfill {
                "explicit_decay_class_backfill"
            } else {
                "explicit_lower_decay_class"
            },
            selected_id: written_selected_entry.id.clone(),
            keep_id: None,
            target_decay_class: Some(target.as_str()),
            diff_preview,
            note: if write {
                "Applied with explicit approval. The selected entry now carries the requested lower durability band."
            } else {
                "Preview only. No files were changed. Rerun with `--write` to apply this lifecycle compaction."
            },
        }),
        review: None,
        write_result,
        staged_write: None,
        message: None,
        warnings: Vec::new(),
    };

    let mut report = report;
    if write {
        let mutation_id = staged_request
            .map(|request| request.op_id.as_str())
            .unwrap_or("memory-compact-direct");
        governance = governance.with_rollback(
            mutation_id,
            write_options.actor_id.as_deref(),
            write_options
                .session_id
                .as_deref()
                .or(autonomous_session_id.as_deref()),
            vec![selected_entry.id.clone()],
            &memory_file.source.content,
            None,
        );
        report.governance = governance.clone();
    }

    finalize_compact_report(
        repo_root,
        layout,
        report,
        write.then(|| queue::MemoryOpPlan {
            target: queue::MemoryFileMutation {
                scope: scope.surface_scope().to_owned(),
                path: memory_file.source.path.display().to_string(),
                next_contents: next_contents.expect("write checked"),
            },
            source: None,
            refresh_locality_id: locality_id.clone(),
            authored_entry_ids: vec![selected_entry.id.clone()],
            governance: Some(governance),
        }),
        staged_request,
        write_options,
    )
}

#[allow(clippy::too_many_arguments)]
fn run_review_flow(
    repo_root: &Path,
    layout: &StateLayout,
    scope: CompactScope,
    locality_id: Option<String>,
    memory_file: &LoadedRuntimeMemorySurface,
    kind: CompactReviewKind,
    entry_id: Option<&str>,
    outcome: ReviewOutcome,
    write: bool,
    staged_request: Option<&QueuedWriteRequest>,
    write_options: &ExclusiveWriteOptions,
) -> Result<MemoryCompactReport> {
    let now = OffsetDateTime::now_utc();
    let built_review = build_review(memory_file, kind, now, entry_id)?;
    let governance_pressure_signals = built_review
        .view
        .pressure_signals
        .iter()
        .map(|signal| governance::signal(signal.signal, signal.detail.clone()))
        .collect::<Vec<_>>();

    match outcome {
        ReviewOutcome::Surface => Ok(MemoryCompactReport {
            command: "memory-compact",
            ok: true,
            path: repo_root.display().to_string(),
            profile: layout.profile().to_string(),
            project_id: locality_id.clone(),
            locality_id,
            scope: scope.label(),
            mode: review_surface_mode(kind),
            memory_file: MemorySurfaceView {
                scope: scope.surface_scope(),
                path: memory_file.source.path.display().to_string(),
                status: memory_file.source.status.as_str(),
            },
            selected_entry: None,
            keep_entry: None,
            governance: governance::GovernanceDecisionView::new(
                governance::GovernanceDecisionInit {
                    action: governance::GovernanceAction::StageEvictionReview,
                    status: governance::GovernanceStatus::Allowed,
                    target_scope: scope.surface_scope().to_owned(),
                    source_scope: Some(scope.surface_scope().to_owned()),
                    entry_type: "review".to_owned(),
                    rationale:
                        "broad removal candidates stay review-only until an explicit removal is chosen"
                            .to_owned(),
                    evidence_summary: vec![format!("review kind `{}` was evaluated", kind.as_str())],
                    pressure_signals: governance_pressure_signals,
                    duplicate_digest: None,
                    rate_limit: None,
                },
            ),
            proposal: None,
            review: Some(built_review.view),
            write_result: None,
            staged_write: None,
            message: None,
            warnings: Vec::new(),
        }),
        ReviewOutcome::Remove => {
            let selected_candidate = built_review
                .candidates
                .first()
                .cloned()
                .expect("remove preview requires a selected eligible candidate");
            let diff_preview = removal_diff_preview(&selected_candidate.entry);

            let next_contents = if write {
                Some(remove_entry_blocks(
                    &memory_file.source.content,
                    std::slice::from_ref(&selected_candidate.entry.id),
                )?)
            } else {
                None
            };
            let write_result = write.then(|| WriteResultView {
                file_action: "remove_block",
                path: memory_file.source.path.display().to_string(),
                written_keep_entry: None,
                written_selected_entry: None,
                removed_entry: Some(selected_candidate.entry.clone()),
            });
            let autonomous_session_id = current_autonomous_session_id(layout)?;
            let mut governance = governance::GovernanceDecisionView::new(
                governance::GovernanceDecisionInit {
                    action: governance::GovernanceAction::StageEvictionReview,
                    status: governance::GovernanceStatus::Allowed,
                    target_scope: scope.surface_scope().to_owned(),
                    source_scope: Some(scope.surface_scope().to_owned()),
                    entry_type: selected_candidate.entry.entry_type.clone(),
                    rationale:
                        "reviewed removals stay governed eviction actions rather than silent deletion"
                            .to_owned(),
                    evidence_summary: vec![format!(
                        "review kind `{}` selected entry `{}`",
                        kind.as_str(),
                        selected_candidate.entry.id
                    )],
                    pressure_signals: governance_pressure_signals.clone(),
                    duplicate_digest: Some(governance::normalized_duplicate_digest(
                        &selected_candidate.entry,
                    )),
                    rate_limit: None,
                },
            );

            let report = MemoryCompactReport {
                command: "memory-compact",
                ok: true,
                path: repo_root.display().to_string(),
                profile: layout.profile().to_string(),
                project_id: locality_id.clone(),
                locality_id: locality_id.clone(),
                scope: scope.label(),
                mode: review_remove_mode(kind, write),
                memory_file: MemorySurfaceView {
                    scope: scope.surface_scope(),
                    path: memory_file.source.path.display().to_string(),
                    status: memory_file.source.status.as_str(),
                },
                selected_entry: Some(selected_candidate.entry.clone()),
                keep_entry: None,
                governance: governance.clone(),
                proposal: Some(CompactProposalView {
                    action: "remove_entry",
                    match_rule: review_remove_match_rule(kind),
                    selected_id: selected_candidate.entry.id.clone(),
                    keep_id: None,
                    target_decay_class: None,
                    diff_preview,
                    note: if write {
                        "Applied with explicit approval. The selected stale entry was removed from the authored memory file."
                    } else {
                        "Preview only. No files were changed. Rerun with `--write` to apply this reviewed removal."
                    },
                }),
                review: None,
                write_result,
                staged_write: None,
                message: None,
                warnings: Vec::new(),
            };

            let mut report = report;
            if write {
                let mutation_id = staged_request
                    .map(|request| request.op_id.as_str())
                    .unwrap_or("memory-compact-direct");
                governance = governance.with_rollback(
                    mutation_id,
                    write_options.actor_id.as_deref(),
                    write_options
                        .session_id
                        .as_deref()
                        .or(autonomous_session_id.as_deref()),
                    vec![selected_candidate.entry.id.clone()],
                    &memory_file.source.content,
                    None,
                );
                report.governance = governance.clone();
            }

            finalize_compact_report(
                repo_root,
                layout,
                report,
                write.then(|| queue::MemoryOpPlan {
                    target: queue::MemoryFileMutation {
                        scope: scope.surface_scope().to_owned(),
                        path: memory_file.source.path.display().to_string(),
                        next_contents: next_contents.expect("write checked"),
                    },
                    source: None,
                    refresh_locality_id: locality_id.clone(),
                    authored_entry_ids: vec![selected_candidate.entry.id.clone()],
                    governance: Some(governance),
                }),
                staged_request,
                write_options,
            )
        }
    }
}

fn build_review(
    memory_file: &LoadedRuntimeMemorySurface,
    kind: CompactReviewKind,
    now: OffsetDateTime,
    targeted_entry_id: Option<&str>,
) -> Result<BuiltReview> {
    let mut candidates = memory_file
        .entries
        .iter()
        .filter_map(|entry| {
            entry
                .as_structured_entry()
                .map(|structured| candidate_for_review(entry.clone(), structured, kind, now))
        })
        .collect::<Result<Vec<_>>>()?
        .into_iter()
        .flatten()
        .collect::<Vec<_>>();

    let prompt_pressure_active =
        memory_file.source.content.len() >= PROMPT_BYTE_PRESSURE_THRESHOLD && candidates.len() > 1;
    sort_review_candidates(kind, prompt_pressure_active, &mut candidates);

    if let Some(entry_id) = targeted_entry_id {
        if let Some(candidate) = candidates
            .iter()
            .find(|candidate| candidate.entry.id == entry_id)
            .cloned()
        {
            candidates = vec![candidate];
        } else {
            let (_, selected_entry) = load_selected_entry(entry_id, memory_file)?;
            match review_eligibility(&selected_entry, kind, now)? {
                ReviewEligibility::Eligible { .. } => unreachable!("eligible candidate must exist"),
                ReviewEligibility::Ineligible { reason } => bail!(
                    "entry `{}` is not eligible for {} review: {}",
                    entry_id,
                    kind.as_str(),
                    reason
                ),
            }
        }
    }

    let pressure_signals = review_pressure_signals(memory_file, prompt_pressure_active);
    let ordering_rule = review_ordering_rule(kind, prompt_pressure_active);
    let note = review_note(kind);
    let view = CompactReviewView {
        review_type: kind.as_str(),
        ordering_rule,
        pressure_signals,
        candidates: candidates
            .iter()
            .map(|candidate| candidate.view.clone())
            .collect::<Vec<_>>(),
        note,
    };

    Ok(BuiltReview { view, candidates })
}

fn candidate_for_review(
    runtime_entry: RuntimeMemoryEntry,
    entry: StructuredMemoryEntry,
    kind: CompactReviewKind,
    now: OffsetDateTime,
) -> Result<Option<ReviewCandidate>> {
    let _ = runtime_entry;
    match review_eligibility(&entry, kind, now)? {
        ReviewEligibility::Eligible {
            eligibility_reason,
            age_basis,
            age_floor_days,
            eligible_at,
            age_seconds,
        } => Ok(Some(ReviewCandidate {
            view: ReviewCandidateView {
                entry: entry.clone(),
                eligibility_reason,
                age_basis,
                age_floor_days,
                eligible_at,
                content_bytes: entry.content.len(),
            },
            entry,
            age_seconds,
        })),
        ReviewEligibility::Ineligible { .. } => Ok(None),
    }
}

fn review_eligibility(
    entry: &StructuredMemoryEntry,
    kind: CompactReviewKind,
    now: OffsetDateTime,
) -> Result<ReviewEligibility> {
    match kind {
        CompactReviewKind::Expired => {
            let Some(expires_at) = entry.expires_at.as_deref() else {
                return Ok(ReviewEligibility::Ineligible {
                    reason: "missing `expires_at`".to_owned(),
                });
            };
            let expires_at = parse_entry_timestamp(entry, "expires_at", expires_at)?;
            let age_seconds = (now - expires_at).whole_seconds();
            if expires_at > now {
                let expires_at = expires_at.format(&Rfc3339)?;
                return Ok(ReviewEligibility::Ineligible {
                    reason: format!("`expires_at` {expires_at} has not passed yet"),
                });
            }

            let expires_at = expires_at.format(&Rfc3339)?;
            Ok(ReviewEligibility::Eligible {
                eligibility_reason: format!("`expires_at` {expires_at} is in the past"),
                age_basis: Some(expires_at.clone()),
                age_floor_days: None,
                eligible_at: Some(expires_at),
                age_seconds,
            })
        }
        CompactReviewKind::Superseded => {
            if entry.state != "superseded" {
                return Ok(ReviewEligibility::Ineligible {
                    reason: format!("state is `{}`, not `superseded`", entry.state),
                });
            }
            let Some(superseded_at) = entry.superseded_at.as_deref() else {
                return Ok(ReviewEligibility::Ineligible {
                    reason: "missing `superseded_at`".to_owned(),
                });
            };
            let Some(decay_class) = entry.decay_class.as_deref() else {
                return Ok(ReviewEligibility::Ineligible {
                    reason: "missing `decay_class`".to_owned(),
                });
            };
            let decay_class = parse_decay_class(decay_class)?;
            let superseded_at = parse_entry_timestamp(entry, "superseded_at", superseded_at)?;
            let floor_days = decay_class.review_floor_days();
            let eligible_at = superseded_at + Duration::days(floor_days);
            if eligible_at > now {
                let eligible_at = eligible_at.format(&Rfc3339)?;
                return Ok(ReviewEligibility::Ineligible {
                    reason: format!(
                        "entry becomes eligible on {} after the {}-day `{}` floor",
                        eligible_at,
                        floor_days,
                        decay_class.as_str()
                    ),
                });
            }

            let superseded_at_text = superseded_at.format(&Rfc3339)?;
            let eligible_at_text = eligible_at.format(&Rfc3339)?;
            Ok(ReviewEligibility::Eligible {
                eligibility_reason: format!(
                    "`superseded_at` {} passed the {}-day `{}` review floor",
                    superseded_at_text,
                    floor_days,
                    decay_class.as_str()
                ),
                age_basis: Some(superseded_at_text),
                age_floor_days: Some(floor_days),
                eligible_at: Some(eligible_at_text),
                age_seconds: (now - superseded_at).whole_seconds(),
            })
        }
        CompactReviewKind::PromotionCandidate => {
            if entry.state != "promotion_candidate" {
                return Ok(ReviewEligibility::Ineligible {
                    reason: format!("state is `{}`, not `promotion_candidate`", entry.state),
                });
            }
            let Some(expires_at) = entry.expires_at.as_deref() else {
                return Ok(ReviewEligibility::Ineligible {
                    reason: "missing `expires_at`".to_owned(),
                });
            };
            let expires_at = parse_entry_timestamp(entry, "expires_at", expires_at)?;
            let age_seconds = (now - expires_at).whole_seconds();
            if expires_at > now {
                let expires_at = expires_at.format(&Rfc3339)?;
                return Ok(ReviewEligibility::Ineligible {
                    reason: format!("`expires_at` {expires_at} has not passed yet"),
                });
            }

            let expires_at = expires_at.format(&Rfc3339)?;
            Ok(ReviewEligibility::Eligible {
                eligibility_reason: format!("`promotion_candidate` entry expired at {expires_at}"),
                age_basis: Some(expires_at.clone()),
                age_floor_days: None,
                eligible_at: Some(expires_at),
                age_seconds,
            })
        }
    }
}

fn parse_entry_timestamp(
    entry: &StructuredMemoryEntry,
    field: &str,
    value: &str,
) -> Result<OffsetDateTime> {
    OffsetDateTime::parse(value, &Rfc3339).map_err(|error| {
        anyhow!(
            "entry `{}` has invalid `{}` timestamp `{}`: {}",
            entry.id,
            field,
            value,
            error
        )
    })
}

fn sort_review_candidates(
    kind: CompactReviewKind,
    prompt_pressure_active: bool,
    candidates: &mut [ReviewCandidate],
) {
    candidates.sort_by(|left, right| {
        if prompt_pressure_active {
            right
                .view
                .content_bytes
                .cmp(&left.view.content_bytes)
                .then_with(|| right.age_seconds.cmp(&left.age_seconds))
                .then_with(|| left.entry.id.cmp(&right.entry.id))
        } else {
            right
                .age_seconds
                .cmp(&left.age_seconds)
                .then_with(|| {
                    if matches!(kind, CompactReviewKind::Superseded) {
                        right.view.content_bytes.cmp(&left.view.content_bytes)
                    } else {
                        left.view.content_bytes.cmp(&right.view.content_bytes)
                    }
                })
                .then_with(|| left.entry.id.cmp(&right.entry.id))
        }
    });
}

fn review_pressure_signals(
    memory_file: &LoadedRuntimeMemorySurface,
    prompt_pressure_active: bool,
) -> Vec<PressureSignalView> {
    if !prompt_pressure_active {
        return Vec::new();
    }

    vec![PressureSignalView {
        signal: "prompt_byte_pressure",
        detail: format!(
            "memory file has {} bytes; larger eligible entries are surfaced first",
            memory_file.source.content.len()
        ),
    }]
}

fn review_ordering_rule(kind: CompactReviewKind, prompt_pressure_active: bool) -> &'static str {
    match (kind, prompt_pressure_active) {
        (CompactReviewKind::Expired, true) => "prompt_byte_pressure_then_expiry_age",
        (CompactReviewKind::Expired, false) => "expiry_age",
        (CompactReviewKind::Superseded, true) => "prompt_byte_pressure_then_superseded_age",
        (CompactReviewKind::Superseded, false) => "superseded_age",
        (CompactReviewKind::PromotionCandidate, true) => {
            "prompt_byte_pressure_then_promotion_candidate_age"
        }
        (CompactReviewKind::PromotionCandidate, false) => "promotion_candidate_age",
    }
}

fn review_note(kind: CompactReviewKind) -> &'static str {
    match kind {
        CompactReviewKind::Expired => {
            "Preview only. No files were changed. Re-run with `--review expired --entry <id> --remove` to preview or apply an explicit removal."
        }
        CompactReviewKind::Superseded => {
            "Preview only. No files were changed. Re-run with `--review superseded --entry <id> --remove` to preview or apply an explicit removal."
        }
        CompactReviewKind::PromotionCandidate => {
            "Preview only. No files were changed. Re-run with `--review promotion-candidate --entry <id> --remove` to preview or apply an explicit stale-candidate removal."
        }
    }
}

fn review_surface_mode(kind: CompactReviewKind) -> &'static str {
    match kind {
        CompactReviewKind::Expired => "expired_review",
        CompactReviewKind::Superseded => "superseded_review",
        CompactReviewKind::PromotionCandidate => "promotion_candidate_review",
    }
}

fn review_remove_mode(kind: CompactReviewKind, write: bool) -> &'static str {
    match (kind, write) {
        (CompactReviewKind::Expired, false) => "expired_remove_preview",
        (CompactReviewKind::Expired, true) => "expired_remove_write",
        (CompactReviewKind::Superseded, false) => "superseded_remove_preview",
        (CompactReviewKind::Superseded, true) => "superseded_remove_write",
        (CompactReviewKind::PromotionCandidate, false) => "promotion_candidate_remove_preview",
        (CompactReviewKind::PromotionCandidate, true) => "promotion_candidate_remove_write",
    }
}

fn review_remove_match_rule(kind: CompactReviewKind) -> &'static str {
    match kind {
        CompactReviewKind::Expired => "eligible_expired_review_remove",
        CompactReviewKind::Superseded => "eligible_superseded_review_remove",
        CompactReviewKind::PromotionCandidate => "eligible_promotion_candidate_review_remove",
    }
}

fn load_selected_entry(
    entry_id: &str,
    memory_file: &LoadedRuntimeMemorySurface,
) -> Result<(RuntimeMemoryEntry, StructuredMemoryEntry)> {
    let runtime_entry = memory_file
        .entries
        .iter()
        .find(|entry| entry.structured_id() == Some(entry_id))
        .cloned()
        .ok_or_else(|| missing_entry_error(entry_id, memory_file))?;
    let structured_entry = runtime_entry
        .as_structured_entry()
        .expect("selected structured memory entry");
    Ok((runtime_entry, structured_entry))
}

fn validate_duplicate_pair(
    selected_entry: &StructuredMemoryEntry,
    keep_entry: &StructuredMemoryEntry,
) -> Result<()> {
    if selected_entry.state == "superseded" {
        bail!(
            "entry `{}` is already superseded; the first `ccd memory compact` slice only handles live duplicates",
            selected_entry.id
        );
    }

    if keep_entry.state == "superseded" {
        bail!(
            "keep entry `{}` is already superseded; choose a non-superseded canonical entry",
            keep_entry.id
        );
    }

    if keep_entry
        .supersedes
        .iter()
        .any(|id| id == &selected_entry.id)
    {
        bail!(
            "keep entry `{}` already records `{}` in `supersedes`; refusing to preview a no-op compaction",
            keep_entry.id,
            selected_entry.id
        );
    }

    if duplicate_key(selected_entry) != duplicate_key(keep_entry) {
        bail!(
            "entries `{}` and `{}` are not exact duplicates by normalized content and type",
            selected_entry.id,
            keep_entry.id
        );
    }

    Ok(())
}

fn updated_keep_entry(
    keep_entry: &RuntimeMemoryEntry,
    superseded_id: &str,
) -> Result<RuntimeMemoryEntry> {
    let mut supersedes = keep_entry.supersedes.clone();
    if supersedes.iter().any(|id| id == superseded_id) {
        bail!(
            "keep entry `{}` already records `{}` in `supersedes`",
            keep_entry.structured_id().unwrap_or("<unknown>"),
            superseded_id
        );
    }
    supersedes.push(superseded_id.to_owned());
    Ok(keep_entry.with_supersedes(supersedes))
}

fn superseded_selected_entry(
    selected_entry: &RuntimeMemoryEntry,
    superseded_at: String,
) -> RuntimeMemoryEntry {
    selected_entry
        .with_structured_state("superseded")
        .with_superseded_at(Some(superseded_at))
}

fn duplicate_key(entry: &StructuredMemoryEntry) -> (String, String) {
    (
        entry.entry_type.clone(),
        entry
            .content
            .split_whitespace()
            .collect::<Vec<_>>()
            .join(" "),
    )
}

fn duplicate_diff_preview(
    keep_entry: &StructuredMemoryEntry,
    written_keep_entry: &StructuredMemoryEntry,
    selected_entry: &StructuredMemoryEntry,
    written_selected_entry: &StructuredMemoryEntry,
) -> String {
    [
        format!("@@ keep {}", keep_entry.id),
        signed_block('-', &render_entry_block(keep_entry)),
        signed_block('+', &render_entry_block(written_keep_entry)),
        format!("@@ duplicate {}", selected_entry.id),
        signed_block('-', &render_entry_block(selected_entry)),
        signed_block('+', &render_entry_block(written_selected_entry)),
    ]
    .join("\n")
}

fn single_entry_diff_preview(
    selected_entry: &StructuredMemoryEntry,
    written_selected_entry: &StructuredMemoryEntry,
) -> String {
    [
        format!("@@ entry {}", selected_entry.id),
        signed_block('-', &render_entry_block(selected_entry)),
        signed_block('+', &render_entry_block(written_selected_entry)),
    ]
    .join("\n")
}

fn removal_diff_preview(selected_entry: &StructuredMemoryEntry) -> String {
    [
        format!("@@ remove {}", selected_entry.id),
        signed_block('-', &render_entry_block(selected_entry)),
    ]
    .join("\n")
}

fn signed_block(sign: char, block: &str) -> String {
    block
        .lines()
        .map(|line| format!("{sign}{line}"))
        .collect::<Vec<_>>()
        .join("\n")
}

fn missing_entry_error(entry_id: &str, memory_file: &LoadedRuntimeMemorySurface) -> anyhow::Error {
    let available_ids = memory_file
        .entries
        .iter()
        .filter_map(RuntimeMemoryEntry::structured_id)
        .collect::<Vec<_>>()
        .join(", ");
    anyhow!(
        "structured memory entry `{}` was not found in {}; available ids: {}",
        entry_id,
        memory_file.source.path.display(),
        available_ids
    )
}

fn validate_decay_class_change(
    selected_entry: &StructuredMemoryEntry,
    target: DecayClass,
) -> Result<()> {
    let Some(current) = selected_entry.decay_class.as_deref() else {
        return Ok(());
    };

    let current = parse_decay_class(current)?;
    if current == target {
        bail!(
            "entry `{}` already has decay_class `{}`; refusing to preview a no-op lifecycle compaction",
            selected_entry.id,
            target.as_str()
        );
    }

    if target.durability_rank() > current.durability_rank() {
        bail!(
            "entry `{}` has decay_class `{}`; `ccd memory compact --decay-class` only supports explicit downgrades or backfills, not upgrades",
            selected_entry.id,
            current.as_str()
        );
    }

    Ok(())
}

fn parse_decay_class(value: &str) -> Result<DecayClass> {
    match value {
        "permanent" => Ok(DecayClass::Permanent),
        "stable" => Ok(DecayClass::Stable),
        "active" => Ok(DecayClass::Active),
        other => bail!(
            "entry has unsupported decay_class `{other}`; expected permanent, stable, or active"
        ),
    }
}

fn should_stage_via_queue(write: bool, scope: CompactScope, layout: &StateLayout) -> Result<bool> {
    if !write {
        return Ok(false);
    }
    let _ = scope;
    queue::queue_required_for_autonomous(layout)
}

fn compact_mode_key<'a>(mode: &'a CompactMode<'a>) -> CompactModeKey<'a> {
    match *mode {
        CompactMode::Duplicate { entry_id, keep_id } => {
            CompactModeKey::Duplicate { entry_id, keep_id }
        }
        CompactMode::DecayClass { entry_id, target } => CompactModeKey::DecayClass {
            entry_id,
            target: target.as_str(),
        },
        CompactMode::Review {
            kind,
            entry_id,
            outcome,
        } => CompactModeKey::Review {
            review: kind.as_str(),
            entry_id,
            outcome: match outcome {
                ReviewOutcome::Surface => "surface",
                ReviewOutcome::Remove => "remove",
            },
        },
    }
}

pub(crate) fn replay_request<'a>(
    scope: CompactScope,
    mode: &'a CompactMode<'a>,
    op_id_hint: Option<&str>,
) -> Result<(String, String)> {
    let request_key = CompactQueueRequestKey {
        command: "memory-compact",
        scope: scope.surface_scope(),
        mode: compact_mode_key(mode),
    };
    let request_fingerprint = queue::fingerprint(&request_key)?;
    let op_id = queue::stable_op_id("compact", &request_fingerprint, op_id_hint);
    Ok((request_fingerprint, op_id))
}

fn finalize_compact_report(
    repo_root: &Path,
    layout: &StateLayout,
    mut report: MemoryCompactReport,
    plan: Option<queue::MemoryOpPlan>,
    staged_request: Option<&QueuedWriteRequest>,
    write_options: &ExclusiveWriteOptions,
) -> Result<MemoryCompactReport> {
    let Some(plan) = plan else {
        return Ok(report);
    };

    if let Some(staged_request) = staged_request {
        let staged_write = queue::stage_and_reconcile(
            repo_root,
            layout,
            "memory-compact",
            &staged_request.op_id,
            &staged_request.request_fingerprint,
            write_options,
            &plan,
        )?;
        let staged_ok = matches!(
            staged_write.outcome,
            crate::state::protected_write::AppendWriteOutcome::Applied
                | crate::state::protected_write::AppendWriteOutcome::IdempotentNoop
        );
        if !staged_ok {
            report.ok = false;
            report.write_result = None;
            report.message = staged_write.message.clone();
        }
        report.staged_write = Some(staged_write);
        if report.ok {
            queue::persist_report_snapshot(layout, &staged_request.op_id, &report)?;
        }
        return Ok(report);
    }

    let outcome = queue::apply_direct_plan(repo_root, layout, &plan)?;
    if outcome == crate::state::protected_write::AppendWriteOutcome::IdempotentNoop {
        if let Some(write_result) = report.write_result.as_mut() {
            write_result.file_action = "already_applied";
        }
    }
    Ok(report)
}

fn current_autonomous_session_id(layout: &StateLayout) -> Result<Option<String>> {
    if queue::queue_required_for_autonomous(layout)? {
        session::load_session_id(layout)
    } else {
        Ok(None)
    }
}

fn ensure_profile_exists(layout: &StateLayout) -> Result<()> {
    let profile_root = layout.profile_root();
    if profile_root.is_dir() {
        return Ok(());
    }

    bail!(
        "profile `{}` does not exist at {}; bootstrap it with `ccd attach` before using `ccd memory compact`",
        layout.profile(),
        profile_root.display()
    )
}

fn ensure_repo_registry_exists(
    layout: &StateLayout,
    repo_root: &Path,
    locality_id: &str,
) -> Result<()> {
    let registry_path = layout.repo_metadata_path(locality_id)?;
    match repo_registry::load(&registry_path)? {
        Some(_) => Ok(()),
        None => bail!(
            "repo registry is missing for locality_id `{}` at {}; run `ccd link --path {}` or `ccd attach --path {}` again",
            locality_id,
            registry_path.display(),
            repo_root.display(),
            repo_root.display()
        ),
    }
}

fn ensure_repo_overlay_exists(layout: &StateLayout, locality_id: &str) -> Result<()> {
    let overlay_root = layout.repo_overlay_root(locality_id)?;
    if overlay_root.is_dir() {
        return Ok(());
    }

    bail!(
        "repo overlay is missing for locality_id `{}` at {}; run `ccd link --path .` or `ccd attach --path .` again",
        locality_id,
        overlay_root.display()
    )
}

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

    #[test]
    fn parse_mode_requires_exactly_one_selector() {
        let error = parse_mode(CompactModeRequest {
            entry_id: Some("entry-1"),
            keep_id: Some("keep-1"),
            decay_class: None,
            review: Some(CompactReviewKind::Expired),
            remove: false,
            write: false,
        })
        .expect_err("multiple selectors should fail");

        assert!(error.to_string().contains("exactly one"));
    }

    #[test]
    fn parse_mode_builds_review_remove_variants() {
        let mode = parse_mode(CompactModeRequest {
            entry_id: Some("entry-2"),
            keep_id: None,
            decay_class: None,
            review: Some(CompactReviewKind::Superseded),
            remove: true,
            write: false,
        })
        .expect("review mode should parse");

        match mode {
            CompactMode::Review {
                kind,
                entry_id,
                outcome,
            } => {
                assert_eq!(kind, CompactReviewKind::Superseded);
                assert_eq!(entry_id, Some("entry-2"));
                assert_eq!(outcome, ReviewOutcome::Remove);
            }
            _ => panic!("expected review mode"),
        }
    }
}
