//! XFA flatten traceability (env-gated, default OFF).
//!
//! Emits a per-document JSON trace when `XFA_FLATTEN_TRACE` is set, capturing
//! parse / bind / script / layout / paint / writer-stage counts so it is
//! traceable **where** XFA content disappears during flatten. Schema:
//! `benchmarks/runs/xfa_enterprise_plan/dynamic_reflow_traceability/XFA_FLATTEN_TRACE_SCHEMA.md`.
//!
//! Cost contract: zero work unless `XFA_FLATTEN_TRACE` is set — [`enabled`]
//! short-circuits before any counting. This is a debugging aid, not a
//! production hot-path feature. Output goes to the file named by
//! `XFA_FLATTEN_TRACE_PATH`, or, if that is unset, a one-line summary to stderr.

use std::fmt::Write as _;

use xfa_layout_engine::form::{DrawContent, FormNodeId, FormNodeType, FormTree, Presence};
use xfa_layout_engine::layout::{LayoutContent, LayoutDom, LayoutNode};

use crate::dynamic::DynamicScriptOutcome;
use crate::render_bridge::PageOverlay;

/// True when `XFA_FLATTEN_TRACE` is set to a non-empty value other than `"0"`.
pub(crate) fn enabled() -> bool {
    matches!(std::env::var("XFA_FLATTEN_TRACE"), Ok(v) if !v.is_empty() && v != "0")
}

/// Counts derived from the merged FormTree (bind stage).
#[derive(Default)]
struct BindCounts {
    form_nodes_total: usize,
    subforms: usize,
    subform_sets: usize,
    areas: usize,
    excl_groups: usize,
    page_areas: usize,
    page_sets: usize,
    fields: usize,
    fields_with_value: usize,
    fields_without_value: usize,
    draws: usize,
    draws_with_text: usize,
    images: usize,
    hidden_nodes: usize,
    /// Nodes reachable only through a hidden/invisible/inactive ancestor — these
    /// are pruned wholesale by the layout engine's `queue_content` filter.
    nodes_under_hidden: usize,
    /// Text-bearing draws that are NOT under any hidden ancestor (i.e. SHOULD be
    /// laid out). A large gap between this and `layout_draw_nodes` indicates a
    /// bounded admission bug rather than a JS/visibility (broad) cause.
    visible_draws_with_text: usize,
}

fn count_bind(tree: &FormTree) -> BindCounts {
    let mut c = BindCounts {
        form_nodes_total: tree.nodes.len(),
        ..Default::default()
    };
    for i in 0..tree.nodes.len() {
        let id = FormNodeId(i);
        let node = tree.get(id);
        if tree.meta(id).presence != Presence::Visible {
            c.hidden_nodes += 1;
        }
        match &node.node_type {
            FormNodeType::Subform => c.subforms += 1,
            FormNodeType::SubformSet => c.subform_sets += 1,
            FormNodeType::Area => c.areas += 1,
            FormNodeType::ExclGroup => c.excl_groups += 1,
            FormNodeType::PageArea { .. } => c.page_areas += 1,
            FormNodeType::PageSet => c.page_sets += 1,
            FormNodeType::Field { value } => {
                c.fields += 1;
                if value.trim().is_empty() {
                    c.fields_without_value += 1;
                } else {
                    c.fields_with_value += 1;
                }
            }
            FormNodeType::Draw(content) => {
                c.draws += 1;
                if let DrawContent::Text(t) = content {
                    if !t.trim().is_empty() {
                        c.draws_with_text += 1;
                    }
                }
            }
            FormNodeType::Image { .. } => c.images += 1,
            FormNodeType::Root => {}
        }
    }

    // DFS from root node(s) to attribute hidden-subtree pruning. A node reached
    // only through a hidden/invisible/inactive ancestor is pruned wholesale by
    // the layout engine's `queue_content` filter, so the entire subtree is lost.
    let roots: Vec<FormNodeId> = (0..tree.nodes.len())
        .map(FormNodeId)
        .filter(|&id| matches!(tree.get(id).node_type, FormNodeType::Root))
        .collect();
    let roots = if roots.is_empty() {
        vec![FormNodeId(0)]
    } else {
        roots
    };
    let mut visited = vec![false; tree.nodes.len()];
    let mut stack: Vec<(FormNodeId, bool)> = roots.into_iter().map(|r| (r, false)).collect();
    while let Some((id, under_hidden)) = stack.pop() {
        if id.0 >= tree.nodes.len() || visited[id.0] {
            continue;
        }
        visited[id.0] = true;
        let node = tree.get(id);
        if under_hidden {
            c.nodes_under_hidden += 1;
        }
        let eff_hidden = under_hidden || tree.meta(id).presence.is_layout_hidden();
        if let FormNodeType::Draw(DrawContent::Text(t)) = &node.node_type {
            if !eff_hidden && !t.trim().is_empty() {
                c.visible_draws_with_text += 1;
            }
        }
        for &child in &node.children {
            stack.push((child, eff_hidden));
        }
    }
    if std::env::var("XFA_HIDDEN_DUMP").ok().as_deref() == Some("1") {
        dump_hidden_nodes(tree);
    }
    c
}

/// D8 (trace-only, env-gated by `XFA_HIDDEN_DUMP=1`, default OFF): emit one
/// stderr line per non-visible FormTree node for investigation. Behaviour-
/// neutral — only runs when the env flag is set AND the trace path is active,
/// and writes to stderr only. Fields: id, type, presence, occur(min/max/initial),
/// bound (has bound_data_node), zinst (is_zero_instance_prototype), parent id,
/// and subtree field/draw/text-char counts.
// Indices are used to address parallel arrays (`parent`) and to build
// `FormNodeId`s, so a range loop is the clearest form here.
#[allow(clippy::needless_range_loop)]
fn dump_hidden_nodes(tree: &FormTree) {
    let n = tree.nodes.len();
    let mut parent = vec![usize::MAX; n];
    for i in 0..n {
        for &c in &tree.get(FormNodeId(i)).children {
            if c.0 < n {
                parent[c.0] = i;
            }
        }
    }
    for i in 0..n {
        let id = FormNodeId(i);
        let m = tree.meta(id);
        if m.presence == Presence::Visible {
            continue;
        }
        let node = tree.get(id);
        // Subtree content tally.
        let (mut fields, mut draws, mut chars) = (0usize, 0usize, 0usize);
        let mut s = vec![id];
        let mut seen = std::collections::HashSet::new();
        while let Some(x) = s.pop() {
            if x.0 >= n || !seen.insert(x.0) {
                continue;
            }
            match &tree.get(x).node_type {
                FormNodeType::Field { value } => {
                    fields += 1;
                    chars += value.trim().len();
                }
                FormNodeType::Draw(DrawContent::Text(t)) => {
                    draws += 1;
                    chars += t.trim().len();
                }
                FormNodeType::Draw(_) => draws += 1,
                _ => {}
            }
            for &c in &tree.get(x).children {
                s.push(c);
            }
        }
        let ty = match &node.node_type {
            FormNodeType::Subform => "subform",
            FormNodeType::SubformSet => "subformset",
            FormNodeType::Area => "area",
            FormNodeType::ExclGroup => "exclgroup",
            FormNodeType::Field { .. } => "field",
            FormNodeType::Draw(_) => "draw",
            FormNodeType::Image { .. } => "image",
            FormNodeType::PageArea { .. } => "pagearea",
            FormNodeType::PageSet => "pageset",
            FormNodeType::Root => "root",
        };
        eprintln!(
            "XFA_HIDDEN_DUMP id={} type={} name={:?} presence={:?} occur(min={},max={:?},init={}) bound={} zinst={} parent={} sub_fields={} sub_draws={} sub_chars={}",
            i,
            ty,
            node.name,
            m.presence,
            node.occur.min,
            node.occur.max,
            node.occur.initial,
            m.bound_data_node.is_some(),
            m.is_zero_instance_prototype,
            parent[i] as i64,
            fields,
            draws,
            chars
        );
    }
}

/// Counts derived from the laid-out pages (layout stage).
#[derive(Default)]
struct LayoutCounts {
    nodes_total: usize,
    text_nodes: usize,
    wrapped_text_nodes: usize,
    field_nodes: usize,
    draw_nodes: usize,
    image_nodes: usize,
    total_chars: usize,
}

fn walk_layout_node(n: &LayoutNode, c: &mut LayoutCounts) {
    c.nodes_total += 1;
    match &n.content {
        LayoutContent::Text(t) => {
            c.text_nodes += 1;
            c.total_chars += t.chars().count();
        }
        LayoutContent::WrappedText { lines, .. } => {
            c.wrapped_text_nodes += 1;
            c.total_chars += lines.iter().map(|l| l.chars().count()).sum::<usize>();
        }
        LayoutContent::Field { value, .. } => {
            c.field_nodes += 1;
            c.total_chars += value.chars().count();
        }
        LayoutContent::Draw(_) => c.draw_nodes += 1,
        LayoutContent::Image { .. } => c.image_nodes += 1,
        LayoutContent::None => {}
    }
    for child in &n.children {
        walk_layout_node(child, c);
    }
}

fn count_layout(layout: &LayoutDom) -> LayoutCounts {
    let mut c = LayoutCounts::default();
    for page in &layout.pages {
        for node in &page.nodes {
            walk_layout_node(node, &mut c);
        }
    }
    c
}

fn json_str(s: &str) -> String {
    let mut out = String::with_capacity(s.len() + 2);
    out.push('"');
    for ch in s.chars() {
        match ch {
            '"' => out.push_str("\\\""),
            '\\' => out.push_str("\\\\"),
            '\n' => out.push_str("\\n"),
            '\r' => out.push_str("\\r"),
            '\t' => out.push_str("\\t"),
            c if (c as u32) < 0x20 => {
                let _ = write!(out, "\\u{:04x}", c as u32);
            }
            c => out.push(c),
        }
    }
    out.push('"');
    out
}

/// Per-page suppression diagnostics (XFA §4.3 data-empty page suppression).
/// Captured before the suppression `retain` so the trace can explain every
/// keep/drop decision and the occur-instance-aware signal that distinguishes a
/// real continuation page from an empty repeated occur-instance.
#[derive(Clone)]
pub(crate) struct PageSuppressionDiag {
    pub page_index: usize,
    pub keep: bool,
    pub reason: &'static str,
    pub field_count: usize,
    pub empty_field_count: usize,
    pub nonempty_field_count: usize,
    pub static_draw_text_chars: usize,
    /// Distinct FormNode ids referenced on the page (occur-instance signature).
    pub distinct_form_nodes: usize,
    /// Index of an earlier page with an identical FormNode-id signature, or -1.
    /// A non-negative value marks this page as a repeated occur-instance.
    pub duplicate_of_page: i64,
    pub runtime_instantiated: bool,
    // --- Layout provenance (XFA_LAYOUT_PROVENANCE_ENGINE_WAVE) ---
    /// True when the page's content sits under a repeating (`occur` max>1)
    /// subform ancestor.
    pub under_repeating_subform: bool,
    /// FormNodeId of the nearest repeating-subform ancestor, or -1.
    pub occur_template_id: i64,
    /// Page nodes bound to a data node (`meta.bound_data_node.is_some()`).
    pub data_bound_nodes_count: usize,
    /// Why this page exists: root_page / continuation / occur_instance /
    /// repeated_empty_instance / static_page_area / unknown.
    pub page_reason: &'static str,
    /// True when this page is a data-empty repeated occur-instance — the only
    /// class that is provenance-safe to drop.
    pub suppression_safe_to_drop: bool,
    /// exact / inferred / unknown.
    pub provenance_confidence: &'static str,
}

/// All inputs needed to assemble a flatten trace. Borrowed; nothing is cloned
/// beyond the small derived count structs.
pub(crate) struct TraceInputs<'a> {
    pub suppression: &'a [PageSuppressionDiag],
    pub input_bytes: usize,
    pub template_bytes: usize,
    pub js_execution_mode: &'a str,
    pub flatten_path: &'a str,
    pub template_packet_found: bool,
    pub datasets_packet_found: bool,
    pub form_packet_found: bool,
    pub image_files: usize,
    pub tree: &'a FormTree,
    pub scripts: &'a DynamicScriptOutcome,
    pub layout: &'a LayoutDom,
    pub pages_produced: usize,
    pub pages_after_suppression: usize,
    pub runtime_instantiated_pages: usize,
    pub overlays: &'a [PageOverlay],
    pub n_layout: usize,
    pub n_existing: usize,
    pub is_static_form: bool,
    pub has_static_content: bool,
    pub preserve_static: bool,
    pub excess_pages_deleted: usize,
    pub widgets_baked: usize,
    pub acroform_removed: bool,
    pub xfa_removed_structural: bool,
    pub needs_rendering_removed: bool,
    pub javascript_actions_stripped: usize,
    pub output_bytes: usize,
    pub output_page_count: usize,
}

/// Compute the earliest stage at which the content chain breaks. Cheap
/// heuristic over already-computed counts; documented in the schema.
fn divergence_hint(i: &TraceInputs, bind: &BindCounts, lay: &LayoutCounts) -> &'static str {
    if !i.template_packet_found {
        return "parse";
    }
    if bind.draws_with_text == 0 && bind.fields == 0 {
        return "bind";
    }

    // Text-bearing nodes that actually reached the laid-out tree. A static draw
    // carrying text becomes a Text/WrappedText leaf (or, rarely, a Draw leaf);
    // so visible text-draws should each yield at least one such node.
    let laid_text = lay.text_nodes + lay.wrapped_text_nodes + lay.draw_nodes;
    let not_hidden_dominated = bind.nodes_under_hidden * 3 < bind.form_nodes_total;

    // (1) BOUNDED, JS-independent: many *visible* static draws (no hidden
    // ancestor) produced no corresponding laid-out text node. This is the safe
    // fix target — content is lost at layout admission, not behind visibility
    // scripts. Threshold: fewer than ~2/3 of visible text-draws survived.
    if bind.visible_draws_with_text >= 8
        && laid_text * 3 < bind.visible_draws_with_text * 2
        && not_hidden_dominated
    {
        return "layout_admission_visible_draw_drop";
    }

    // (2) BROAD, JS/visibility-driven: most nodes are pruned because they sit
    // under a `presence="hidden"` ancestor that runtime scripts (not executed in
    // BestEffortStatic) would reveal. Static recovery is not possible.
    if bind.nodes_under_hidden * 2 >= bind.form_nodes_total {
        return "hidden_subtree_prune";
    }

    // (3) Page suppression removed real pages.
    if i.pages_after_suppression < i.pages_produced {
        return "layout_suppression";
    }

    // (4) Generic large drop of text-bearing bound nodes not explained above.
    let bind_text_bearing = bind.draws_with_text + bind.fields_with_value;
    let layout_text_bearing = laid_text + lay.field_nodes;
    if bind_text_bearing >= 8 && (layout_text_bearing * 2) < bind_text_bearing {
        return "layout_admission";
    }
    if lay.nodes_total > 0 && i.overlays.iter().all(|o| o.content_stream.is_empty()) {
        return "paint";
    }
    if i.excess_pages_deleted > 0 {
        return "writer_excess_delete";
    }
    if i.preserve_static && i.n_layout > i.n_existing {
        return "writer_static_preserve_clamp";
    }
    "none"
}

/// JSON array of repeating (`occur` max>1/unbounded) subforms: their occur
/// params and how many instances actually reached the laid-out tree. Lets the
/// trace show whether under-/over-pagination is occur/instance driven (it is the
/// runtime-instance signal the suppression problem was missing).
fn repeating_subforms_json(tree: &FormTree, layout: &LayoutDom) -> String {
    use xfa_layout_engine::form::FormNodeType;
    let mut inst = vec![0usize; tree.nodes.len()];
    fn walk(n: &LayoutNode, inst: &mut [usize]) {
        if n.form_node.0 < inst.len() {
            inst[n.form_node.0] += 1;
        }
        for c in &n.children {
            walk(c, inst);
        }
    }
    for p in &layout.pages {
        for n in &p.nodes {
            walk(n, &mut inst);
        }
    }
    let mut out = String::from("[");
    let mut first = true;
    for (idx, &inst_count) in inst.iter().enumerate() {
        let node = tree.get(FormNodeId(idx));
        if !matches!(
            node.node_type,
            FormNodeType::Subform
                | FormNodeType::SubformSet
                | FormNodeType::Area
                | FormNodeType::ExclGroup
        ) || !node.occur.is_repeating()
        {
            continue;
        }
        if !first {
            out.push(',');
        }
        first = false;
        let max = node.occur.max.map_or(-1i64, |m| m as i64);
        let _ = write!(
            out,
            "{{\"template_id\":{},\"occur_min\":{},\"occur_max\":{},\"occur_initial\":{},\"layout_instances\":{}}}",
            idx, node.occur.min, max, node.occur.initial, inst_count
        );
    }
    out.push(']');
    out
}

/// Build the JSON trace and emit it per the env contract. Call only when
/// [`enabled`] returned true.
pub(crate) fn emit(i: &TraceInputs) {
    let bind = count_bind(i.tree);
    let lay = count_layout(i.layout);
    let s = i.scripts;
    let overlay_total_bytes: usize = i.overlays.iter().map(|o| o.content_stream.len()).sum();
    let overlay_substantial = i.overlays.iter().any(|o| o.content_stream.len() > 1000);
    let hint = divergence_hint(i, &bind, &lay);

    let mut per_page = String::from("[");
    for (idx, o) in i.overlays.iter().enumerate() {
        if idx > 0 {
            per_page.push(',');
        }
        let _ = write!(
            per_page,
            "{{\"page\":{},\"overlay_bytes\":{}}}",
            idx + 1,
            o.content_stream.len()
        );
    }
    per_page.push(']');

    let mut supp = String::from("[");
    for (idx, d) in i.suppression.iter().enumerate() {
        if idx > 0 {
            supp.push(',');
        }
        let _ = write!(
            supp,
            "{{\"page\":{},\"keep\":{},\"reason\":{},\"field_count\":{},\"empty_field_count\":{},\"nonempty_field_count\":{},\"static_draw_text_chars\":{},\"distinct_form_nodes\":{},\"duplicate_of_page\":{},\"runtime_instantiated\":{},\"under_repeating_subform\":{},\"occur_template_id\":{},\"data_bound_nodes_count\":{},\"page_reason\":{},\"suppression_safe_to_drop\":{},\"provenance_confidence\":{}}}",
            d.page_index + 1,
            d.keep,
            json_str(d.reason),
            d.field_count,
            d.empty_field_count,
            d.nonempty_field_count,
            d.static_draw_text_chars,
            d.distinct_form_nodes,
            d.duplicate_of_page,
            d.runtime_instantiated,
            d.under_repeating_subform,
            d.occur_template_id,
            d.data_bound_nodes_count,
            json_str(d.page_reason),
            d.suppression_safe_to_drop,
            json_str(d.provenance_confidence),
        );
    }
    supp.push(']');

    // Epic A E-1: build lifecycle JSON array.
    let lifecycle_json = {
        let mut lc = String::from("[");
        for (idx, e) in s.script_lifecycle.iter().enumerate() {
            if idx > 0 {
                lc.push(',');
            }
            let _ = write!(
                lc,
                "{{\"script_idx\":{},\"node_id\":{},\"node_name\":{},\"activity\":{},\"lang\":{},\"outcome\":{}}}",
                e.script_idx,
                e.node_id,
                json_str(&e.node_name),
                json_str(&e.activity),
                json_str(e.lang),
                json_str(e.outcome),
            );
        }
        lc.push(']');
        lc
    };

    // Epic A E-6: build skipped_activities JSON object.
    let sa = &s.skipped_activities;
    let skipped_activities_json = {
        let mut sa_out = String::new();
        let _ = write!(
            sa_out,
            "{{\"initialize\":{},\"calculate\":{},\"click\":{},\"doc_ready\":{},\"layout_ready\":{},\"other\":{}}}",
            sa.initialize, sa.calculate, sa.click, sa.doc_ready, sa.layout_ready, sa.other,
        );
        sa_out
    };

    let mut j = String::with_capacity(2048);
    let _ = write!(
        j,
        "{{\"schema_version\":\"1.1\",\
\"input_bytes\":{},\"template_bytes\":{},\
\"js_execution_mode\":{},\"flatten_path\":{},\
\"parse\":{{\"template_packet_found\":{},\"datasets_packet_found\":{},\"form_packet_found\":{},\"image_files\":{}}},\
\"bind\":{{\"form_nodes_total\":{},\"subforms\":{},\"subform_sets\":{},\"areas\":{},\"excl_groups\":{},\"page_areas\":{},\"page_sets\":{},\"fields\":{},\"fields_with_value\":{},\"fields_without_value\":{},\"draws\":{},\"draws_with_text\":{},\"images\":{},\"hidden_nodes\":{},\"nodes_under_hidden\":{},\"visible_draws_with_text\":{}}},\
\"script\":{{\"output_quality\":{},\"js_present\":{},\"js_skipped\":{},\"other_skipped\":{},\"formcalc_run\":{},\"formcalc_errors\":{},\"js_executed\":{},\"js_runtime_errors\":{},\"js_timeouts\":{},\"js_oom\":{},\"js_host_calls\":{},\"js_mutations\":{},\"js_instance_writes\":{},\"js_list_writes\":{},\"js_binding_errors\":{},\"js_resolve_failures\":{},\"js_data_reads\":{},\"js_unsupported_host_calls\":{},\"js_probe_skips\":{},\"variables_scripts_collected\":{},\"variables_data_items_collected\":{},\"script_objects_registered\":{},\"script_objects_register_failed\":{},\"script_objects_subform_scoped\":{},\"som_lookups_total\":{},\"som_lookup_successes\":{},\"som_lookup_failures\":{},\"som_lookup_ambiguous\":{},\"som_subform_scripts_exposed\":{},\"som_occur_path_refs\":{},\"occur_lookups_total\":{},\"occur_lookup_successes\":{},\"occur_lookup_failures\":{},\"occur_property_reads\":{},\"occur_property_writes\":{},\"occur_min_writes\":{},\"occur_max_writes\":{},\"occur_mutations_captured\":{},\"occur_mutations_applied\":{},\"occur_mutations_skipped\":{},\"occur_application_ambiguous\":{},\"occur_application_targets\":{},\"presence_retry_enabled\":{},\"presence_retry_candidates\":{},\"presence_retry_admitted\":{},\"presence_retry_skipped\":{},\"presence_retry_nodes_under_admitted\":{},\"presence_retry_text_nodes_admitted\":{},\"lifecycle\":{},\"skipped_activities\":{},\"form_dom_match_failures\":{}}},\
\"layout\":{{\"pages_produced\":{},\"pages_after_suppression\":{},\"pages_suppressed\":{},\"runtime_instantiated_pages\":{},\"layout_nodes_total\":{},\"layout_text_nodes\":{},\"layout_wrapped_text_nodes\":{},\"layout_field_nodes\":{},\"layout_draw_nodes\":{},\"layout_image_nodes\":{},\"layout_total_chars\":{}}},\
\"paint\":{{\"overlays_generated\":{},\"overlay_total_bytes\":{},\"overlay_substantial\":{},\"per_page\":{}}},\
\"writer\":{{\"n_layout\":{},\"n_existing\":{},\"is_static_form\":{},\"has_static_content\":{},\"preserve_static\":{},\"excess_pages_deleted\":{},\"widgets_baked\":{},\"acroform_removed\":{},\"xfa_removed_structural\":{},\"needs_rendering_removed\":{},\"javascript_actions_stripped\":{},\"output_bytes\":{},\"output_page_count\":{}}},\
\"suppression\":{},\
\"repeating_subforms\":{},\
\"stage_first_divergence_hint\":{}}}",
        i.input_bytes, i.template_bytes,
        json_str(i.js_execution_mode), json_str(i.flatten_path),
        i.template_packet_found, i.datasets_packet_found, i.form_packet_found, i.image_files,
        bind.form_nodes_total, bind.subforms, bind.subform_sets, bind.areas, bind.excl_groups, bind.page_areas, bind.page_sets, bind.fields, bind.fields_with_value, bind.fields_without_value, bind.draws, bind.draws_with_text, bind.images, bind.hidden_nodes, bind.nodes_under_hidden, bind.visible_draws_with_text,
        json_str(s.output_quality.as_str()), s.js_present, s.js_skipped, s.other_skipped, s.formcalc_run, s.formcalc_errors, s.js_executed, s.js_runtime_errors, s.js_timeouts, s.js_oom, s.js_host_calls, s.js_mutations, s.js_instance_writes, s.js_list_writes, s.js_binding_errors, s.js_resolve_failures, s.js_data_reads, s.js_unsupported_host_calls, s.js_probe_skips, s.variables_scripts_collected, s.variables_data_items_collected, s.script_objects_registered, s.script_objects_register_failed, s.script_objects_subform_scoped, s.som_lookups_total, s.som_lookup_successes, s.som_lookup_failures, s.som_lookup_ambiguous, s.som_subform_scripts_exposed, s.som_occur_path_refs, s.occur_lookups_total, s.occur_lookup_successes, s.occur_lookup_failures, s.occur_property_reads, s.occur_property_writes, s.occur_min_writes, s.occur_max_writes, s.occur_mutations_captured, s.occur_mutations_applied, s.occur_mutations_skipped, s.occur_application_ambiguous, s.occur_application_targets, s.presence_retry_enabled, s.presence_retry_candidates, s.presence_retry_admitted, s.presence_retry_skipped, s.presence_retry_nodes_under_admitted, s.presence_retry_text_nodes_admitted,
        lifecycle_json, skipped_activities_json, s.form_dom_match_failures,
        i.pages_produced, i.pages_after_suppression, i.pages_produced.saturating_sub(i.pages_after_suppression), i.runtime_instantiated_pages, lay.nodes_total, lay.text_nodes, lay.wrapped_text_nodes, lay.field_nodes, lay.draw_nodes, lay.image_nodes, lay.total_chars,
        i.overlays.len(), overlay_total_bytes, overlay_substantial, per_page,
        i.n_layout, i.n_existing, i.is_static_form, i.has_static_content, i.preserve_static, i.excess_pages_deleted, i.widgets_baked, i.acroform_removed, i.xfa_removed_structural, i.needs_rendering_removed, i.javascript_actions_stripped, i.output_bytes, i.output_page_count,
        supp,
        repeating_subforms_json(i.tree, i.layout),
        json_str(hint),
    );

    match std::env::var("XFA_FLATTEN_TRACE_PATH") {
        Ok(path) if !path.is_empty() => {
            if let Err(e) = std::fs::write(&path, j.as_bytes()) {
                eprintln!("XFA_FLATTEN_TRACE: failed to write {path}: {e}");
            }
        }
        _ => {
            eprintln!(
                "XFA_FLATTEN_TRACE summary: path={} bind_draws_text={} layout_chars={} pages {}->{}/{} overlays={} out_pages={} divergence={}",
                i.flatten_path, bind.draws_with_text, lay.total_chars,
                i.pages_produced, i.pages_after_suppression, i.n_existing,
                i.overlays.len(), i.output_page_count, hint,
            );
        }
    }
}
