carryctx 0.3.0

Persistent project context for coding agents
use crate::domain::preset::{PresetLockEntry, PresetLockfile, PresetManifest};
use crate::error::CarryCtxError;
use sha2::{Digest, Sha256};

use std::fs;
use std::path::Path;

pub struct PresetManager<'a> {
    pub repo_root: &'a Path,
}

impl<'a> PresetManager<'a> {
    pub fn new(repo_root: &'a Path) -> Self {
        Self { repo_root }
    }

    fn carryctx_dir(&self) -> std::path::PathBuf {
        self.repo_root.join(".carryctx")
    }

    fn presets_dir(&self) -> std::path::PathBuf {
        self.carryctx_dir().join("presets")
    }

    fn lockfile_path(&self) -> std::path::PathBuf {
        self.carryctx_dir().join("presets.lock")
    }

    pub fn read_lockfile(&self) -> Result<PresetLockfile, CarryCtxError> {
        let lock_path = self.lockfile_path();
        if !lock_path.exists() {
            return Ok(PresetLockfile::default());
        }

        let content = fs::read_to_string(&lock_path).map_err(|e| {
            CarryCtxError::database_error(format!("Failed to read presets.lock: {e}"))
        })?;

        toml::from_str(&content).map_err(|e| {
            CarryCtxError::database_error(format!("Failed to parse presets.lock: {e}"))
        })
    }

    pub fn write_lockfile(&self, lockfile: &PresetLockfile) -> Result<(), CarryCtxError> {
        let lock_path = self.lockfile_path();
        if let Some(parent) = lock_path.parent() {
            fs::create_dir_all(parent).map_err(|e| {
                CarryCtxError::database_error(format!("Failed to create .carryctx dir: {e}"))
            })?;
        }

        let content = toml::to_string_pretty(lockfile).map_err(|e| {
            CarryCtxError::database_error(format!("Failed to serialize presets.lock: {e}"))
        })?;

        let final_content = format!(
            "# This file is automatically generated by carryctx.\n# Do not edit manually.\n\n{}",
            content
        );

        fs::write(&lock_path, final_content).map_err(|e| {
            CarryCtxError::database_error(format!("Failed to write presets.lock: {e}"))
        })
    }

    pub fn install_preset(&self, source_path: &Path) -> Result<PresetLockEntry, CarryCtxError> {
        let content = fs::read_to_string(source_path).map_err(|e| {
            CarryCtxError::resource_not_found(format!(
                "Failed to read preset source {}: {}",
                source_path.display(),
                e
            ))
        })?;

        // Parse to validate
        let manifest: PresetManifest = serde_json::from_str(&content).map_err(|e| {
            CarryCtxError::invalid_arguments(format!("Failed to parse preset JSON: {e}"))
        })?;

        // Validate preset name: only alphanumeric, hyphens, underscores, single slashes
        if !manifest
            .name
            .chars()
            .all(|c| c.is_alphanumeric() || c == '-' || c == '_' || c == '/')
            || manifest.name.contains("//")
            || manifest.name.starts_with('/')
            || manifest.name.contains("..")
        {
            return Err(CarryCtxError::invalid_arguments(format!(
                "Invalid preset name '{}': must be alphanumeric with hyphens, underscores, and slashes only",
                manifest.name
            )));
        }

        // Hash content
        let mut hasher = Sha256::new();
        hasher.update(content.as_bytes());
        let hash = format!("sha256-{}", hex::encode(hasher.finalize()));

        // Create presets dir and target parent dir (in case name contains path separators)
        let presets_dir = self.presets_dir();
        let target_file = presets_dir.join(format!("{}.json", manifest.name));
        if let Some(parent) = target_file.parent() {
            fs::create_dir_all(parent).map_err(|e| {
                CarryCtxError::database_error(format!("Failed to create preset directory: {e}"))
            })?;
        }

        // Write JSON file to .carryctx/presets/<name>.json
        fs::write(&target_file, &content).map_err(|e| {
            CarryCtxError::database_error(format!(
                "Failed to save preset to {}: {}",
                target_file.display(),
                e
            ))
        })?;

        // Update lockfile
        let mut lockfile = self.read_lockfile()?;
        let entry = PresetLockEntry {
            version: manifest.version.clone(),
            source: source_path.to_string_lossy().to_string(),
            integrity: hash,
            permissions_granted: manifest.permissions.clone(),
        };

        lockfile
            .presets
            .insert(manifest.name.clone(), entry.clone());
        self.write_lockfile(&lockfile)?;

        Ok(entry)
    }

    pub fn activate_preset(&self, name: &str) -> Result<PresetLockEntry, CarryCtxError> {
        let lockfile = self.read_lockfile()?;
        let entry = lockfile.presets.get(name).cloned().ok_or_else(|| {
            CarryCtxError::resource_not_found(format!(
                "Preset '{}' is not installed in presets.lock",
                name
            ))
        })?;

        // For now, activation just checks integrity if the file exists
        let preset_path = self.presets_dir().join(format!("{}.json", name));
        if !preset_path.exists() {
            return Err(CarryCtxError::resource_not_found(format!(
                "Preset file missing: {}",
                preset_path.display()
            )));
        }

        let content = fs::read_to_string(&preset_path).map_err(|e| {
            CarryCtxError::database_error(format!(
                "Failed to read {}: {}",
                preset_path.display(),
                e
            ))
        })?;

        let mut hasher = Sha256::new();
        hasher.update(content.as_bytes());
        let current_hash = format!("sha256-{}", hex::encode(hasher.finalize()));

        if current_hash != entry.integrity {
            return Err(CarryCtxError::state_conflict(format!(
                "Integrity check failed for preset '{}'. Expected {} but got {}",
                name, entry.integrity, current_hash
            )));
        }

        // Output some message about permissions
        Ok(entry)
    }
}