ccd-cli 1.0.0-beta.3

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

use anyhow::{Context, Result};
use serde_json::Value;

use crate::commands;
use crate::output::{self, OutputFormat};
use crate::paths;
use crate::repo;
use crate::state;

pub(crate) fn preflight(args: crate::PreflightArgs, format: OutputFormat) -> Result<ExitCode> {
    let repo_path = paths::cli::resolve(&args.path)?;
    let report = commands::preflight::run(&repo_path, args.profile.as_deref())?;
    output::render_report(format, &report)
}

pub(crate) fn start(args: crate::StartArgs, format: OutputFormat) -> Result<ExitCode> {
    if args.check && args.fields.is_some() {
        anyhow::bail!(
            "--fields is not supported with `start --check`; the check report already includes all readiness data without field filtering"
        );
    }
    if args.check && args.memory_depth.is_some() {
        anyhow::bail!("--memory-depth is not supported with `start --check`");
    }
    if args.check && args.activate {
        anyhow::bail!("--activate is not supported with `start --check`");
    }
    if args.fields.is_some() && format != OutputFormat::Json {
        anyhow::bail!("--fields requires --output json");
    }
    if args.memory_depth.is_some() && format != OutputFormat::Json {
        anyhow::bail!("--memory-depth requires --output json");
    }
    if let Some(fields) = &args.fields {
        output::validate_start_fields(fields)?;
    }
    let repo_path = paths::cli::resolve(&args.path)?;
    if args.check {
        let report = commands::start::run_check(&repo_path, args.profile.as_deref(), args.refresh)?;
        return output::render_report(format, &report);
    }

    let use_metadata = args.memory_depth == Some(output::MemoryDepth::Metadata);
    let build_start_report = || -> Result<_> {
        if let Some(fields) = &args.fields {
            if !output::needs_source_rendering(fields) {
                commands::start::run_compiled_only(
                    &repo_path,
                    args.profile.as_deref(),
                    args.refresh,
                )
            } else {
                commands::start::run(&repo_path, args.profile.as_deref(), args.refresh)
            }
        } else {
            commands::start::run(&repo_path, args.profile.as_deref(), args.refresh)
        }
    };
    let mut report = build_start_report()?;

    if args.activate {
        let locality_id = repo::marker::load(&repo_path)
            .ok()
            .flatten()
            .map(|marker| marker.locality_id);
        let start_options = state::session::SessionStartOptions {
            mode: args.mode.map(crate::SessionModeArg::into_state_mode),
            lifecycle: state::session::SessionLifecycle::Interactive,
            owner_kind: None,
            actor_id: None,
            supervisor_id: None,
            lease_ttl_secs: None,
        };
        let activation = state::session::start(
            &repo_path,
            args.profile.as_deref(),
            locality_id.as_deref(),
            start_options,
        )?;
        report = build_start_report()?.with_activation(activation);
    }

    if format == OutputFormat::Json {
        let mut value = serde_json::to_value(&report)?;
        if use_metadata {
            value = output::strip_memory_content(value);
        }
        if let Some(fields) = &args.fields {
            value = filter_requested_start_fields(value, fields)?;
        } else {
            value = output::apply_default_json_contract(value, output::DefaultJsonContract::Start)?;
        }
        println!("{}", serde_json::to_string_pretty(&value)?);
        Ok(ExitCode::SUCCESS)
    } else {
        output::render_report(format, &report)
    }
}

fn filter_requested_start_fields(mut value: Value, fields: &[String]) -> Result<Value> {
    let obj = value
        .as_object_mut()
        .context("start report is not a JSON object")?;
    let keys_to_remove: Vec<String> = obj
        .keys()
        .filter(|key| !fields.iter().any(|field| field == *key))
        .cloned()
        .collect();
    for key in keys_to_remove {
        obj.remove(&key);
    }
    Ok(value)
}

pub(crate) fn status(args: crate::StatusArgs, format: OutputFormat) -> Result<ExitCode> {
    if args.fields.is_some() && format != OutputFormat::Json {
        anyhow::bail!("--fields requires --output json");
    }
    if let Some(fields) = &args.fields {
        output::validate_fields(&output::STATUS_FIELD_FILTER_SPEC, fields)?;
    }
    let repo_path = paths::cli::resolve(&args.path)?;
    let report = commands::status::run(&repo_path, args.profile.as_deref())?;
    match &args.fields {
        Some(fields) => {
            output::render_report_with_fields(report, fields, &output::STATUS_FIELD_FILTER_SPEC)
        }
        None if format == OutputFormat::Json => {
            let value = serde_json::to_value(&report)?;
            let filtered =
                output::apply_default_json_contract(value, output::DefaultJsonContract::Status)?;
            println!("{}", serde_json::to_string_pretty(&filtered)?);
            Ok(ExitCode::SUCCESS)
        }
        None => output::render_report(format, &report),
    }
}

pub(crate) fn session(command: crate::SessionCommand, format: OutputFormat) -> Result<ExitCode> {
    match command {
        crate::SessionCommand::Open(args) => {
            let repo_path = paths::cli::resolve(&args.path)?;
            let report = commands::session_open::run(
                &repo_path,
                args.profile.as_deref(),
                args.worktree.as_deref(),
                args.branch.as_deref(),
                args.from_ref.as_deref(),
                args.pod.as_deref(),
            )?;
            output::render_report(format, &report)
        }
    }
}

pub(crate) fn session_state(
    command: crate::SessionStateCommand,
    format: OutputFormat,
) -> Result<ExitCode> {
    match command {
        crate::SessionStateCommand::Start(args) => {
            let repo_path = paths::cli::resolve(&args.base.path)?;
            let locality_id = repo::marker::load(&repo_path)
                .ok()
                .flatten()
                .map(|marker| marker.locality_id);
            let start_options = state::session::SessionStartOptions {
                mode: args.mode.map(crate::SessionModeArg::into_state_mode),
                lifecycle: args.lifecycle.into_state_lifecycle(),
                owner_kind: args
                    .owner_kind
                    .map(crate::AutonomousOwnerKindArg::into_state_owner_kind),
                actor_id: args.actor_id,
                supervisor_id: args.supervisor_id,
                lease_ttl_secs: args.lease_seconds,
            };
            let report = state::session::start(
                &repo_path,
                args.base.profile.as_deref(),
                locality_id.as_deref(),
                start_options,
            )?;
            output::render_report(format, &report)
        }
        crate::SessionStateCommand::Heartbeat(args) => {
            let repo_path = paths::cli::resolve(&args.base.path)?;
            let report = state::session::heartbeat(
                &repo_path,
                args.base.profile.as_deref(),
                state::session::SessionHeartbeatOptions {
                    actor_id: args.actor_id,
                    activity: args.activity,
                },
            )?;
            output::render_report(format, &report)
        }
        crate::SessionStateCommand::Clear(args) => {
            let repo_path = paths::cli::resolve(&args.base.path)?;
            let locality_id = repo::marker::load(&repo_path)
                .ok()
                .flatten()
                .map(|marker| marker.locality_id);
            let report = state::session::clear(
                &repo_path,
                args.base.profile.as_deref(),
                locality_id.as_deref(),
                state::session::SessionClearOptions {
                    actor_id: args.actor_id,
                    reason: args.reason,
                },
            )?;
            output::render_report(format, &report)
        }
        crate::SessionStateCommand::Takeover(args) => {
            let repo_path = paths::cli::resolve(&args.base.path)?;
            let locality_id = repo::marker::load(&repo_path)
                .ok()
                .flatten()
                .map(|marker| marker.locality_id);
            let report = state::session::takeover(
                &repo_path,
                args.base.profile.as_deref(),
                locality_id.as_deref(),
                state::session::SessionTakeoverOptions {
                    actor_id: args.actor_id,
                    supervisor_id: args.supervisor_id,
                    reason: args.reason,
                },
            )?;
            output::render_report(format, &report)
        }
        crate::SessionStateCommand::Gates { command } => session_gates(command, format),
    }
}

fn session_gates(command: crate::SessionGateCommand, format: OutputFormat) -> Result<ExitCode> {
    match command {
        crate::SessionGateCommand::List(args) => {
            let repo_path = paths::cli::resolve(&args.path)?;
            let report = state::session_gates::list(&repo_path, args.profile.as_deref())?;
            output::render_report(format, &report)
        }
        crate::SessionGateCommand::Replace(args) => {
            let repo_path = paths::cli::resolve(&args.base.path)?;
            let report = state::session_gates::replace(
                &repo_path,
                args.base.profile.as_deref(),
                args.gates,
                args.protected_write.into_state_options(),
            )?;
            output::render_report(format, &report)
        }
        crate::SessionGateCommand::Seed(args) => {
            let repo_path = paths::cli::resolve(&args.base.path)?;
            let report = state::session_gates::seed(
                &repo_path,
                args.base.profile.as_deref(),
                args.from.into_state_source(),
                args.protected_write.into_state_options(),
            )?;
            output::render_report(format, &report)
        }
        crate::SessionGateCommand::SetStatus(args) => {
            let repo_path = paths::cli::resolve(&args.base.path)?;
            let report = state::session_gates::set_status(
                &repo_path,
                args.base.profile.as_deref(),
                args.index,
                args.status.into_status(),
                args.protected_write.into_state_options(),
            )?;
            output::render_report(format, &report)
        }
        crate::SessionGateCommand::Advance(args) => {
            let repo_path = paths::cli::resolve(&args.base.path)?;
            let report = state::session_gates::advance(
                &repo_path,
                args.base.profile.as_deref(),
                args.protected_write.into_state_options(),
            )?;
            output::render_report(format, &report)
        }
        crate::SessionGateCommand::Clear(args) => {
            let repo_path = paths::cli::resolve(&args.base.path)?;
            let report = state::session_gates::clear(
                &repo_path,
                args.base.profile.as_deref(),
                args.protected_write.into_state_options(),
            )?;
            output::render_report(format, &report)
        }
    }
}