ccd-cli 1.0.0-beta.2

Bootstrap and validate Continuous Context Development repositories
use std::path::Path;
use std::process::ExitCode;

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

use crate::output::CommandReport;
use crate::paths::state::StateLayout;
use crate::profile;
use crate::repo::marker as repo_marker;
use crate::state::compiled as compiled_state;
use crate::state::escalation as escalation_state;
use crate::state::machine_presence;
use crate::state::pod_identity;
use crate::state::policy_projection;
use crate::state::runtime::{self as runtime_state, LoadedRuntimeState, RuntimeTextSurface};
use crate::state::session as session_state;
use crate::state::session_gates;

#[derive(Serialize)]
pub struct RuntimeStateExportReport {
    command: &'static str,
    ok: bool,
    path: String,
    profile: String,
    project_id: String,
    locality_id: String,
    export: RuntimeStateExportView,
}

#[derive(Serialize)]
struct RuntimeStateExportView {
    kind: &'static str,
    load_mode: &'static str,
    derived: bool,
    disposable: bool,
    notice: &'static str,
    native_workspace_runtime: NativeCloneRuntimeView,
    native_clone_runtime: NativeCloneRuntimeView,
    sources: RuntimeSourceSurfacesView,
    execution_gates: session_gates::ExecutionGatesView,
    recovery: runtime_state::RuntimeRecoveryView,
    policy_projection: policy_projection::PolicyProjectionView,
    machine_presence: machine_presence::MachinePresenceView,
    execution_context: machine_presence::MachineExecutionContextView,
    takeover_preconditions: machine_presence::MachineTakeoverPreconditionsView,
    #[serde(skip_serializing_if = "Option::is_none")]
    projection_target: Option<&'static str>,
    #[serde(skip_serializing_if = "Option::is_none")]
    projection_format: Option<&'static str>,
    #[serde(skip_serializing_if = "Option::is_none")]
    projection: Option<compiled_state::CompiledStateStore>,
    #[serde(skip_serializing_if = "Option::is_none")]
    symbolic_projection: Option<compiled_state::SymbolicProjectionView>,
    #[serde(skip_serializing_if = "Option::is_none")]
    bundle_projection: Option<compiled_state::BundleProjectionView>,
    #[serde(skip_serializing_if = "Option::is_none")]
    state: Option<runtime_state::RuntimeState>,
}

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

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

#[derive(Serialize)]
struct RuntimeSourceSurfacesView {
    profile_memory: RuntimeSourceSurfaceView,
    project_memory: RuntimeSourceSurfaceView,
    locality_memory: RuntimeSourceSurfaceView,
    pod_memory: RuntimeSourceSurfaceView,
    work_stream_memory: RuntimeSourceSurfaceView,
    branch_memory: RuntimeSourceSurfaceView,
    workspace_memory: RuntimeSourceSurfaceView,
    clone_memory: RuntimeSourceSurfaceView,
    execution_gates: RuntimeSourceSurfaceView,
    handoff: RuntimeSourceSurfaceView,
    handoff_export: HandoffExportView,
    checkpoint: RuntimeSourceSurfaceView,
    working_buffer: RuntimeSourceSurfaceView,
}

#[derive(Serialize)]
struct RuntimeSourceSurfaceView {
    kind: &'static str,
    path: String,
    status: &'static str,
    source_role: &'static str,
    #[serde(skip_serializing_if = "Option::is_none")]
    migrated_from: Option<String>,
}

impl CommandReport for RuntimeStateExportReport {
    fn exit_code(&self) -> ExitCode {
        ExitCode::SUCCESS
    }

    fn render_text(&self) {
        match (self.export.projection_target, self.export.projection_format) {
            (Some(target), Some(format)) => println!(
                "Derived `{}` `{}` projection preview for profile `{}` in linked project `{}`.",
                format, target, self.profile, self.project_id
            ),
            (Some(target), None) => println!(
                "Derived `{}` projection preview for profile `{}` in linked project `{}`.",
                target, self.profile, self.project_id
            ),
            (None, _) => println!(
                "Derived runtime-state export for profile `{}` in linked project `{}`.",
                self.profile, self.project_id
            ),
        };
        println!("{}", self.export.notice);
        match &self.export.bundle_projection {
            Some(bundle) => print!("{}", bundle.markdown),
            None => println!(
                "{}",
                serde_json::to_string_pretty(&self.export)
                    .expect("runtime-state export serialization")
            ),
        }
    }
}

pub fn run(
    repo_root: &Path,
    explicit_profile: Option<&str>,
    projection_target: Option<compiled_state::ProjectionTarget>,
    projection_format: Option<compiled_state::ProjectionFormat>,
) -> Result<RuntimeStateExportReport> {
    let profile = profile::resolve(explicit_profile)?;
    let layout = StateLayout::resolve(repo_root, profile.clone())?;
    ensure_profile_exists(&layout, repo_root)?;
    let locality_id = ensure_repo_linked(repo_root)?;
    let runtime = runtime_state::load_runtime_state(repo_root, &layout, &locality_id)?;
    let active_pod_identity = pod_identity::resolve_active_identity(&layout)?;
    let policy_sources = policy_projection::read_policy_sources(
        &layout,
        &locality_id,
        active_pod_identity.as_ref(),
    )?;
    let session_context = policy_projection::load_session_context(&layout)?;
    let escalation_entries = escalation_state::load_for_layout(&layout)?;
    let escalation_view = escalation_state::build_view(&layout, &escalation_entries);
    let tracked_session = session_state::load_for_layout(&layout)?;
    let now_epoch_s = session_state::now_epoch_s()?;
    let session_lifecycle = tracked_session
        .as_ref()
        .map(|state| session_state::lifecycle_projection(state, now_epoch_s, None, None));
    let machine_identity = pod_identity::resolve_machine_identity_view(&layout)?;
    let machine_presence = machine_presence::resolve_machine_presence_view(&layout)?;
    let execution_context = machine_presence::build_execution_context_view(
        &layout,
        Some(&locality_id),
        &machine_identity,
        &machine_presence,
        session_lifecycle.as_ref(),
    );
    let takeover_preconditions = machine_presence::build_takeover_preconditions_view(
        &execution_context,
        session_lifecycle.as_ref(),
        escalation_view.blocking_count > 0,
        escalation_view.blocking_count,
        true,
    );
    if projection_target.is_none() && projection_format.is_some() {
        bail!("`--projection-format` requires `--projection-target`");
    }
    let projection_format = projection_target
        .map(|_| projection_format.unwrap_or(compiled_state::ProjectionFormat::Narrative));
    let projection = match (projection_target, projection_format) {
        (Some(target), Some(compiled_state::ProjectionFormat::Narrative)) => Some((
            target,
            compiled_state::preview_for_target_with_cache(&layout, &runtime, target)?.value,
        )),
        _ => None,
    };
    let symbolic_projection = match (projection_target, projection_format) {
        (Some(target), Some(compiled_state::ProjectionFormat::Symbolic)) => Some((
            target,
            compiled_state::preview_symbolic_for_target_with_cache(&layout, &runtime, target)?
                .value,
        )),
        _ => None,
    };
    let bundle_projection = match (projection_target, projection_format) {
        (Some(target), Some(compiled_state::ProjectionFormat::Bundle)) => Some((
            target,
            compiled_state::preview_bundle_for_target_with_cache(&layout, &runtime, target)?.value,
        )),
        _ => None,
    };
    let projection_kind =
        if projection.is_some() || symbolic_projection.is_some() || bundle_projection.is_some() {
            "compiled_projection_preview"
        } else {
            "debug_dump"
        };
    let projection_notice = if projection.is_some()
        || symbolic_projection.is_some()
        || bundle_projection.is_some()
    {
        "Derived projection preview only. This output is disposable and must not be treated as authored project truth."
    } else {
        "Derived debug export only. This dump is disposable and must not be treated as authored project truth."
    };
    let projection_target_label = projection_target.map(compiled_state::ProjectionTarget::as_str);
    let projection_format_label = projection_format.map(compiled_state::ProjectionFormat::as_str);
    let compiled_projection = projection
        .as_ref()
        .map(|(_, projection)| projection.clone());
    let symbolic_projection = symbolic_projection
        .as_ref()
        .map(|(_, projection)| projection.clone());
    let bundle_projection = bundle_projection
        .as_ref()
        .map(|(_, projection)| projection.clone());
    let native_clone_runtime_status = match runtime_state::native_handoff_exists(&layout)? {
        true => "loaded",
        false => "missing",
    };
    let sources = build_sources_view(&layout, &runtime);
    let recovery = runtime_state::recovery_view(&runtime.recovery);
    let policy_projection =
        policy_projection::build_view(&policy_sources, session_context, &escalation_view);
    let runtime_state = if compiled_projection.is_none()
        && symbolic_projection.is_none()
        && bundle_projection.is_none()
    {
        Some(runtime.state)
    } else {
        None
    };

    Ok(RuntimeStateExportReport {
        command: "runtime-state-export",
        ok: true,
        path: repo_root.display().to_string(),
        profile: profile.to_string(),
        project_id: locality_id.clone(),
        locality_id,
        export: RuntimeStateExportView {
            kind: projection_kind,
            load_mode: "native_canonical",
            derived: true,
            disposable: true,
            notice: projection_notice,
            native_workspace_runtime: NativeCloneRuntimeView {
                path: layout.state_db_path().display().to_string(),
                status: native_clone_runtime_status,
            },
            native_clone_runtime: NativeCloneRuntimeView {
                path: layout.state_db_path().display().to_string(),
                status: native_clone_runtime_status,
            },
            sources,
            execution_gates: runtime.execution_gates.view.clone(),
            recovery,
            policy_projection,
            machine_presence,
            execution_context,
            takeover_preconditions,
            projection_target: projection_target_label,
            projection_format: projection_format_label,
            projection: compiled_projection,
            symbolic_projection,
            bundle_projection,
            state: runtime_state,
        },
    })
}

fn build_sources_view(
    layout: &StateLayout,
    runtime: &LoadedRuntimeState,
) -> RuntimeSourceSurfacesView {
    let export_path = layout.handoff_path();
    let export_status = if export_path.is_file() {
        "present"
    } else {
        "absent"
    };
    // `project_*`/`workspace_*` remain as compatibility aliases for the
    // locality/clone terminology while downstream consumers finish the naming
    // migration.
    RuntimeSourceSurfacesView {
        profile_memory: surface_view(&runtime.sources.profile_memory),
        project_memory: surface_view(&runtime.sources.locality_memory),
        locality_memory: surface_view(&runtime.sources.locality_memory),
        pod_memory: surface_view(&runtime.sources.pod_memory),
        work_stream_memory: surface_view(&runtime.sources.branch_memory),
        branch_memory: surface_view(&runtime.sources.branch_memory),
        workspace_memory: surface_view(&runtime.sources.clone_memory),
        clone_memory: surface_view(&runtime.sources.clone_memory),
        execution_gates: surface_view(&runtime.sources.execution_gates),
        handoff: surface_view(&runtime.sources.handoff),
        handoff_export: HandoffExportView {
            path: export_path.display().to_string(),
            status: export_status,
        },
        checkpoint: recovery_surface_view(&runtime.recovery.sources.checkpoint),
        working_buffer: recovery_surface_view(&runtime.recovery.sources.working_buffer),
    }
}

fn surface_view(surface: &RuntimeTextSurface) -> RuntimeSourceSurfaceView {
    RuntimeSourceSurfaceView {
        kind: surface.kind,
        path: surface.path.display().to_string(),
        status: surface.status.as_str(),
        source_role: if surface.status.is_loaded_native() {
            "native_runtime_projection"
        } else {
            "authored_adapter"
        },
        migrated_from: surface.migrated_from.clone(),
    }
}

fn recovery_surface_view(
    surface: &runtime_state::RuntimeRecoverySurface,
) -> RuntimeSourceSurfaceView {
    RuntimeSourceSurfaceView {
        kind: surface.kind,
        path: surface.path.display().to_string(),
        status: surface.status,
        source_role: "workspace_local_recovery",
        migrated_from: None,
    }
}

fn ensure_profile_exists(layout: &StateLayout, repo_root: &Path) -> 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 --path {}` before using `ccd runtime-state export`",
        layout.profile(),
        profile_root.display(),
        repo_root.display()
    )
}

fn ensure_repo_linked(repo_root: &Path) -> Result<String> {
    let Some(marker) = repo_marker::load(repo_root)? else {
        bail!(
            "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()
        )
    };

    Ok(marker.locality_id)
}