ccd-cli 1.0.0-beta.4

Bootstrap and validate Continuous Context Development repositories
use crate::handoff::GitState;
use crate::state::session_gates;

use super::{
    DriftAggregateStatus, EvaluationBucket, EvaluationInputs, MemoryStatus, NextStepStatus,
    RadarEvaluation, WrapUpStatus,
};

pub(super) fn build_evaluation(inputs: EvaluationInputs<'_>) -> RadarEvaluation {
    RadarEvaluation {
        next_step: build_next_step_bucket(&inputs),
        memory: build_memory_bucket(&inputs),
        wrap_up: build_wrap_up_bucket(&inputs),
    }
}

fn build_next_step_bucket(inputs: &EvaluationInputs<'_>) -> EvaluationBucket<NextStepStatus> {
    if !inputs.candidates.handoff.is_empty() {
        return EvaluationBucket {
            status: NextStepStatus::ReviewRequired,
            summary:
                "Repo state changed relative to the recorded handoff; refresh the workspace-local handoff before closing."
                    .to_owned(),
            evidence: inputs
                .candidates
                .handoff
                .iter()
                .map(|candidate| candidate.summary.clone())
                .collect(),
        };
    }

    match inputs.execution_gates.attention_anchor.as_ref() {
        Some(anchor) if anchor.status == session_gates::ExecutionGateStatus::Blocked => {
            EvaluationBucket {
                status: NextStepStatus::ReviewRequired,
                summary:
                    "The first unfinished execution gate is blocked; resolve it or rewrite the gate set before clean wrap-up."
                        .to_owned(),
                evidence: execution_gate_evidence(inputs.execution_gates),
            }
        }
        Some(_) => EvaluationBucket {
            status: NextStepStatus::Continue,
            summary:
                "Execution gates are active; keep working through the first unfinished gate before clean wrap-up."
                    .to_owned(),
            evidence: execution_gate_evidence(inputs.execution_gates),
        },
        None => EvaluationBucket {
            status: NextStepStatus::NoChangeDetected,
            summary: "No deterministic CLI signal says the next step changed.".to_owned(),
            evidence: vec![
                "Current HEAD is already reflected in the workspace-local handoff or no repo-state delta was detected."
                    .to_owned(),
            ],
        },
    }
}

fn build_memory_bucket(inputs: &EvaluationInputs<'_>) -> EvaluationBucket<MemoryStatus> {
    if !inputs.candidates.memory.is_empty() {
        return EvaluationBucket {
            status: MemoryStatus::ReviewRequired,
            summary:
                "Structured memory entries touched this session may belong in a higher memory scope; review them before wrap-up."
                    .to_owned(),
            evidence: inputs
                .candidates
                .memory
                .iter()
                .map(|candidate| candidate.summary.clone())
                .collect(),
        };
    }
    if inputs.effective_memory.content.is_empty() {
        return EvaluationBucket {
            status: MemoryStatus::NoCliSignal,
            summary:
                "No effective CCD-local memory is recorded for this profile, linked project, active work stream, or workspace."
                    .to_owned(),
            evidence: vec![
                "Persist durable lessons deliberately in profile, project, work stream, or workspace memory when a pattern repeats."
                    .to_owned(),
            ],
        };
    }
    EvaluationBucket {
        status: MemoryStatus::Loaded,
        summary:
            "Effective CCD-local memory is loaded for this profile, linked project, active work stream, or workspace."
                .to_owned(),
        evidence: inputs.effective_memory.contributing_paths.clone(),
    }
}

fn build_wrap_up_bucket(inputs: &EvaluationInputs<'_>) -> EvaluationBucket<WrapUpStatus> {
    if inputs.escalation_view.blocking_count > 0 {
        return EvaluationBucket {
            status: WrapUpStatus::NeedsReview,
            summary:
                "Blocking escalation(s) are active; resolve them before using Radar wrap-up as a clean close."
                    .to_owned(),
            evidence: inputs
                .escalation_view
                .entries
                .iter()
                .filter(|e| matches!(e.kind, crate::state::escalation::EscalationKind::Blocking))
                .map(|e| format!("{}: {}", e.id, e.reason))
                .collect(),
        };
    }
    if inputs.repo_native_checks.failures > 0 {
        return EvaluationBucket {
            status: WrapUpStatus::NeedsReview,
            summary:
                "One or more repo-native checks failed; resolve them before using Radar wrap-up as a clean close."
                    .to_owned(),
            evidence: inputs
                .repo_native_checks
                .checks
                .iter()
                .filter(|check| check.severity == "error")
                .map(|check| format!("{}: {}", check.id, check.message))
                .collect(),
        };
    }
    if inputs.behavioral_drift.status == DriftAggregateStatus::NeedsRecalibration {
        return EvaluationBucket {
            status: WrapUpStatus::NeedsReview,
            summary:
                "Behavioral drift was detected; recalibrate the handoff or focus before using Radar wrap-up as a clean close."
                    .to_owned(),
            evidence: inputs.behavioral_drift.evidence.clone(),
        };
    }
    if inputs.git.is_some_and(|git| git.behind > 0) {
        return EvaluationBucket {
            status: WrapUpStatus::NeedsReview,
            summary:
                "The branch is behind its upstream; reconcile before using Radar wrap-up as a clean close."
                    .to_owned(),
            evidence: vec![format!(
                "Behind upstream by {} commit(s).",
                inputs.git.expect("checked above").behind
            )],
        };
    }
    if inputs.git.is_some_and(|git| !git.clean) {
        return EvaluationBucket {
            status: WrapUpStatus::ReadyToCommit,
            summary:
                "Local changes are present; the repo is ready for validation, review, and commit."
                    .to_owned(),
            evidence: wrap_up_evidence(inputs.git.expect("checked above")),
        };
    }
    if inputs.git.is_some_and(|git| git.ahead > 0) {
        return EvaluationBucket {
            status: WrapUpStatus::ReadyToPush,
            summary: "No local file changes remain and local commits are ready to push.".to_owned(),
            evidence: vec![format!(
                "Ahead of upstream by {} commit(s).",
                inputs.git.expect("checked above").ahead
            )],
        };
    }
    if inputs.git.is_none() {
        return EvaluationBucket {
            status: WrapUpStatus::Clean,
            summary:
                "Git checkout context is unavailable by design for directory-substrate workspaces."
                    .to_owned(),
            evidence: vec![
                "Wrap-up review is limited to continuity, memory, execution gates, and repo-native checks.".to_owned(),
            ],
        };
    }
    EvaluationBucket {
        status: WrapUpStatus::Clean,
        summary: "No local changes or unpublished commits were detected.".to_owned(),
        evidence: vec!["Worktree is clean and branch is in sync with upstream.".to_owned()],
    }
}

fn execution_gate_evidence(view: &session_gates::ExecutionGatesView) -> Vec<String> {
    let mut evidence = Vec::new();
    if let Some(seed) = &view.seeded_from {
        evidence.push(format!("Gate set source: {seed}."));
    }
    if let Some(anchor) = &view.attention_anchor {
        evidence.push(format!(
            "Current gate [{}/{} {}]: {}",
            anchor.index,
            view.total_count,
            anchor.status.as_str(),
            anchor.text
        ));
    }
    if view.unfinished_count > 0 {
        evidence.push(format!(
            "{} execution gate(s) remain unfinished.",
            view.unfinished_count
        ));
    }
    evidence
}

fn wrap_up_evidence(git: &GitState) -> Vec<String> {
    let mut evidence = Vec::new();

    if !git.staged_files.is_empty() {
        evidence.push(format!("Staged files: {}", git.staged_files.join(", ")));
    }
    if !git.unstaged_files.is_empty() {
        evidence.push(format!("Unstaged files: {}", git.unstaged_files.join(", ")));
    }
    if !git.untracked_files.is_empty() {
        evidence.push(format!(
            "Untracked files: {}",
            git.untracked_files.join(", ")
        ));
    }

    evidence
}