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

use anyhow::{bail, Result};
use serde::Serialize;
use tracing::debug;

use crate::memory::entries::StructuredMemoryEntry;
use crate::memory::file::{append_block_to_contents, render_entry_block};
use crate::memory::{governance, promote, 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;

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

impl CandidateSourceScope {
    fn cli_label(self) -> &'static str {
        match self {
            Self::Clone => "workspace-memory",
            Self::Branch => "work-stream-memory",
        }
    }

    fn report_scope(self) -> &'static str {
        match self {
            Self::Clone => "workspace_memory",
            Self::Branch => "work_stream_memory",
        }
    }
}

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

impl CandidateDestination {
    fn cli_label(self) -> &'static str {
        match self {
            Self::Branch => "work-stream-memory",
            Self::Repo => "project-memory",
        }
    }

    fn report_scope(self) -> &'static str {
        match self {
            Self::Branch => "work_stream_memory",
            Self::Repo => "project_memory",
        }
    }
}

#[derive(Serialize)]
pub struct MemoryCandidateAdmitReport {
    command: &'static str,
    ok: bool,
    path: String,
    profile: String,
    project_id: String,
    locality_id: String,
    mode: &'static str,
    source: MemorySurfaceView,
    destination: MemorySurfaceView,
    selected_entry: StructuredMemoryEntry,
    destination_preview: DestinationPreviewView,
    next_action: NextActionView,
    governance: governance::GovernanceDecisionView,
    #[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 DestinationPreviewView {
    append_mode: &'static str,
    file_status: &'static str,
    provenance_link: String,
    entry_block: String,
    note: String,
}

#[derive(Serialize)]
struct NextActionView {
    kind: &'static str,
    argv: Vec<String>,
    note: &'static str,
}

#[derive(Serialize)]
struct WriteResultView {
    destination_action: &'static str,
    destination_path: String,
    compiled_state_action: &'static str,
    written_destination_entry: StructuredMemoryEntry,
}

impl CommandReport for MemoryCandidateAdmitReport {
    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 candidate write was rejected.")
            );
            return;
        }

        let destination_scope = self.destination.scope.replace('_', " ");
        match &self.write_result {
            Some(_) => println!(
                "Admitted reviewed memory candidate {} into {}.",
                self.selected_entry.id, destination_scope
            ),
            None => println!(
                "Prepared memory candidate admission preview for entry {}.",
                self.selected_entry.id
            ),
        }
        println!("Mode: {}", self.mode);
        println!("Source: {} ({})", self.source.path, self.source.status);
        println!(
            "Destination: {} ({})",
            self.destination.path, self.destination.status
        );
        println!("Entry type: {}", self.selected_entry.entry_type);
        println!(
            "Destination provenance: {}",
            self.destination_preview.provenance_link
        );
        println!(
            "Governance: {} ({})",
            self.governance.action.as_str(),
            self.governance.status.as_str()
        );

        if let Some(write_result) = &self.write_result {
            println!(
                "Destination write: {} ({})",
                write_result.destination_action, write_result.destination_path
            );
            println!(
                "Compiled state: {}",
                write_result.compiled_state_action.replace('_', " ")
            );
        }
        if let Some(staged_write) = &self.staged_write {
            println!(
                "Queued op: {} ({})",
                staged_write.op_id,
                match staged_write.outcome {
                    crate::state::protected_write::AppendWriteOutcome::Applied => "applied",
                    crate::state::protected_write::AppendWriteOutcome::IdempotentNoop => {
                        "idempotent_noop"
                    }
                    crate::state::protected_write::AppendWriteOutcome::OwnershipConflict => {
                        "ownership_conflict"
                    }
                    crate::state::protected_write::AppendWriteOutcome::StaleSession => {
                        "stale_session"
                    }
                    crate::state::protected_write::AppendWriteOutcome::UnsupportedMultiwriter => {
                        "unsupported_multiwriter"
                    }
                    crate::state::protected_write::AppendWriteOutcome::DuplicateIdConflict => {
                        "duplicate_id_conflict"
                    }
                }
            );
        }

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

        println!();
        println!("Destination block:");
        println!("{}", self.destination_preview.entry_block);
        println!();
        println!("{}", self.destination_preview.note);
        println!();
        println!("Next review command:");
        println!("{}", self.next_action.argv.join(" "));
        println!("{}", self.next_action.note);
    }
}

#[allow(clippy::too_many_arguments)]
pub fn run_admit(
    repo_root: &Path,
    explicit_profile: Option<&str>,
    entry_id: &str,
    source_scope: CandidateSourceScope,
    destination: CandidateDestination,
    write: bool,
    write_options: ExclusiveWriteOptions,
    op_id_hint: Option<&str>,
) -> Result<MemoryCandidateAdmitReport> {
    validate_route(source_scope, destination)?;

    let profile = profile::resolve(explicit_profile)?;
    let layout = StateLayout::resolve(repo_root, profile.clone())?;
    ensure_profile_exists(&layout)?;

    let marker = repo_marker::load(repo_root)?.ok_or_else(|| {
        anyhow::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 staged_request = if should_stage_via_queue(write, destination, &layout)? {
        let request_key = CandidateQueueRequestKey {
            command: "memory-candidate-admit",
            entry_id,
            source_scope: source_scope.report_scope(),
            destination: destination.report_scope(),
        };
        let request_fingerprint = queue::fingerprint(&request_key)?;
        let op_id = queue::stable_op_id("candidate_admit", &request_fingerprint, op_id_hint);
        Some((request_fingerprint, op_id))
    } else {
        None
    };

    let source_memory = load_source_surface(repo_root, &layout, &locality_id, source_scope)?;
    let (selected_runtime_entry, selected_entry) = promote::load_selected_entry(
        entry_id,
        &source_memory,
        source_scope.cli_label(),
        "`ccd memory candidate admit`",
    )?;
    validate_candidate_source(&selected_entry)?;

    let destination_memory =
        load_destination_surface(repo_root, &layout, &locality_id, destination)?;
    if destination_memory
        .entries
        .iter()
        .any(|entry| entry.structured_id() == Some(selected_entry.id.as_str()))
    {
        bail!(
            "{} already contains entry id `{}` at {}; refusing to write or preview an unexpected overwrite",
            destination.cli_label(),
            selected_entry.id,
            destination_memory.source.path.display()
        );
    }

    debug!(
        source = source_scope.cli_label(),
        destination = destination.cli_label(),
        path = %destination_memory.source.path.display(),
        write,
        "admitting reviewed memory candidate"
    );

    let provenance_link = format!(
        "{}#{}",
        source_memory.source.path.display(),
        selected_entry.id
    );
    let autonomous = queue::queue_required_for_autonomous(&layout)?;
    let active_session_id = if autonomous {
        session::load_session_id(&layout)?
    } else {
        None
    };
    let repo_overlap_memory = if destination == CandidateDestination::Branch {
        Some(runtime_state::load_locality_memory_surface(
            &layout,
            &locality_id,
        )?)
    } else {
        None
    };
    let mut governance = candidate_governance_decision(CandidateGovernanceContext {
        layout: &layout,
        source_scope,
        destination,
        selected_entry: &selected_entry,
        destination_entries: &destination_memory.entries,
        repo_overlap_entries: repo_overlap_memory
            .as_ref()
            .map(|surface| surface.entries.as_slice()),
        autonomous,
        session_id: active_session_id.as_deref(),
        current_op_id: staged_request.as_ref().map(|(_, op_id)| op_id.as_str()),
    })?;
    let written_destination_runtime =
        admitted_destination_entry(&selected_runtime_entry, &provenance_link);
    let written_destination_entry = written_destination_runtime
        .as_structured_entry()
        .expect("admitted destination entry");

    let destination_preview = DestinationPreviewView {
        append_mode: "append_block",
        file_status: destination_memory.source.status.as_str(),
        provenance_link,
        entry_block: render_entry_block(&written_destination_entry),
        note: candidate_preview_note(write, autonomous, &governance),
    };
    let next_action = next_promotion_action(repo_root, profile.as_str(), entry_id, destination);

    let next_destination_contents = append_block_to_contents(
        &destination_memory.source.content,
        &destination_preview.entry_block,
    );
    let write_result = write.then(|| WriteResultView {
        destination_action: "append_block",
        destination_path: destination_memory.source.path.display().to_string(),
        compiled_state_action: "refreshed",
        written_destination_entry: written_destination_entry.clone(),
    });

    let mut report = MemoryCandidateAdmitReport {
        command: "memory-candidate-admit",
        ok: true,
        path: repo_root.display().to_string(),
        profile: profile.to_string(),
        project_id: locality_id.to_owned(),
        locality_id: locality_id.to_owned(),
        mode: mode_label(source_scope, destination, write),
        source: MemorySurfaceView {
            scope: source_scope.report_scope(),
            path: source_memory.source.path.display().to_string(),
            status: source_memory.source.status.as_str(),
        },
        destination: MemorySurfaceView {
            scope: destination.report_scope(),
            path: destination_memory.source.path.display().to_string(),
            status: destination_memory.source.status.as_str(),
        },
        selected_entry,
        destination_preview,
        next_action,
        governance: governance.clone(),
        write_result,
        staged_write: None,
        message: None,
        warnings: Vec::new(),
    };

    if write {
        let mutation_id = staged_request
            .as_ref()
            .map(|(_, op_id)| op_id.as_str())
            .unwrap_or("memory-candidate-admit-direct");
        governance = governance.with_rollback(
            mutation_id,
            write_options.actor_id.as_deref(),
            write_options
                .session_id
                .as_deref()
                .or(active_session_id.as_deref()),
            vec![report.selected_entry.id.clone()],
            &destination_memory.source.content,
            None,
        );
        report.governance = governance.clone();
    }

    if write && governance.blocked_for_write(autonomous) {
        report.ok = false;
        report.write_result = None;
        report.message = Some(governance.blocked_write_message());
        return Ok(report);
    }

    if let Some((request_fingerprint, op_id)) = staged_request {
        let plan = queue::MemoryOpPlan {
            target: queue::MemoryFileMutation {
                scope: destination.report_scope().to_owned(),
                path: destination_memory.source.path.display().to_string(),
                next_contents: next_destination_contents,
            },
            source: None,
            refresh_locality_id: Some(locality_id.clone()),
            authored_entry_ids: vec![report.selected_entry.id.clone()],
            governance: Some(governance),
        };
        let staged_write = queue::stage_and_reconcile(
            repo_root,
            &layout,
            "memory-candidate-admit",
            &op_id,
            &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, &op_id, &report)?;
        }
    } else if write {
        let outcome = queue::apply_direct_plan(
            repo_root,
            &layout,
            &queue::MemoryOpPlan {
                target: queue::MemoryFileMutation {
                    scope: destination.report_scope().to_owned(),
                    path: destination_memory.source.path.display().to_string(),
                    next_contents: next_destination_contents,
                },
                source: None,
                refresh_locality_id: Some(locality_id.clone()),
                authored_entry_ids: vec![report.selected_entry.id.clone()],
                governance: Some(governance),
            },
        )?;
        if outcome == crate::state::protected_write::AppendWriteOutcome::IdempotentNoop {
            if let Some(write_result) = report.write_result.as_mut() {
                write_result.compiled_state_action = "unchanged";
            }
        }
    }

    Ok(report)
}

fn validate_route(
    source_scope: CandidateSourceScope,
    destination: CandidateDestination,
) -> Result<()> {
    match (source_scope, destination) {
        (CandidateSourceScope::Clone, CandidateDestination::Branch)
        | (CandidateSourceScope::Clone, CandidateDestination::Repo)
        | (CandidateSourceScope::Branch, CandidateDestination::Repo) => Ok(()),
        (CandidateSourceScope::Branch, CandidateDestination::Branch) => bail!(
            "`ccd memory candidate admit --source-scope work-stream-memory` only supports `--destination project-memory`"
        ),
    }
}

fn validate_candidate_source(selected_entry: &StructuredMemoryEntry) -> Result<()> {
    if selected_entry.state != "active" {
        bail!(
            "entry `{}` has state `{}`; `ccd memory candidate admit` only accepts `active` source entries in this first slice",
            selected_entry.id,
            selected_entry.state
        );
    }
    if !promote::is_promotable_entry_type(&selected_entry.entry_type) {
        bail!(
            "entry `{}` has type `{}`, which is viewable but not admit-ready in the first cut",
            selected_entry.id,
            selected_entry.entry_type
        );
    }
    Ok(())
}

fn load_source_surface(
    repo_root: &Path,
    layout: &StateLayout,
    locality_id: &str,
    scope: CandidateSourceScope,
) -> Result<LoadedRuntimeMemorySurface> {
    match scope {
        CandidateSourceScope::Clone => runtime_state::load_clone_memory_surface(layout),
        CandidateSourceScope::Branch => {
            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 candidate admit --source-scope work-stream-memory`"
                );
            }
            runtime_state::load_branch_memory_surface(repo_root, layout, locality_id)
        }
    }
}

fn load_destination_surface(
    repo_root: &Path,
    layout: &StateLayout,
    locality_id: &str,
    destination: CandidateDestination,
) -> Result<LoadedRuntimeMemorySurface> {
    match destination {
        CandidateDestination::Branch => {
            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 candidate admit --destination work-stream-memory`"
                );
            }
            runtime_state::load_branch_memory_surface(repo_root, layout, locality_id)
        }
        CandidateDestination::Repo => {
            runtime_state::load_locality_memory_surface(layout, locality_id)
        }
    }
}

fn admitted_destination_entry(
    selected_entry: &RuntimeMemoryEntry,
    provenance_link: &str,
) -> RuntimeMemoryEntry {
    selected_entry
        .with_structured_state("promotion_candidate")
        .with_source_ref(Some(provenance_link.to_owned()))
}

fn next_promotion_action(
    repo_root: &Path,
    profile: &str,
    entry_id: &str,
    destination: CandidateDestination,
) -> NextActionView {
    let mut argv = vec![
        "ccd".to_owned(),
        "memory".to_owned(),
        "promote".to_owned(),
        "--path".to_owned(),
        repo_root.display().to_string(),
        "--profile".to_owned(),
        profile.to_owned(),
        "--entry".to_owned(),
        entry_id.to_owned(),
        "--destination".to_owned(),
    ];
    let (destination_arg, note) = match destination {
        CandidateDestination::Branch => (
            "project-memory",
            "Review promotion from work-stream memory into project memory once this candidate proves durable beyond the work stream.",
        ),
        CandidateDestination::Repo => (
            "profile-memory",
            "Review promotion from project memory into profile memory when this candidate proves reusable beyond the project.",
        ),
    };
    argv.push(destination_arg.to_owned());
    NextActionView {
        kind: "memory_promote_preview",
        argv,
        note,
    }
}

fn mode_label(
    source_scope: CandidateSourceScope,
    destination: CandidateDestination,
    write: bool,
) -> &'static str {
    match (source_scope, destination, write) {
        (CandidateSourceScope::Clone, CandidateDestination::Branch, false) => {
            "workspace_memory_to_work_stream_candidate_preview"
        }
        (CandidateSourceScope::Clone, CandidateDestination::Branch, true) => {
            "workspace_memory_to_work_stream_candidate_write"
        }
        (CandidateSourceScope::Branch, CandidateDestination::Repo, false) => {
            "work_stream_memory_to_project_candidate_preview"
        }
        (CandidateSourceScope::Branch, CandidateDestination::Repo, true) => {
            "work_stream_memory_to_project_candidate_write"
        }
        (_, CandidateDestination::Repo, false) => "workspace_memory_to_project_candidate_preview",
        (_, CandidateDestination::Repo, true) => "workspace_memory_to_project_candidate_write",
        _ => unreachable!("invalid source/destination route already rejected"),
    }
}

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

struct CandidateGovernanceContext<'a> {
    layout: &'a StateLayout,
    source_scope: CandidateSourceScope,
    destination: CandidateDestination,
    selected_entry: &'a StructuredMemoryEntry,
    destination_entries: &'a [RuntimeMemoryEntry],
    repo_overlap_entries: Option<&'a [RuntimeMemoryEntry]>,
    autonomous: bool,
    session_id: Option<&'a str>,
    current_op_id: Option<&'a str>,
}

fn candidate_governance_decision(
    context: CandidateGovernanceContext<'_>,
) -> Result<governance::GovernanceDecisionView> {
    if let Some(duplicate) =
        governance::find_runtime_duplicate(context.destination_entries, context.selected_entry)
    {
        return Ok(governance::GovernanceDecisionView::new(
            governance::GovernanceDecisionInit {
                action: governance::GovernanceAction::Discard,
                status: governance::GovernanceStatus::Discarded,
                target_scope: context.destination.report_scope().to_owned(),
                source_scope: Some(context.source_scope.report_scope().to_owned()),
                entry_type: context.selected_entry.entry_type.clone(),
                rationale: format!(
                    "a normalized duplicate already exists in {} as `{}`",
                    context.destination.report_scope(),
                    duplicate.entry_id
                ),
                evidence_summary: vec![
                    "duplicate suppression applies before widening scope".to_owned(),
                    "the destination already carries the same normalized lesson".to_owned(),
                ],
                pressure_signals: vec![governance::signal(
                    "duplicate_match",
                    format!(
                        "{} already contains normalized duplicate `{}`",
                        context.destination.report_scope(),
                        duplicate.entry_id
                    ),
                )],
                duplicate_digest: Some(duplicate.digest),
                rate_limit: None,
            },
        ));
    }

    if context.destination == CandidateDestination::Branch {
        if let Some(repo_entries) = context.repo_overlap_entries {
            if let Some(duplicate) =
                governance::find_runtime_duplicate(repo_entries, context.selected_entry)
            {
                return Ok(governance::GovernanceDecisionView::new(
                    governance::GovernanceDecisionInit {
                        action: governance::GovernanceAction::Discard,
                        status: governance::GovernanceStatus::Discarded,
                        target_scope: context.destination.report_scope().to_owned(),
                        source_scope: Some(context.source_scope.report_scope().to_owned()),
                        entry_type: context.selected_entry.entry_type.clone(),
                        rationale: format!(
                            "work-stream capture overlaps existing project memory entry `{}`",
                            duplicate.entry_id
                        ),
                        evidence_summary: vec![
                            "work-stream capture stays local when project memory already contains the lesson"
                                .to_owned(),
                        ],
                        pressure_signals: vec![governance::signal(
                            "broader_scope_overlap",
                        format!(
                            "project_memory already carries normalized duplicate `{}`",
                            duplicate.entry_id
                        ),
                        )],
                        duplicate_digest: Some(duplicate.digest),
                        rate_limit: None,
                    },
                ));
            }
        }
    }

    let action = match context.destination {
        CandidateDestination::Branch => governance::GovernanceAction::AdmitWorkStream,
        CandidateDestination::Repo => governance::GovernanceAction::StageProjectPromotion,
    };
    let rate_limit = if context.autonomous {
        governance::session_rate_limit(
            context.layout,
            context.session_id,
            context.destination.report_scope(),
            context.current_op_id,
        )?
    } else {
        None
    };
    let mut pressure_signals = Vec::new();
    if let Some(rate_limit) = &rate_limit {
        if rate_limit.remaining == 0 {
            pressure_signals.push(governance::signal(
                "session_cap_reached",
                format!(
                    "{} already has {} queued admissions or promotions in this session",
                    rate_limit.scope, rate_limit.count
                ),
            ));
        } else if rate_limit.remaining == 1 {
            pressure_signals.push(governance::signal(
                "session_cap_near",
                format!(
                    "{} has one remaining governed mutation slot in this session",
                    rate_limit.scope
                ),
            ));
        }
    }

    let status = if rate_limit
        .as_ref()
        .is_some_and(|limit| limit.status == "reached")
    {
        governance::GovernanceStatus::RateLimited
    } else {
        governance::GovernanceStatus::Allowed
    };
    let rationale = match action {
        governance::GovernanceAction::AdmitWorkStream => {
            "work-stream admission is allowed for a focused lesson on an active non-trunk work stream"
                .to_owned()
        }
        governance::GovernanceAction::StageProjectPromotion => {
            "project-scope admission stays staged as a governed promotion candidate".to_owned()
        }
        _ => unreachable!("candidate admit only targets work-stream or project memory"),
    };

    Ok(governance::GovernanceDecisionView::new(
        governance::GovernanceDecisionInit {
            action,
            status,
            target_scope: context.destination.report_scope().to_owned(),
            source_scope: Some(context.source_scope.report_scope().to_owned()),
            entry_type: context.selected_entry.entry_type.clone(),
            rationale,
            evidence_summary: vec![
                format!(
                    "entry type `{}` is admit-ready",
                    context.selected_entry.entry_type
                ),
                format!(
                    "source scope `{}` was validated",
                    context.source_scope.report_scope()
                ),
                format!(
                    "destination scope `{}` remains bounded",
                    context.destination.report_scope()
                ),
            ],
            pressure_signals,
            duplicate_digest: Some(governance::normalized_duplicate_digest(
                context.selected_entry,
            )),
            rate_limit,
        },
    ))
}

fn candidate_preview_note(
    write: bool,
    autonomous: bool,
    decision: &governance::GovernanceDecisionView,
) -> String {
    if write && decision.blocked_for_write(autonomous) {
        return format!(
            "Write was not applied. Governance outcome: {} ({})",
            decision.action.as_str(),
            decision.blocked_write_message()
        );
    }

    if !write && decision.status != governance::GovernanceStatus::Allowed {
        return format!(
            "Preview only. Governance outcome: {} ({})",
            decision.action.as_str(),
            decision.rationale
        );
    }

    if write {
        "Applied with explicit approval. The block above was appended as a higher-scope `promotion_candidate` entry.".to_owned()
    } else {
        "Preview only. No files were changed. Rerun with `--write` to stage this reviewed candidate in the destination memory scope.".to_owned()
    }
}

#[derive(Serialize)]
struct CandidateQueueRequestKey<'a> {
    command: &'static str,
    entry_id: &'a str,
    source_scope: &'static str,
    destination: &'static str,
}

pub(crate) fn replay_request(
    entry_id: &str,
    source_scope: CandidateSourceScope,
    destination: CandidateDestination,
    op_id_hint: Option<&str>,
) -> Result<(String, String)> {
    let request_key = CandidateQueueRequestKey {
        command: "memory-candidate-admit",
        entry_id,
        source_scope: source_scope.report_scope(),
        destination: destination.report_scope(),
    };
    let request_fingerprint = queue::fingerprint(&request_key)?;
    let op_id = queue::stable_op_id("candidate_admit", &request_fingerprint, op_id_hint);
    Ok((request_fingerprint, op_id))
}

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 candidate admit`",
        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 for `{}` is missing at {}; run `ccd attach --path .` or `ccd link --path .` to restore it before using `ccd memory candidate admit`",
        locality_id,
        overlay_root.display()
    )
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::memory::entries::StructuredMemoryEntry;

    fn structured_entry(state: &str, entry_type: &str) -> StructuredMemoryEntry {
        StructuredMemoryEntry {
            id: "entry-1".to_owned(),
            entry_type: entry_type.to_owned(),
            state: state.to_owned(),
            created_at: "2024-01-01T00:00:00Z".to_owned(),
            last_touched_session: 1,
            origin: "manual".to_owned(),
            superseded_at: None,
            decay_class: None,
            expires_at: None,
            tags: Vec::new(),
            source_ref: None,
            supersedes: Vec::new(),
            content: "A lesson worth keeping".to_owned(),
        }
    }

    #[test]
    fn validate_route_rejects_branch_to_branch_admission() {
        let error = validate_route(CandidateSourceScope::Branch, CandidateDestination::Branch)
            .expect_err("branch-to-branch route should fail");

        assert!(error.to_string().contains("project-memory"));
    }

    #[test]
    fn validate_candidate_source_rejects_inactive_entries() {
        let error = validate_candidate_source(&structured_entry("superseded", "rule"))
            .expect_err("inactive source entries should fail");

        assert!(error.to_string().contains("active"));
    }
}
