carryctx 0.0.3

Persistent project context for coding agents
use std::path::Path;

use crate::adapter::git::GitCli;
use crate::domain::checkpoint::{Checkpoint, CheckpointCorrection};
use crate::domain::git_snapshot::GitSnapshot;
use crate::error::CarryCtxError;
use crate::repository::{CheckpointRepository, EventRepository, NewEvent};

pub struct CreateCheckpointInput {
    pub project_id: String,
    pub task_id: String,
    pub session_id: Option<String>,
    pub agent_id: Option<String>,
    pub worktree_id: Option<String>,
    pub branch: Option<String>,
    pub head: Option<String>,
    pub done: Vec<String>,
    pub remaining: Vec<String>,
    pub blockers: Vec<String>,
    pub risks: Vec<String>,
    pub next_actions: Vec<String>,
    pub notes: Vec<String>,
    pub repo_path: Option<String>,
}

fn build_git_snapshot(
    git_cli: &GitCli,
    repo_path: Option<&str>,
) -> Result<Option<GitSnapshot>, CarryCtxError> {
    match repo_path {
        Some(path) => {
            let snapshot = git_cli.get_snapshot(Path::new(path))?;
            Ok(Some(snapshot))
        }
        None => Ok(None),
    }
}

pub fn create_checkpoint(
    checkpoint_repo: &dyn CheckpointRepository,
    event_repo: &dyn EventRepository,
    git_cli: &GitCli,
    input: &CreateCheckpointInput,
    now: &str,
) -> Result<Checkpoint, CarryCtxError> {
    let git_snapshot = build_git_snapshot(git_cli, input.repo_path.as_deref())?;

    let (
        head,
        branch,
        dirty,
        staged,
        modified,
        deleted,
        renamed,
        untracked,
        diff_files,
        diff_insertions,
        diff_deletions,
    ) = match &git_snapshot {
        Some(s) => (
            Some(s.head.clone()),
            s.branch.clone(),
            s.dirty,
            s.staged.clone(),
            s.modified.clone(),
            s.deleted.clone(),
            s.renamed
                .iter()
                .map(|r| crate::domain::checkpoint::RenamedFile {
                    from: r.from.clone(),
                    to: r.to.clone(),
                })
                .collect(),
            s.untracked.clone(),
            s.diff_stats.as_ref().map(|d| d.files),
            s.diff_stats.as_ref().map(|d| d.insertions),
            s.diff_stats.as_ref().map(|d| d.deletions),
        ),
        None => (
            input.head.clone(),
            input.branch.clone(),
            false,
            vec![],
            vec![],
            vec![],
            vec![],
            vec![],
            None,
            None,
            None,
        ),
    };

    let cp = Checkpoint {
        id: ulid::Ulid::new().to_string(),
        project_id: input.project_id.clone(),
        task_id: input.task_id.clone(),
        session_id: input.session_id.clone(),
        agent_id: input.agent_id.clone(),
        worktree_id: input.worktree_id.clone(),
        branch,
        head,
        dirty,
        staged_files: staged,
        modified_files: modified,
        deleted_files: deleted,
        renamed_files: renamed,
        untracked_files: untracked,
        diff_files,
        diff_insertions,
        diff_deletions,
        done: input.done.clone(),
        remaining: input.remaining.clone(),
        blockers: input.blockers.clone(),
        risks: input.risks.clone(),
        next_actions: input.next_actions.clone(),
        notes: input.notes.clone(),
        created_at: now.to_string(),
    };

    let saved = checkpoint_repo.create(&cp)?;

    event_repo.append(&NewEvent {
        id: ulid::Ulid::new().to_string(),
        project_id: input.project_id.clone(),
        event_type: "checkpoint.created".into(),
        actor_agent_id: input.agent_id.clone(),
        session_id: input.session_id.clone(),
        task_id: Some(input.task_id.clone()),
        payload: serde_json::json!({
            "checkpoint_id": saved.id,
            "task_id": saved.task_id,
            "dirty": saved.dirty,
            "done_count": saved.done.len(),
            "remaining_count": saved.remaining.len(),
        }),
        occurred_at: now.to_string(),
    })?;

    Ok(saved)
}

pub fn list_checkpoints(
    checkpoint_repo: &dyn CheckpointRepository,
    project_id: &str,
    task_id: Option<&str>,
) -> Result<Vec<Checkpoint>, CarryCtxError> {
    checkpoint_repo.list(project_id, task_id)
}

pub fn show_checkpoint(
    checkpoint_repo: &dyn CheckpointRepository,
    project_id: &str,
    checkpoint_id: &str,
) -> Result<Checkpoint, CarryCtxError> {
    checkpoint_repo
        .find_by_id(project_id, checkpoint_id)?
        .ok_or_else(|| {
            CarryCtxError::resource_not_found(format!("Checkpoint '{}' not found", checkpoint_id))
        })
}

pub struct CorrectCheckpointInput {
    pub project_id: String,
    pub checkpoint_id: String,
    pub done: Option<Vec<String>>,
    pub remaining: Option<Vec<String>>,
    pub blockers: Option<Vec<String>>,
    pub risks: Option<Vec<String>>,
    pub next_actions: Option<Vec<String>>,
    pub notes: Option<Vec<String>>,
}

pub fn correct_checkpoint(
    checkpoint_repo: &dyn CheckpointRepository,
    event_repo: &dyn EventRepository,
    input: &CorrectCheckpointInput,
    now: &str,
) -> Result<(), CarryCtxError> {
    let existing = checkpoint_repo
        .find_by_id(&input.project_id, &input.checkpoint_id)?
        .ok_or_else(|| {
            CarryCtxError::resource_not_found(format!(
                "Checkpoint '{}' not found",
                input.checkpoint_id
            ))
        })?;

    let correction = CheckpointCorrection {
        id: ulid::Ulid::new().to_string(),
        checkpoint_id: input.checkpoint_id.clone(),
        done: input.done.clone(),
        remaining: input.remaining.clone(),
        blockers: input.blockers.clone(),
        risks: input.risks.clone(),
        next_actions: input.next_actions.clone(),
        notes: input.notes.clone(),
        created_at: now.to_string(),
    };

    checkpoint_repo.correct(&correction)?;

    event_repo.append(&NewEvent {
        id: ulid::Ulid::new().to_string(),
        project_id: input.project_id.clone(),
        event_type: "checkpoint.corrected".into(),
        actor_agent_id: None,
        session_id: None,
        task_id: Some(existing.task_id.clone()),
        payload: serde_json::json!({
            "checkpoint_id": input.checkpoint_id,
            "correction_id": correction.id,
            "fields": {
                "done": input.done.is_some(),
                "remaining": input.remaining.is_some(),
                "blockers": input.blockers.is_some(),
                "risks": input.risks.is_some(),
                "next_actions": input.next_actions.is_some(),
                "notes": input.notes.is_some(),
            },
        }),
        occurred_at: now.to_string(),
    })?;

    Ok(())
}