grite 0.3.0

Git-backed issue tracker with CRDT merging, designed for AI coding agents
//! Command routing through daemon or local execution
//!
//! This module handles the decision of whether to route a command
//! through the daemon or execute it locally.

use libgrite_core::GriteError;
use libgrite_ipc::{IpcClient, IpcCommand, IpcRequest, IpcResponse};

use crate::cli::Cli;
use crate::commands::daemon::ensure_daemon_running;
use crate::context::{ExecutionMode, GriteContext};

/// Result of routing a command
pub enum RouteResult {
    /// Execute locally
    Local,
    /// Routed through daemon, response received
    DaemonResponse(IpcResponse),
    /// Blocked by daemon lock
    Blocked { pid: u32, expires_in_ms: u64 },
}

/// Route an IPC command through the daemon if available
///
/// Returns RouteResult::Local if the command should be executed locally.
/// Returns RouteResult::DaemonResponse if the daemon handled it.
/// Returns RouteResult::Blocked if the data dir is owned by another process.
///
/// When no daemon is running and --no-daemon is not set, this function
/// will attempt to auto-spawn the daemon before routing the command.
pub fn route_command(
    ctx: &GriteContext,
    cli: &Cli,
    command: IpcCommand,
) -> Result<RouteResult, GriteError> {
    match ctx.execution_mode(cli.no_daemon) {
        ExecutionMode::Local => {
            // Try to auto-spawn daemon unless --no-daemon is set
            if !cli.no_daemon {
                if let Ok(Some(endpoint)) = ensure_daemon_running(cli) {
                    // Daemon started, try to connect and route through it
                    if let Ok(client) = IpcClient::connect(&endpoint) {
                        let response = send_to_daemon(ctx, &client, command)?;
                        return Ok(RouteResult::DaemonResponse(response));
                    }
                }
            }
            // Fall back to local execution
            Ok(RouteResult::Local)
        }
        ExecutionMode::Daemon { client, endpoint: _ } => {
            let response = send_to_daemon(ctx, &client, command)?;
            Ok(RouteResult::DaemonResponse(response))
        }
        ExecutionMode::Blocked { lock } => {
            Ok(RouteResult::Blocked {
                pid: lock.pid,
                expires_in_ms: lock.time_remaining_ms(),
            })
        }
    }
}

/// Send a command to the daemon
fn send_to_daemon(
    ctx: &GriteContext,
    client: &IpcClient,
    command: IpcCommand,
) -> Result<IpcResponse, GriteError> {
    let request = IpcRequest::new(
        uuid::Uuid::new_v4().to_string(),
        ctx.repo_root().to_string_lossy().to_string(),
        ctx.actor_id.clone(),
        ctx.data_dir.to_string_lossy().to_string(),
        command,
    );

    client
        .send_with_retry(&request, 3)
        .map_err(|e| GriteError::Internal(format!("IPC error: {}", e)))
}

/// Check if a command should use daemon routing
///
/// Some commands (like init, actor management) should always run locally.
pub fn should_route_through_daemon(cmd: &crate::cli::Command) -> bool {
    use crate::cli::{Command, DbCommand};

    match cmd {
        // Always local - these manage the grite setup itself
        Command::Init { .. } => false,
        Command::Actor { .. } => false,

        // Daemon and lock commands are handled specially
        Command::Daemon { .. } => false,
        Command::Lock { .. } => false, // Locks require git ref access

        // Db commands: stats can route, check/verify are local-only
        Command::Db { cmd: db_cmd } => match db_cmd {
            DbCommand::Stats => true,
            DbCommand::Check { .. } => false, // Integrity check is local
            DbCommand::Verify { .. } => false, // Signature verify is local
        },

        // Doctor is local-only (health checks)
        Command::Doctor { .. } => false,

        // Context commands are local-only (need filesystem access)
        Command::Context { .. } => false,

        // Issue commands can be routed, except dep (needs cycle detection)
        Command::Issue { cmd } => !matches!(cmd, crate::cli::IssueCommand::Dep { .. }),
        Command::Export { .. } => true,
        Command::Rebuild { from_snapshot } => !from_snapshot, // Snapshot-based rebuild is local-only
        Command::Sync { .. } => true,
        Command::Snapshot { .. } => true,
    }
}

/// Convert a CLI command to an IPC command
///
/// Returns None for commands that should always run locally.
pub fn cli_to_ipc_command(cmd: &crate::cli::Command) -> Option<IpcCommand> {
    use crate::cli::{Command, ExportFormat};

    match cmd {
        Command::Issue { cmd: issue_cmd } => Some(issue_to_ipc(issue_cmd)),
        Command::Db { cmd: db_cmd } => Some(db_to_ipc(db_cmd)),
        Command::Export { format, since } => Some(IpcCommand::Export {
            format: match format {
                ExportFormat::Json => "json".to_string(),
                ExportFormat::Md => "md".to_string(),
            },
            since: since.clone(),
        }),
        Command::Rebuild { from_snapshot } if !from_snapshot => Some(IpcCommand::Rebuild),
        Command::Rebuild { .. } => None, // Snapshot-based rebuild handled locally
        Command::Sync { remote, pull, push } => Some(IpcCommand::Sync {
            remote: remote.clone(),
            pull: *pull,
            push: *push,
        }),
        Command::Snapshot { cmd: snap_cmd } => Some(snapshot_to_ipc(snap_cmd)),
        // These don't route through daemon
        Command::Init { .. } | Command::Actor { .. } | Command::Daemon { .. } | Command::Lock { .. } | Command::Doctor { .. } | Command::Context { .. } => None,
    }
}

fn issue_to_ipc(cmd: &crate::cli::IssueCommand) -> IpcCommand {
    use crate::cli::{IssueCommand, LabelCommand, AssigneeCommand, LinkCommand, AttachmentCommand};

    match cmd {
        IssueCommand::Create { title, body, label } => IpcCommand::IssueCreate {
            title: title.clone(),
            body: body.clone(),
            labels: label.clone(),
        },
        IssueCommand::List { state, label } => IpcCommand::IssueList {
            state: state.clone(),
            label: label.clone(),
        },
        IssueCommand::Show { id } => IpcCommand::IssueShow {
            issue_id: id.clone(),
        },
        IssueCommand::Update { id, title, body, .. } => IpcCommand::IssueUpdate {
            issue_id: id.clone(),
            title: title.clone(),
            body: body.clone(),
        },
        IssueCommand::Comment { id, body, .. } => IpcCommand::IssueComment {
            issue_id: id.clone(),
            body: body.clone(),
        },
        IssueCommand::Close { id, .. } => IpcCommand::IssueClose {
            issue_id: id.clone(),
        },
        IssueCommand::Reopen { id, .. } => IpcCommand::IssueReopen {
            issue_id: id.clone(),
        },
        IssueCommand::Label { cmd: label_cmd } => match label_cmd {
            LabelCommand::Add { id, label, .. } => IpcCommand::IssueLabel {
                issue_id: id.clone(),
                add: vec![label.clone()],
                remove: vec![],
            },
            LabelCommand::Remove { id, label, .. } => IpcCommand::IssueLabel {
                issue_id: id.clone(),
                add: vec![],
                remove: vec![label.clone()],
            },
        },
        IssueCommand::Assignee { cmd: assign_cmd } => match assign_cmd {
            AssigneeCommand::Add { id, user, .. } => IpcCommand::IssueAssign {
                issue_id: id.clone(),
                add: vec![user.clone()],
                remove: vec![],
            },
            AssigneeCommand::Remove { id, user, .. } => IpcCommand::IssueAssign {
                issue_id: id.clone(),
                add: vec![],
                remove: vec![user.clone()],
            },
        },
        IssueCommand::Link { cmd: link_cmd } => match link_cmd {
            LinkCommand::Add { id, url, note, .. } => IpcCommand::IssueLink {
                issue_id: id.clone(),
                url: url.clone(),
                note: note.clone(),
            },
        },
        IssueCommand::Attachment { cmd: attach_cmd } => match attach_cmd {
            AttachmentCommand::Add { id, name, sha256, mime, .. } => IpcCommand::IssueAttach {
                issue_id: id.clone(),
                file_path: format!("{}:{}:{}", name, sha256, mime),
            },
        },
        // Dep commands are local-only, should not reach here
        IssueCommand::Dep { .. } => {
            unreachable!("Dep commands should not be routed through daemon")
        }
    }
}

fn db_to_ipc(cmd: &crate::cli::DbCommand) -> IpcCommand {
    use crate::cli::DbCommand;

    match cmd {
        DbCommand::Stats => IpcCommand::DbStats,
        // Check and Verify are local-only, shouldn't reach here
        DbCommand::Check { .. } | DbCommand::Verify { .. } => {
            unreachable!("Check and Verify commands should not be routed through daemon")
        }
    }
}

fn snapshot_to_ipc(cmd: &crate::cli::SnapshotCommand) -> IpcCommand {
    use crate::cli::SnapshotCommand;

    match cmd {
        SnapshotCommand::Create => IpcCommand::SnapshotCreate,
        SnapshotCommand::List => IpcCommand::SnapshotList,
        SnapshotCommand::Gc { keep } => IpcCommand::SnapshotGc {
            keep: *keep as u32,
        },
    }
}