//! Transactional render pipeline (ADR-025 / D29).
//!
//! Wraps the byte-stable [`render`](crate::render::render) function with a
//! mandatory `nickel typecheck` step before the bytes are written to disk.
//! When typecheck fails the operation aborts, the destination file is left
//! untouched, and the failure surfaces to the caller. This closes the
//! invariant that "every NCL file on disk parses against its schema."

use std::path::Path;
use std::process::Command;

use crate::render::{render, RenderError};
use crate::types::StateConfig;

/// Failures emitted while running the render → typecheck → write pipeline.
#[derive(Debug, thiserror::Error)]
pub enum PipelineError {
    /// The renderer itself failed.
    #[error("render: {0}")]
    Render(#[from] RenderError),

    /// `nickel typecheck` reported a problem on the rendered bytes.
    #[error("nickel typecheck rejected rendered '{path}': {stderr}")]
    TypecheckFailed { path: String, stderr: String },

    /// `nickel` binary was unavailable or the process could not be spawned.
    #[error("nickel binary unavailable: {0}")]
    NickelMissing(#[source] std::io::Error),

    /// Disk IO (writing the staged or final file) failed.
    #[error("io error in {context}: {source}")]
    Io {
        context: String,
        #[source]
        source: std::io::Error,
    },
}

/// Render `state` for `path` and write the bytes atomically once
/// `nickel typecheck` has accepted them.
///
/// Atomicity is implemented by writing to a sibling temp file first and
/// then renaming on success. When typecheck rejects the staged bytes the
/// temp file is removed and the destination is left untouched.
///
/// # Errors
///
/// - [`PipelineError::Render`] — propagated from [`crate::render::render`] for
///   unsupported paths or render failure.
/// - [`PipelineError::TypecheckFailed`] — when `nickel typecheck` reports a
///   contract or syntax error on the staged file.
/// - [`PipelineError::NickelMissing`] — the `nickel` binary cannot be spawned.
/// - [`PipelineError::Io`] — directory creation, write, or rename failed.
pub fn render_and_write(state: &StateConfig, path: &Path) -> Result<(), PipelineError> {
    let bytes = render(state, path)?;

    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent).map_err(|e| PipelineError::Io {
            context: format!("creating directory {}", parent.display()),
            source: e,
        })?;
    }

    let stage_path = staging_path(path);
    std::fs::write(&stage_path, bytes.as_bytes()).map_err(|e| PipelineError::Io {
        context: format!("writing staged {}", stage_path.display()),
        source: e,
    })?;

    let typecheck = Command::new("nickel")
        .arg("typecheck")
        .arg(&stage_path)
        .output();

    match typecheck {
        Ok(output) if output.status.success() => {
            std::fs::rename(&stage_path, path).map_err(|e| PipelineError::Io {
                context: format!("renaming {} -> {}", stage_path.display(), path.display()),
                source: e,
            })
        }
        Ok(output) => {
            let _ = std::fs::remove_file(&stage_path);
            Err(PipelineError::TypecheckFailed {
                path: path.display().to_string(),
                stderr: String::from_utf8_lossy(&output.stderr).trim().to_owned(),
            })
        }
        Err(e) => {
            let _ = std::fs::remove_file(&stage_path);
            Err(PipelineError::NickelMissing(e))
        }
    }
}

fn staging_path(target: &Path) -> std::path::PathBuf {
    let mut name = target
        .file_name()
        .map(std::ffi::OsString::from)
        .unwrap_or_default();
    name.push(".staged");
    target.with_file_name(name)
}
