ccd-cli 1.0.0-beta.4

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

use anyhow::Result;
use clap::{ArgMatches, Command as ClapCommand};
use serde_json::Value;

use crate::commands::describe::CommandDescriptor;
use crate::mcp::protocol::Tool;
use crate::output::OutputFormat;
use crate::paths::state::StateLayout;
use crate::state::thread_transfer::{
    ThreadAttachmentExportContext, ThreadAttachmentExportResult, ThreadAttachmentImportPreview,
    ThreadDiagnostic, ValidatedAttachmentMetadata,
};

mod fixity;

pub(crate) struct HealthDiagnostic {
    pub check: &'static str,
    pub severity: &'static str,
    pub file: String,
    pub message: String,
    pub details: Option<serde_json::Value>,
}

pub(crate) trait Extension {
    fn name(&self) -> &'static str;

    fn command_groups(&self) -> &'static [&'static str];

    fn cli_command(&self) -> Option<ClapCommand> {
        None
    }

    fn dispatch_cli(
        &self,
        _subcommand_name: &str,
        _matches: &ArgMatches,
        _output: OutputFormat,
    ) -> Option<Result<ExitCode>> {
        None
    }

    fn mcp_tools(&self, _commands: &[CommandDescriptor]) -> Vec<Tool> {
        Vec::new()
    }

    fn dispatch_mcp(&self, _tool_name: &str, _args: &Value) -> Option<Result<Value>> {
        None
    }

    fn health_diagnostics(
        &self,
        _layout: &StateLayout,
        _repo_root: &Path,
        _locality_id: &str,
    ) -> Result<Vec<HealthDiagnostic>> {
        Ok(Vec::new())
    }
}

pub(crate) fn registered() -> Vec<&'static dyn Extension> {
    Vec::new()
}

pub(crate) trait ThreadAttachmentProvider: Sync {
    fn name(&self) -> &'static str;

    fn extension_type(&self) -> &'static str;

    fn export_thread_attachments(
        &self,
        context: &ThreadAttachmentExportContext<'_>,
    ) -> Result<ThreadAttachmentExportResult>;

    fn preview_thread_attachment_import(
        &self,
        index: usize,
        attachment: &ValidatedAttachmentMetadata,
    ) -> Option<ThreadAttachmentImportPreview>;
}

pub(crate) fn thread_attachment_providers() -> Vec<&'static dyn ThreadAttachmentProvider> {
    vec![&fixity::FIXITY]
}

pub(crate) fn thread_transfer_attachment_candidates(
    context: &ThreadAttachmentExportContext<'_>,
) -> (Vec<Value>, Vec<ThreadDiagnostic>) {
    let mut attachments = Vec::new();
    let mut diagnostics = Vec::new();

    for provider in thread_attachment_providers() {
        match provider.export_thread_attachments(context) {
            Ok(result) => {
                attachments.extend(result.attachments);
                diagnostics.extend(result.diagnostics);
            }
            Err(error) => diagnostics.push(ThreadDiagnostic {
                severity: "warning",
                code: "extension_unavailable",
                message: format!(
                    "Thread attachment provider `{}` was unavailable during export: {error}.",
                    provider.name()
                ),
            }),
        }
    }

    (attachments, diagnostics)
}

pub(crate) fn preview_thread_attachment_import(
    index: usize,
    attachment: &ValidatedAttachmentMetadata,
) -> Option<ThreadAttachmentImportPreview> {
    for provider in thread_attachment_providers() {
        if attachment.extension_type != provider.extension_type() {
            continue;
        }
        if let Some(preview) = provider.preview_thread_attachment_import(index, attachment) {
            return Some(preview);
        }
    }
    None
}

#[cfg(test)]
pub(crate) fn owned_command_groups() -> Vec<&'static str> {
    registered()
        .into_iter()
        .flat_map(|extension| extension.command_groups().iter().copied())
        .collect()
}

pub(crate) fn augment_clap(mut command: ClapCommand) -> ClapCommand {
    for extension in registered() {
        debug_assert!(!extension.name().is_empty());
        debug_assert!(!extension.command_groups().is_empty());
        if let Some(subcommand) = extension.cli_command() {
            command = command.subcommand(subcommand);
        }
    }
    command
}

pub(crate) fn dispatch_cli(
    subcommand_name: &str,
    matches: &ArgMatches,
    output: OutputFormat,
) -> Option<Result<ExitCode>> {
    for extension in registered() {
        if let Some(result) = extension.dispatch_cli(subcommand_name, matches, output) {
            return Some(result);
        }
    }
    None
}

pub(crate) fn build_mcp_tools(commands: &[CommandDescriptor]) -> Vec<Tool> {
    let mut tools = Vec::new();
    for extension in registered() {
        debug_assert!(!extension.name().is_empty());
        debug_assert!(!extension.command_groups().is_empty());
        tools.extend(extension.mcp_tools(commands));
    }
    tools
}

pub(crate) fn dispatch_mcp(tool_name: &str, args: &Value) -> Option<Result<Value>> {
    for extension in registered() {
        if let Some(report) = extension.dispatch_mcp(tool_name, args) {
            return Some(report);
        }
    }
    None
}

pub(crate) fn health_diagnostics(
    layout: &StateLayout,
    repo_root: &Path,
    locality_id: &str,
) -> Result<Vec<HealthDiagnostic>> {
    let mut all = Vec::new();
    for extension in registered() {
        all.extend(extension.health_diagnostics(layout, repo_root, locality_id)?);
    }
    Ok(all)
}

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

    #[test]
    fn no_cli_or_mcp_extensions_are_registered_in_the_kernel() {
        assert!(registered().is_empty());
        assert!(owned_command_groups().is_empty());
    }

    #[test]
    fn fixity_thread_attachment_provider_is_registered() {
        let providers = thread_attachment_providers();
        assert_eq!(providers.len(), 1);
        assert_eq!(providers[0].name(), "fixity");
        assert_eq!(providers[0].extension_type(), "fixity");
    }

    #[test]
    fn thread_attachment_provider_extension_types_are_unique() {
        let providers = thread_attachment_providers();
        let mut extension_types = std::collections::BTreeSet::new();
        for provider in providers {
            assert!(
                extension_types.insert(provider.extension_type()),
                "duplicate thread attachment provider extension_type `{}`",
                provider.extension_type()
            );
        }
    }

    #[test]
    fn extension_mcp_tools_are_empty_in_the_kernel() {
        let schema = crate::commands::describe::run();
        let tools = build_mcp_tools(&schema.commands);
        assert!(tools.is_empty());
    }

    #[test]
    fn extension_cli_commands_are_empty_in_the_kernel() {
        let command = augment_clap(clap::Command::new("ccd"));
        assert!(command.get_subcommands().next().is_none());
    }
}