package ai.nektron.grownet;

import ai.nektron.grownet.metrics.RegionMetrics;
import ai.nektron.grownet.debug.Trace;
import ai.nektron.grownet.growth.GrowthPolicy;
import ai.nektron.grownet.growth.GrowthEngine;

import java.util.*;
import ai.nektron.grownet.policy.*;

/**
 * Region: groups layers, owns simple wiring helpers and tick/prune orchestration.
 * API matches the demos (addLayer, connectLayers, bindInput, tick, prune),
 * and keeps parity with the C++/docs counterparts.
 */
public final class Region {

    public static final class PruneSummary {
        public int prunedSynapses = 0;
        public int prunedEdges    = 0;    // reserved for future inter-layer tract pruning
    }

    // ------------------------------ fields -------------------------------
    private final String name;
    private final List<Layer> layers = new ArrayList<>();
    private final Map<String, List<Integer>> inputPorts  = new HashMap<>();
    private final Map<String, List<Integer>> outputPorts = new HashMap<>();
    
    // Edge layers per port (auto-created on bind)
    private final Map<String, Integer> inputEdges = new HashMap<>();
    private final Map<String, Integer> outputEdges = new HashMap<>();
    private final RegionBus bus = new RegionBus();   // reserved for future tract batching
    private final Random rng = new Random(1234);
    private final List<MeshRule> meshRules = new ArrayList<>();
    // Optional: track tracts if you decide to bridge via Tract; kept empty by default.
    private final List<Tract> tracts = new ArrayList<>();

    // Spatial metrics toggle (opt-in)
    private boolean enableSpatialMetrics = false;

    private static final class MeshRule {
        final int src, dst; final double prob; final boolean feedback;
        MeshRule(int s, int d, double p, boolean f) { src=s; dst=d; prob=p; feedback=f; }
    }

    // [GROWNET:ANCHOR::AFTER_METRICS]
    private GrowthPolicy growthPolicy = null;   // optional, controls best‑effort growth
    // Optional proximity connectivity policy (sidecar)
    private ProximityConfig proximityConfig = null;

    // --------------------------- construction ----------------------------
    public Region(String name) { this.name = name; }

    /** Create a mixed layer. Returns the index of the new layer. */
    public int addLayer(int excitatoryCount, int inhibitoryCount, int modulatoryCount) {
        Layer layer = new Layer(excitatoryCount, inhibitoryCount, modulatoryCount);
        try { layer.setRegion(this); } catch (Throwable ignored) {}
        layers.add(layer);
        return layers.size() - 1;
    }

    /** Create a shape-aware input layer (e.g., grayscale image). */
    public int addInputLayer2D(int height, int width, double gain, double epsilonFire) {
        InputLayer2D in = new InputLayer2D(height, width, gain, epsilonFire);
        layers.add(in);
        return layers.size() - 1;
    }


/** Create a shape-aware N-D input layer defined by shape[] (row-major). */
public int addInputLayerND(int[] shape, double gain, double epsilonFire) {
    InputLayerND in = new InputLayerND(shape, gain, epsilonFire);
    layers.add(in);
    return layers.size() - 1;
}



    /** Create a shape-aware output layer (e.g., image writer). */
    public int addOutputLayer2D(int height, int width, double smoothing) {
        OutputLayer2D out = new OutputLayer2D(height, width, smoothing);
        layers.add(out);
        return layers.size() - 1;
    }

    
    // ------------------------------ edge helpers ---------------------------
    /** Ensure there is an Input edge layer for this port; create lazily if missing. */
    public int ensureInputEdge(String port) {
        Integer idx = inputEdges.get(port);
        if (idx != null) return idx;
        // Minimal scalar input edge: a 1-neuron layer that forwards into the graph.
        int edge = addLayer(1, 0, 0);
        inputEdges.put(port, edge);
        return edge;
    }

    /** Ensure there is an Output edge layer for this port; create lazily if missing. */
    public int ensureOutputEdge(String port) {
        Integer idx = outputEdges.get(port);
        if (idx != null) return idx;
        // Minimal scalar output edge: a 1-neuron layer acting as a sink (placeholder).
        int edge = addLayer(1, 0, 0);
        outputEdges.put(port, edge);
        return edge;
    }

    // ------------------------------ wiring -------------------------------
    /**
     * Randomly connect every neuron in source layer to neurons in destination
     * layer with probability {@code probability}. If {@code feedback} is true,
     * mark created synapses as feedback edges (your Neuron.connect handles this flag).
     * @return number of edges created
     */
    public int connectLayers(int sourceIndex, int destIndex, double probability, boolean feedback) {
        if (sourceIndex < 0 || sourceIndex >= layers.size())
            throw new IndexOutOfBoundsException("sourceIndex out of range");
        if (destIndex < 0 || destIndex >= layers.size())
            throw new IndexOutOfBoundsException("destIndex out of range");
        if (probability < 0.0) probability = 0.0;
        if (probability > 1.0) probability = 1.0;

        Layer src = layers.get(sourceIndex);
        Layer dst = layers.get(destIndex);

        int edges = 0;
        for (Neuron neuronA : src.getNeurons()) {
            for (Neuron neuronB : dst.getNeurons()) {
                if (neuronA == neuronB) continue;                  // defensive; layers differ anyway
                if (rng.nextDouble() < probability) {
                    neuronA.connect(neuronB, feedback);
                    edges++;
                }
            }
        }
        meshRules.add(new MeshRule(sourceIndex, destIndex, probability, feedback));
        return edges;
    }

    /** Convenient overload; defaults to feedforward (no feedback flag). */
    public int connectLayers(int sourceIndex, int destIndex, double probability) {
        return connectLayers(sourceIndex, destIndex, probability, false);
    }

    /** Bind scalar input: create/reuse InputEdge(port) and wire it → target layers (p=1.0). */
    public void bindInput(String port, List<Integer> layerIndices) {
        Objects.requireNonNull(port, "port");
        Objects.requireNonNull(layerIndices, "layerIndices");
        inputPorts.put(port, List.copyOf(layerIndices)); // for introspection only

        // Convenience: if any bound layer is an InputLayer2D, treat it as the edge
        Integer existing = inputEdges.get(port);
        Integer imageEdge = null;
        for (int layerIndex : layerIndices) {
            Layer maybe = layers.get(layerIndex);
            if (maybe instanceof InputLayer2D) { imageEdge = layerIndex; break; }
        }
        if (imageEdge != null) {
            inputEdges.put(port, imageEdge);
            // Wire that edge to any other bound layers (excluding itself)
            for (int layerIndex : layerIndices) if (layerIndex != imageEdge) {
                connectLayers(imageEdge, layerIndex, /*probability=*/1.0, /*feedback=*/false);
            }
            return;
        }

        int edge = ensureInputEdge(port);
        for (int layerIndex : layerIndices) {
            if (layerIndex == edge) continue;
            connectLayers(edge, layerIndex, /*probability=*/1.0, /*feedback=*/false);
        }
    }

    /** Bind scalar output: wire target layers → OutputEdge(port) (p=1.0). */
    public void bindOutput(String port, List<Integer> layerIndices) {
        Objects.requireNonNull(port, "port");
        Objects.requireNonNull(layerIndices, "layerIndices");
        outputPorts.put(port, List.copyOf(layerIndices));

        int edge = ensureOutputEdge(port);
        for (int layerIndex : layerIndices) {
            if (layerIndex == edge) continue;
            connectLayers(layerIndex, edge, /*probability=*/1.0, /*feedback=*/false);
        }
    }

    /** Bind a 2D input edge; creates InputLayer2D edge if absent or wrong type, then wires it → targets. */
    public void bindInput2D(String port, int height, int width, double gain, double epsilonFire, List<Integer> layerIndices) {

        Integer idx = inputEdges.get(port);
        if (idx != null && !(layers.get(idx) instanceof InputLayer2D)) {
            throw new IllegalStateException(
                    "Port '" + port + "' already bound as scalar. Use a different port or unbind first.");
        }

        Objects.requireNonNull(port, "port");
        Objects.requireNonNull(layerIndices, "layerIndices");

        idx = inputEdges.get(port);
        boolean needNew2D = true;
        if (idx != null) {
            Layer maybe = layers.get(idx);
            needNew2D = !(maybe instanceof InputLayer2D);
        }
        if (idx == null || needNew2D) {
            int edgeIndex = addInputLayer2D(height, width, gain, epsilonFire);
            inputEdges.put(port, edgeIndex);
            idx = edgeIndex;
        }
        inputPorts.put(port, List.copyOf(layerIndices));

        for (int layerIndex : layerIndices) {
            if (layerIndex == idx) continue;
            connectLayers(idx, layerIndex, /*probability=*/1.0, /*feedback=*/false);
        }
    }


    /** Bind an N-D input edge; creates InputLayerND if absent or wrong type/shape, then wires it → targets. */
    public void bindInputND(String port, int[] shape, double gain, double epsilonFire, List<Integer> layerIndices) {
        Objects.requireNonNull(port, "port");
        Objects.requireNonNull(shape, "shape");
        Objects.requireNonNull(layerIndices, "layerIndices");
    
        Integer idx = inputEdges.get(port);
        boolean needNew = true;
        if (idx != null) {
            Layer maybe = layers.get(idx);
            if (maybe instanceof InputLayerND) {
                InputLayerND nd = (InputLayerND) maybe;
                needNew = !nd.hasShape(shape);
            }
        }
        if (idx == null || needNew) {
            int edgeIndex = addInputLayerND(shape, gain, epsilonFire);
            inputEdges.put(port, edgeIndex);
            idx = edgeIndex;
        }
        inputPorts.put(port, List.copyOf(layerIndices)); // introspection only
        for (int layerIndex : layerIndices) {
            if (layerIndex == idx) continue;
            connectLayers(idx, layerIndex, /*probability=*/1.0, /*feedback=*/false);
        }
    }



    
    // ------------------------------ pulses (region-wide) -------------------
    /** Temporarily raise inhibition for the next tick (applies to all layer buses). */
    public void pulseInhibition(double factor) {
        bus.setInhibitionFactor(factor);
        for (Layer layer : layers) {
            layer.getBus().setInhibitionFactor(factor);
        }
    }

    /** Temporarily scale modulation for the next tick (applies to all layer buses). */
    public void pulseModulation(double factor) {
        bus.setModulationFactor(factor);
        for (Layer layer : layers) {
            layer.getBus().setModulationFactor(factor);
        }
    }

    // -------------------------------- tick -------------------------------
    /**
     * Drive all entry layers bound to {@code port} with {@code value} (single scalar per tick).
     * Returns lightweight metrics. (deliveredEvents remains a best‑effort placeholder unless
     * your Layer/Neuron tracks it explicitly.)
     */
    /** Scalar tick: port must be bound to a scalar InputEdge (via bindInput). */
    /** Drive a scalar into the edge bound to {@code port}; returns per-tick metrics. */
    public RegionMetrics tick(String port, double value) {

        RegionMetrics regionMetrics = new RegionMetrics();

        Integer edgeIdx = inputEdges.get(port);
        if (edgeIdx == null) {
            throw new IllegalArgumentException("No InputEdge for port '" + port + "'. Call bindInput(...) first.");
        }

        layers.get(edgeIdx).forward(value);
        // Optional proximity connectivity (post-propagation, pre end_tick/decay)
        try {
            if (proximityConfig != null && proximityConfig.isEnabled()) {
                ProximityEngine.apply(this, proximityConfig);
            }
        } catch (Throwable ignored) { }
        regionMetrics.incDeliveredEvents();

        // End-of-tick housekeeping
        for (Layer l : layers) l.endTick();
        bus.decay();

        // Structural metrics
        for (Layer l : layers) {
            for (Neuron neuron : l.getNeurons()) {
                regionMetrics.addSlots(neuron.getSlots().size());
                regionMetrics.addSynapses(neuron.getOutgoing().size());
            }
        }
        // Best‑effort growth hook
        try {
            if (growthPolicy != null) {
                GrowthEngine.applyFocusOutlierHints(this, growthPolicy);
                GrowthEngine.maybeGrow(this, growthPolicy);
            }
        } catch (Throwable ignored) { }
        return regionMetrics;
    }


    /**
     * Drive this region with a 2D image (values in [0, 1] or any float range).
     * Follows the same onInput/onOutput contract as scalar ticks.
     */
    /** 2D tick (image-agnostic name). Port must be bound to a 2D InputEdge (InputLayer2D). */

    /** N-D tick: deliver a flat row-major array with explicit shape to an InputLayerND edge. */
    /** Deliver a row-major flat tensor + shape into an InputLayerND edge. */
    public RegionMetrics tickND(String port, double[] flat, int[] shape) {
        RegionMetrics metrics = new RegionMetrics();

        Integer edge = inputEdges.get(port);
        if (edge == null)
            throw new IllegalArgumentException("No InputEdge for port '" + port + "'. Call bindInputND(...) first.");

        Layer layer = layers.get(edge);
        if (!(layer instanceof InputLayerND))
            throw new IllegalArgumentException("InputEdge for '" + port + "' is not ND (expected InputLayerND).");

        ((InputLayerND) layer).forwardND(flat, shape);
        // Optional proximity connectivity (post-propagation, pre end_tick/decay)
        try {
            if (proximityConfig != null && proximityConfig.isEnabled()) {
                ProximityEngine.apply(this, proximityConfig);
            }
        } catch (Throwable ignored) { }
        metrics.incDeliveredEvents();

        for (Layer l : layers) l.endTick();
        bus.decay();

        for (Layer l : layers) {
            for (Neuron neuron : l.getNeurons()) {
                metrics.addSlots(neuron.getSlots().size());
                metrics.addSynapses(neuron.getOutgoing().size());
            }
        }
        // Best‑effort region growth (layers)
        try {
            if (growthPolicy != null) {
                GrowthEngine.maybeGrow(this, growthPolicy);
            }
        } catch (Throwable ignored) { }
        return metrics;
    }

    /** Drive a 2D frame into an InputLayer2D edge bound to {@code port}. */
    public RegionMetrics tick2D(String port, double[][] frame) {
        RegionMetrics metrics = new RegionMetrics();

        Integer edge = inputEdges.get(port);
        if (edge == null)
            throw new IllegalArgumentException("No InputEdge for port '" + port + "'. Call bindInput2D(...) first.");

        Layer layer = layers.get(edge);
        if (!(layer instanceof InputLayer2D))
            throw new IllegalArgumentException("InputEdge for '" + port + "' is not 2D (expected InputLayer2D).");

        ((InputLayer2D) layer).forwardImage(frame);
        // Optional proximity connectivity (post-propagation, pre end_tick/decay)
        try {
            if (proximityConfig != null && proximityConfig.isEnabled()) {
                ProximityEngine.apply(this, proximityConfig);
            }
        } catch (Throwable ignored) { }
        metrics.incDeliveredEvents();

        // End-of-tick housekeeping
        for (Layer l : layers) l.endTick();
        bus.decay();

        // Aggregate structure metrics
        for (Layer l : layers) {
            for (Neuron neuron : l.getNeurons()) {
                metrics.addSlots(neuron.getSlots().size());
                metrics.addSynapses(neuron.getOutgoing().size());
            }
        }
        // Optional spatial metrics (env or flag)
        try {
            String env = System.getenv("GROWNET_ENABLE_SPATIAL_METRICS");
            boolean doSpatial = enableSpatialMetrics || "1".equals(env);
            if (doSpatial) {
                // Prefer furthest downstream OutputLayer2D frame; fallback to input if output is all zeros
                double[][] chosen = null;
                for (int i = layers.size() - 1; i >= 0; --i) {
                    Layer candidateLayer = layers.get(i);
                    if (candidateLayer instanceof OutputLayer2D) {
                        chosen = ((OutputLayer2D) candidateLayer).getFrame();
                        break;
                    }
                }
                if (chosen == null) {
                    chosen = frame;
                } else if (isAllZero(chosen) && !isAllZero(frame)) {
                    chosen = frame;
                }

                long active = 0L;
                double total = 0.0, sumRow = 0.0, sumCol = 0.0;
                int rowMin = Integer.MAX_VALUE, rowMax = -1, colMin = Integer.MAX_VALUE, colMax = -1;
                int imageHeight = chosen.length;
                int imageWidth = (imageHeight > 0 ? chosen[0].length : 0);
                for (int rowIndex = 0; rowIndex < imageHeight; ++rowIndex) {
                    double[] rowVec = chosen[rowIndex];
                    int columnLimit = Math.min(imageWidth, rowVec.length);
                    for (int colIndex = 0; colIndex < columnLimit; ++colIndex) {
                        double value = rowVec[colIndex];
                        if (value > 0.0) {
                            active += 1;
                            total  += value;
                            sumRow += rowIndex * value;
                            sumCol += colIndex * value;
                            if (rowIndex < rowMin) rowMin = rowIndex;
                            if (rowIndex > rowMax) rowMax = rowIndex;
                            if (colIndex < colMin) colMin = colIndex;
                            if (colIndex > colMax) colMax = colIndex;
                        }
                    }
                }
                metrics.setActivePixels(active);
                if (total > 0.0) {
                    metrics.setCentroid(sumRow / total, sumCol / total);
                } else {
                    metrics.setCentroid(0.0, 0.0);
                }
                if (rowMax >= rowMin && colMax >= colMin) {
                    metrics.setBBox(rowMin, rowMax, colMin, colMax);
                } else {
                    metrics.setBBox(0, -1, 0, -1);
                }
            }
        } catch (Throwable ignored) { }
        // Best‑effort region growth (layers)
        try {
            if (growthPolicy != null) {
                GrowthEngine.maybeGrow(this, growthPolicy);
            }
        } catch (Throwable ignored) { }
        return metrics;
    }

    public RegionMetrics tickImage(String port, double[][] frame) {
        return tick2D(port, frame);
    }

    /** Compute spatial metrics from a 2D image (optionally prefer downstream output). */
    public RegionMetrics computeSpatialMetrics(double[][] image2d, boolean preferOutput) {
        RegionMetrics metrics = new RegionMetrics();
        try {
            double[][] chosen = image2d;
            if (preferOutput) {
                // Prefer furthest downstream OutputLayer2D if any
                for (int i = layers.size() - 1; i >= 0; --i) {
                    Layer candidateLayer = layers.get(i);
                    if (candidateLayer instanceof OutputLayer2D) {
                        try {
                            double[][] out = ((OutputLayer2D) candidateLayer).getFrame();
                            chosen = out;
                            break;
                        } catch (Throwable ignored) {}
                    }
                }
                if (chosen != image2d && isAllZero(chosen) && !isAllZero(image2d)) {
                    chosen = image2d;
                }
            }
            long active = 0; double total = 0.0, sumRow = 0.0, sumCol = 0.0;
            int rowMin = Integer.MAX_VALUE, rowMax = -1, colMin = Integer.MAX_VALUE, colMax = -1;
            int H = (chosen == null ? 0 : chosen.length);
            int W = (H > 0 ? chosen[0].length : 0);
            for (int r = 0; r < H; ++r) {
                double[] row = chosen[r];
                int limit = Math.min(W, row.length);
                for (int c = 0; c < limit; ++c) {
                    double v = row[c];
                    if (v > 0.0) {
                        active += 1; total += v; sumRow += r * v; sumCol += c * v;
                        if (r < rowMin) rowMin = r; if (r > rowMax) rowMax = r;
                        if (c < colMin) colMin = c; if (c > colMax) colMax = c;
                    }
                }
            }
            metrics.setActivePixels((int) active);
            if (total > 0.0) metrics.setCentroid(sumRow / total, sumCol / total); else metrics.setCentroid(0.0, 0.0);
            if (rowMax >= rowMin && colMax >= colMin) metrics.setBBox(rowMin, rowMax, colMin, colMax); else metrics.setBBox(0, -1, 0, -1);
        } catch (Throwable ignored) { }
        return metrics;
    }

    /** Expose region-growth step as a public helper (add one spillover layer if policy triggers). */
    public void maybeGrowRegion() {
        try {
            if (growthPolicy != null) {
                GrowthEngine.maybeGrow(this, growthPolicy);
            }
        } catch (Throwable ignored) { }
    }

    /**
     * Windowed deterministic wiring (2D). Returns number of unique participating source pixels.
     */
    public int connectLayersWindowed(
            int sourceIndex,
            int destIndex,
            int kernelHeight,
            int kernelWidth,
            int strideHeight,
            int strideWidth,
            String padding,
            boolean feedback) {
        if (sourceIndex < 0 || sourceIndex >= layers.size())
            throw new IndexOutOfBoundsException("sourceIndex out of range");
        if (destIndex < 0 || destIndex >= layers.size())
            throw new IndexOutOfBoundsException("destIndex out of range");
        if (kernelHeight <= 0 || kernelWidth <= 0)
            throw new IllegalArgumentException("kernel dims must be > 0");
        if (strideHeight <= 0 || strideWidth <= 0)
            throw new IllegalArgumentException("stride must be > 0");

        Layer src = layers.get(sourceIndex);
        Layer dst = layers.get(destIndex);

        int gridHeight = 0, gridWidth = 0;
        if (src instanceof InputLayer2D) {
            gridHeight = ((InputLayer2D) src).getHeight();
            gridWidth = ((InputLayer2D) src).getWidth();
        } else if (dst instanceof OutputLayer2D) {
            gridHeight = ((OutputLayer2D) dst).getHeight();
            gridWidth = ((OutputLayer2D) dst).getWidth();
        } else {
            int neuronCount = src.getNeurons().size();
            int sideLength = (int)Math.sqrt(neuronCount);
            if (sideLength * sideLength != neuronCount) throw new IllegalStateException("source is not 2D-compatible");
            gridHeight = sideLength; gridWidth = sideLength;
        }

        java.util.List<int[]> origins = new java.util.ArrayList<>();
        if ("same".equalsIgnoreCase(padding)) {
            int outRows = (int)Math.ceil((double)gridHeight / strideHeight);
            int outCols = (int)Math.ceil((double)gridWidth / strideWidth);
            int padRows = Math.max(0, (outRows - 1) * strideHeight + kernelHeight - gridHeight);
            int padCols = Math.max(0, (outCols - 1) * strideWidth + kernelWidth  - gridWidth);
            int rowStart = -padRows / 2;
            int colStart = -padCols / 2;
            for (int originRow = rowStart; originRow + kernelHeight <= gridHeight + padRows; originRow += strideHeight) {
                for (int originCol = colStart; originCol + kernelWidth <= gridWidth + padCols; originCol += strideWidth) {
                    origins.add(new int[]{originRow, originCol});
                }
            }
        } else { // VALID
            for (int originRow = 0; originRow + kernelHeight <= gridHeight; originRow += strideHeight) {
                for (int originCol = 0; originCol + kernelWidth <= gridWidth; originCol += strideWidth) {
                    origins.add(new int[]{originRow, originCol});
                }
            }
        }

        java.util.Set<Integer> allowed = new java.util.HashSet<>();
        java.util.Map<Integer, Integer> centerMap = (dst instanceof OutputLayer2D)
                ? new java.util.HashMap<>() : java.util.Collections.emptyMap();

        if (dst instanceof OutputLayer2D) {
            for (int[] origin : origins) {
                int originRow = origin[0], originCol = origin[1];
                int clippedRowStart = Math.max(0, originRow), clippedColStart = Math.max(0, originCol);
                int clippedRowEnd = Math.min(gridHeight, originRow + kernelHeight), clippedColEnd = Math.min(gridWidth, originCol + kernelWidth);
                if (clippedRowStart >= clippedRowEnd || clippedColStart >= clippedColEnd) continue;
                int centerRow = Math.min(gridHeight - 1, Math.max(0, originRow + kernelHeight / 2));
                int centerCol = Math.min(gridWidth - 1, Math.max(0, originCol + kernelWidth  / 2));
                int centerIndex = centerRow * gridWidth + centerCol;
                for (int rowIndex = clippedRowStart; rowIndex < clippedRowEnd; ++rowIndex) {
                    for (int colIndex = clippedColStart; colIndex < clippedColEnd; ++colIndex) {
                        int srcFlatIndex = rowIndex * gridWidth + colIndex;
                        allowed.add(srcFlatIndex);
                        ((java.util.Map<Integer,Integer>)centerMap).putIfAbsent(srcFlatIndex, centerIndex);
                    }
                }
            }
            Tract tract = new Tract(src, dst, bus, feedback, allowed, centerMap);
            tracts.add(tract);
        } else {
            java.util.Set<Integer> seen = new java.util.HashSet<>();
            for (int[] origin : origins) {
                int originRow = origin[0], originCol = origin[1];
                int clippedRowStart = Math.max(0, originRow), clippedColStart = Math.max(0, originCol);
                int clippedRowEnd = Math.min(gridHeight, originRow + kernelHeight), clippedColEnd = Math.min(gridWidth, originCol + kernelWidth);
                if (clippedRowStart >= clippedRowEnd || clippedColStart >= clippedColEnd) continue;
                for (int rowIndex = clippedRowStart; rowIndex < clippedRowEnd; ++rowIndex) {
                    for (int colIndex = clippedColStart; colIndex < clippedColEnd; ++colIndex) {
                        int srcFlatIndex = rowIndex * gridWidth + colIndex;
                        if (seen.add(srcFlatIndex)) allowed.add(srcFlatIndex);
                    }
                }
            }
            Tract tract = new Tract(src, dst, bus, feedback, allowed, null);
            tracts.add(tract);
        }
        return allowed.size();
    }

    // ---- growth plumbing ----
    void autowireNewNeuron(Layer layerRef, int newIdx) {
        int layerIndex = layers.indexOf(layerRef); if (layerIndex < 0) return;
        // Outbound
        for (MeshRule rule : meshRules) if (rule.src == layerIndex) {
            Neuron sourceNeuron = layers.get(layerIndex).getNeurons().get(newIdx);
            for (Neuron targetNeuron : layers.get(rule.dst).getNeurons()) if (rng.nextDouble() <= rule.prob) sourceNeuron.connect(targetNeuron, rule.feedback);
        }
        // Inbound
        for (MeshRule rule : meshRules) if (rule.dst == layerIndex) {
            Neuron targetNeuron = layers.get(layerIndex).getNeurons().get(newIdx);
            for (Neuron sourceNeuron : layers.get(rule.src).getNeurons()) if (rng.nextDouble() <= rule.prob) sourceNeuron.connect(targetNeuron, rule.feedback);
        }
        // Tracts where this layer is the source: subscribe the new source neuron (if any tracts exist)
        for (Tract tractEntry : tracts) {
            if (tractEntry != null && tractEntry.getSource() == layerRef) {
                tractEntry.attachSourceNeuron(newIdx);
            }
        }
    }
    public int requestLayerGrowth(Layer saturated) {
        return requestLayerGrowth(saturated, 1.0, 4);
    }

    /** Overload with explicit connection probability for saturated → new wiring. */
    public int requestLayerGrowth(Layer saturated, double connectionProbability) {
        return requestLayerGrowth(saturated, connectionProbability, 4);
    }

    public int requestLayerGrowth(Layer saturated, double connectionProbability, int excitatoryCount) {
        int layerIndex = layers.indexOf(saturated); if (layerIndex < 0) return -1;
        int normalizedExcitatoryCount = Math.max(1, excitatoryCount);
        int newIdx = addLayer(normalizedExcitatoryCount, 0, 0);
        connectLayers(layerIndex, newIdx, connectionProbability, false);
        Map<String, Object> growthEvent = new LinkedHashMap<>();
        Trace.addLayerFields(growthEvent, saturated);
        growthEvent.put("created_layer", true);
        growthEvent.put("reason", "pressure");
        growthEvent.put("dest_layer_index", newIdx);
        Trace.emit("growth.layer.create", growthEvent);
        return newIdx;
    }


    // ----------------------------- maintenance ---------------------------
    /**
     * Prune weak/stale synapses (and optionally tract‑level links in the future).
     * Java version keeps a two‑argument signature for simplicity.
     */
    public PruneSummary prune(long synapseStaleWindow, double synapseMinStrength) {
        PruneSummary pruneSummary = new PruneSummary();
        for (Layer layer : layers) {
            for (Neuron neuron : layer.getNeurons()) {
                pruneSummary.prunedSynapses += neuron.pruneSynapses(synapseStaleWindow, synapseMinStrength);
            }
        }
        // pruneSummary.prunedEdges stays zero until you track inter‑layer tracts explicitly
        return pruneSummary;
    }

    // ------------------------------ accessors ----------------------------
    public String getName() { return name; }
    public List<Layer> getLayers() { return layers; }
    /** Back‑compat alias; prefer {@link #getLayers()}. */
    public List<Layer> layers() { return layers; }
    public RegionBus getBus() { return bus; }

    // Spatial metrics toggle
    public Region setEnableSpatialMetrics(boolean v) { this.enableSpatialMetrics = v; return this; }
    public boolean isEnableSpatialMetrics() { return enableSpatialMetrics; }

    private static boolean isAllZero(double[][] image) {
        if (image == null || image.length == 0) return true;
        for (double[] row : image) {
            for (double value : row) {
                if (value != 0.0) return false;
            }
        }
        return true;
    }

    // ------------------------------ growth policy ---------------------------
    public GrowthPolicy getGrowthPolicy() { return growthPolicy; }
    public Region setGrowthPolicy(GrowthPolicy gp) { this.growthPolicy = gp; return this; }
    // Proximity policy helpers
    public Region setProximityConfig(ProximityConfig pc) { this.proximityConfig = pc; return this; }
    public ProximityConfig getProximityConfig() { return proximityConfig; }
}
