use anyhow::Context;
use codex_git_utils::GitBaselineDiff;
use codex_git_utils::diff_since_latest_init;
use codex_git_utils::ensure_git_baseline_repository;
use codex_git_utils::reset_git_repository;
use std::path::Path;

/// Prepares the memory directory for git-baseline diffing.
///
/// This keeps an existing usable `.git/` baseline intact. It initializes a new git baseline when the
/// metadata is missing or unusable, and removes any stale generated `phase2_workspace_diff.md` file
/// so that the next diff does not include a previous prompt artifact.
pub async fn prepare_memory_workspace(root: &Path) -> anyhow::Result<()> {
    tokio::fs::create_dir_all(root)
        .await
        .with_context(|| format!("create memory workspace {}", root.display()))?;
    remove_workspace_diff(root).await?;
    ensure_git_baseline_repository(root).await?;
    Ok(())
}

/// Returns the current workspace diff after removing any stale generated diff artifact.
///
/// The removed file is only `phase2_workspace_diff.md`; memory artifacts and `.git/` metadata are
/// left intact.
pub async fn memory_workspace_diff(root: &Path) -> anyhow::Result<GitBaselineDiff> {
    remove_workspace_diff(root).await?;
    diff_since_latest_init(root).await
}

/// Writes `phase2_workspace_diff.md` with a bounded git-style diff from the current baseline.
pub async fn write_workspace_diff(root: &Path, diff: &GitBaselineDiff) -> anyhow::Result<()> {
    let path = root.join(crate::workspace_diff::FILENAME);
    tokio::fs::write(&path, render_workspace_diff_file(diff))
        .await
        .with_context(|| format!("write memory workspace diff file {}", path.display()))
}

/// Marks the current memory root as the new baseline.
///
/// The generated diff file is removed before resetting the baseline so deleted memory content is
/// not retained in the prompt artifact or in unreachable git objects.
pub async fn reset_memory_workspace_baseline(root: &Path) -> anyhow::Result<()> {
    remove_workspace_diff(root).await?;
    reset_git_repository(root).await
}

/// Removes the generated `phase2_workspace_diff.md` prompt artifact.
///
/// This does not remove `.git/`, reset the baseline, or delete memory content. It is used before
/// diffing and before baseline reset so the generated diff file itself is not treated as memory
/// workspace input.
pub(super) async fn remove_workspace_diff(root: &Path) -> anyhow::Result<()> {
    let path = root.join(crate::workspace_diff::FILENAME);
    match tokio::fs::remove_file(&path).await {
        Ok(()) => Ok(()),
        Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
        Err(err) => Err(err)
            .with_context(|| format!("remove memory workspace diff file {}", path.display())),
    }
}

fn render_workspace_diff_file(diff: &GitBaselineDiff) -> String {
    let mut rendered = String::from(
        "# Memory Workspace Diff\n\n\
         Generated by Codex before Phase 2 memory consolidation. Read this file first and do not edit it.\n\n\
         ## Status\n",
    );

    if !diff.has_changes() {
        rendered.push_str("- none\n");
        return rendered;
    }

    for change in &diff.changes {
        rendered.push_str(&format!("- {} {}\n", change.status.label(), change.path));
    }
    rendered.push_str("\n## Diff\n\n```diff\n");
    append_bounded_diff(&mut rendered, &diff.unified_diff);
    rendered.push_str("```\n");
    rendered
}

fn append_bounded_diff(rendered: &mut String, diff: &str) {
    if diff.len() <= crate::workspace_diff::MAX_BYTES {
        rendered.push_str(diff);
        if !diff.ends_with('\n') {
            rendered.push('\n');
        }
        return;
    }

    let boundary = previous_char_boundary(diff, crate::workspace_diff::MAX_BYTES);
    rendered.push_str(&diff[..boundary]);
    if !rendered.ends_with('\n') {
        rendered.push('\n');
    }
    rendered.push_str(&format!(
        "\n[workspace diff truncated at {} bytes]\n",
        crate::workspace_diff::MAX_BYTES
    ));
}

fn previous_char_boundary(value: &str, max_bytes: usize) -> usize {
    if max_bytes >= value.len() {
        return value.len();
    }
    let mut index = max_bytes;
    while !value.is_char_boundary(index) {
        index -= 1;
    }
    index
}

#[cfg(test)]
#[path = "workspace_tests.rs"]
mod tests;
