use std::io::Write;
use std::path::Path;
use nichlink_build_method::{FaceView, declared_grafts, read_build_scope};
use serde_json::{Value, json};
use super::super::build_out_dir;
use super::json::{render_json, write_error};
use super::report::{cut_endpoint, cut_form};
pub(super) const OVERLAY_NOTE: &str = "static projection of the build's scope and declared cuts; the live effective tree is `Registry::dump_effective` (overlay_static + dump) inside a host that links both registries";
pub(super) fn overlay_report(
manifest: &Path,
faces: &[FaceView],
json_output: bool,
out: &mut dyn Write,
) -> Result<(), String> {
let out_dir = build_out_dir(manifest);
let scope = nichlink_build_method::build_output_is_current(manifest, &out_dir)
.then(|| read_build_scope(&out_dir).ok())
.flatten();
let declared = declared_grafts(manifest);
let plans = super::super::grafts::plan_rows(manifest, faces, declared.as_ref().ok())?;
let mut slots = Vec::new();
let mut pruned = Vec::new();
for face in faces {
let selected = scope.as_ref().map(|scope| {
scope.all
|| scope.selected_sources.contains(&face.source)
|| scope.selected_ids.contains(&face.id)
});
let kept = selected.map(|selected| {
selected
|| scope
.as_ref()
.is_some_and(|scope| scope.keeps(&face.module))
});
let replacement = declared
.as_ref()
.ok()
.and_then(|declared| {
declared
.cuts
.iter()
.find(|cut| cut.names_face(&face.path, Some(&face.module)))
})
.map(|cut| {
json!({
"cut": cut_endpoint(cut),
"graft": cut.graft,
"full": cut.full,
"line": cut.line,
"form": cut_form(cut),
})
});
let row = json!({
"path": face.path,
"node": face.id.to_string(),
"kind": face.kind,
"source": face.source,
"selected": selected,
"kept": kept,
"replacement": replacement,
});
if kept == Some(false) {
pruned.push(row.clone());
} else {
slots.push(row);
}
}
let entry = declared
.as_ref()
.ok()
.map(|declared| declared.entry.display().to_string());
let entry_error = declared.as_ref().err().cloned();
let scope_json = match &scope {
Some(scope) => json!({
"known": true,
"mode": scope.mode,
"all": scope.all,
"reason": scope.reason,
}),
None => json!({"known": false}),
};
if json_output {
let report = json!({
"schema": "nichlink.explain-overlay/1",
"kind": "static-projection",
"entry": entry,
"entry_error": entry_error,
"scope": scope_json,
"slots": slots,
"pruned": pruned,
"plans": plans,
"note": OVERLAY_NOTE,
});
writeln!(out, "{}", render_json(&report)).map_err(write_error)?;
return Ok(());
}
writeln!(
out,
"overlay preview (static projection of the build's scope and declared cuts)"
)
.map_err(write_error)?;
match (&entry, &entry_error) {
(Some(entry), _) => writeln!(out, " entry: {entry}").map_err(write_error)?,
(None, Some(error)) => {
writeln!(out, " entry: unreadable ({error})").map_err(write_error)?
}
(None, None) => {}
}
if let Some(scope) = &scope {
writeln!(
out,
" scope: mode={} all={} reason={}",
scope.mode,
scope.all,
scope.reason.as_deref().unwrap_or("<none>")
)
.map_err(write_error)?;
} else {
writeln!(
out,
" scope: unknown; run `nichlink check` to publish source_scope.tsv"
)
.map_err(write_error)?;
}
writeln!(out, " slots ({}):", slots.len()).map_err(write_error)?;
for slot in &slots {
let replacement = slot["replacement"]
.as_object()
.map(|_| {
format!(
" <- graft={} full={} (line {})",
slot["replacement"]["graft"].as_str().unwrap_or("<unknown>"),
slot["replacement"]["full"],
slot["replacement"]["line"]
)
})
.unwrap_or_default();
writeln!(
out,
" {} kind={}{replacement}",
slot["path"].as_str().unwrap_or("<unknown>"),
slot["kind"].as_str().unwrap_or("<unknown>")
)
.map_err(write_error)?;
}
writeln!(out, " pruned ({}):", pruned.len()).map_err(write_error)?;
for slot in &pruned {
writeln!(
out,
" {} kind={}",
slot["path"].as_str().unwrap_or("<unknown>"),
slot["kind"].as_str().unwrap_or("<unknown>")
)
.map_err(write_error)?;
}
writeln!(out, " plan records ({}):", plans.len()).map_err(write_error)?;
for plan in &plans {
match plan.get("error").and_then(Value::as_str) {
Some(error) => writeln!(
out,
" {}: unreadable ({error})",
plan["selector"].as_str().unwrap_or("<unknown>")
)
.map_err(write_error)?,
None => writeln!(
out,
" selector={} target={} graft={} full={} declared={}",
plan["selector"].as_str().unwrap_or("<unknown>"),
plan["target_path"].as_str().unwrap_or("<unknown>"),
plan["graft"].as_str().unwrap_or("<unknown>"),
plan["full"],
plan["declared"]
)
.map_err(write_error)?,
}
}
Ok(())
}