#include "gfx/dag_node_editor.h"
#include "gfx/dag_catalog.h"
#include "gfx/dag_node_previews.h"
#include "imgui.h"
#include "imgui_node_editor.h"
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <functional>
#include <queue>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>

namespace ed = ax::NodeEditor;

namespace fn::gfx {

static constexpr int MAX_NODES = 16;

static ed::EditorContext*         s_ctx = nullptr;
static uint32_t                   s_next_uid = 1;
static std::unordered_set<uint32_t> s_positioned;
// Real pin positions in canvas space, captured during node draw and consulted
// by the splice hit-test. Without this, a cable's hit zone is offset from the
// visible cable whenever node height ≠ pin row height (e.g. preview open).
static std::unordered_map<uintptr_t, ImVec2> s_pin_canvas_pos;

// ── ID encoding ──────────────────────────────────────────────────────────────
// node id   = editor_uid
// output pin id = (editor_uid << 8) | 0
// input pin id  = (editor_uid << 8) | (slot + 1)   slot 0..3
// link id       = (from_node_uid << 20) | (to_node_uid << 8) | slot

static uintptr_t node_id(uint32_t uid) {
    return static_cast<uintptr_t>(uid);
}
static uintptr_t output_pin_id(uint32_t uid) {
    return (static_cast<uintptr_t>(uid) << 8) | 0u;
}
static uintptr_t input_pin_id(uint32_t uid, int slot) {
    return (static_cast<uintptr_t>(uid) << 8) | static_cast<uintptr_t>(slot + 1);
}
static uintptr_t link_id(uint32_t from_uid, uint32_t to_uid, int slot) {
    return (static_cast<uintptr_t>(from_uid) << 20)
         | (static_cast<uintptr_t>(to_uid)   <<  8)
         | static_cast<uintptr_t>(slot);
}

// Decode pin id back
static bool is_output_pin(uintptr_t id) { return (id & 0xFF) == 0; }
static uint32_t uid_from_pin(uintptr_t id) { return static_cast<uint32_t>(id >> 8); }
static int slot_from_input_pin(uintptr_t id) { return static_cast<int>(id & 0xFF) - 1; }

// Closest distance from point p to the segment [a, b] (canvas space).
static float dist_point_to_segment(ImVec2 p, ImVec2 a, ImVec2 b) {
    float abx = b.x - a.x, aby = b.y - a.y;
    float apx = p.x - a.x, apy = p.y - a.y;
    float ab2 = abx * abx + aby * aby;
    if (ab2 <= 1e-6f) return std::sqrt(apx * apx + apy * apy);
    float t = (apx * abx + apy * aby) / ab2;
    t = std::max(0.0f, std::min(1.0f, t));
    float qx = a.x + t * abx, qy = a.y + t * aby;
    float dx = p.x - qx, dy = p.y - qy;
    return std::sqrt(dx * dx + dy * dy);
}

// Same horizontal-bias control offset imgui-node-editor uses for its links.
static inline float bezier_ctrl(float dx) {
    return std::max(40.0f, std::abs(dx) * 0.5f);
}

// Closest distance from point p to a cubic bezier curve, sampled as 24 chords.
static float dist_point_to_bezier(ImVec2 p, ImVec2 p0, ImVec2 p1, ImVec2 p2, ImVec2 p3) {
    constexpr int N = 24;
    float best = 1e30f;
    ImVec2 prev = p0;
    for (int i = 1; i <= N; ++i) {
        float t = static_cast<float>(i) / static_cast<float>(N);
        float u = 1.0f - t;
        float b0 = u * u * u;
        float b1 = 3.0f * u * u * t;
        float b2 = 3.0f * u * t * t;
        float b3 = t * t * t;
        ImVec2 pt(b0 * p0.x + b1 * p1.x + b2 * p2.x + b3 * p3.x,
                  b0 * p0.y + b1 * p1.y + b2 * p2.y + b3 * p3.y);
        float d = dist_point_to_segment(p, prev, pt);
        if (d < best) best = d;
        prev = pt;
    }
    return best;
}

static int find_by_uid(const std::vector<DagStep>& p, uint32_t uid) {
    for (int i = 0; i < static_cast<int>(p.size()); ++i) {
        if (p[static_cast<size_t>(i)].editor_uid == uid) return i;
    }
    return -1;
}
static int find_by_id(const std::vector<DagStep>& p, const std::string& id) {
    for (int i = 0; i < static_cast<int>(p.size()); ++i) {
        if (p[static_cast<size_t>(i)].id == id) return i;
    }
    return -1;
}

static ImVec4 kind_color(DagKind kind) {
    switch (kind) {
        case DagKind::Gen:    return ImVec4(0.25f, 0.55f, 0.90f, 1.0f);
        case DagKind::Op:     return ImVec4(0.65f, 0.40f, 0.90f, 1.0f);
        case DagKind::Blend:  return ImVec4(0.90f, 0.65f, 0.15f, 1.0f);
        case DagKind::Output: return ImVec4(0.85f, 0.25f, 0.25f, 1.0f);
    }
    return ImVec4(1, 1, 1, 1);
}

static constexpr float PIN_RADIUS    = 14.0f;   // big grabbable target
static constexpr float PIN_DIAMETER  = PIN_RADIUS * 2.0f;
static constexpr float CONTROL_WIDTH = 220.0f;
static constexpr float COL_GAP       = 14.0f;   // input ↔ controls ↔ output gap
static constexpr float CABLE_THICK   = 3.5f;
static const ImVec4    PIN_COLOR    = ImVec4(0.78f, 0.78f, 0.82f, 1.0f);
static const ImVec4    PIN_BORDER   = ImVec4(0.20f, 0.20f, 0.22f, 1.0f);
static const ImVec4    SPLICE_COLOR = ImVec4(1.00f, 0.82f, 0.18f, 1.0f); // golden preview cable

enum class PinSide { Input, Output };

// Draws a filled circle straddling the node's edge (half outside, half inside)
// and reserves only the inside half (PIN_RADIUS × PIN_DIAMETER) so the layout
// stays compact. The full circle is set as the pin's hit rect via ed::PinRect
// so the user can grab the protruding half.
static void draw_pin_circle(PinSide side) {
    ImVec2 cursor = ImGui::GetCursorScreenPos();
    float center_x = (side == PinSide::Input) ? cursor.x : cursor.x + PIN_RADIUS;
    ImVec2 center(center_x, cursor.y + PIN_RADIUS);
    ed::PinRect(ImVec2(center.x - PIN_RADIUS, center.y - PIN_RADIUS),
                ImVec2(center.x + PIN_RADIUS, center.y + PIN_RADIUS));
    ImDrawList* dl = ImGui::GetWindowDrawList();
    dl->AddCircleFilled(center, PIN_RADIUS, ImGui::ColorConvertFloat4ToU32(PIN_COLOR));
    dl->AddCircle(center, PIN_RADIUS, ImGui::ColorConvertFloat4ToU32(PIN_BORDER), 0, 2.0f);
    ImGui::Dummy(ImVec2(PIN_RADIUS, PIN_DIAMETER));
}

// Topological sort using Kahn's algorithm.
// Returns false if a cycle is detected (should not happen — BeginCreate rejects cycles).
static bool topo_sort(std::vector<DagStep>& pipeline) {
    int n = static_cast<int>(pipeline.size());
    if (n == 0) return true;

    // Build adjacency: edge from src -> dst if dst.source_ids[k] == src.id
    std::unordered_map<std::string, int> id_to_idx;
    for (int i = 0; i < n; ++i) id_to_idx[pipeline[static_cast<size_t>(i)].id] = i;

    std::vector<int> in_degree(static_cast<size_t>(n), 0);
    std::vector<std::vector<int>> adj(static_cast<size_t>(n));

    for (int i = 0; i < n; ++i) {
        const DagStep& s = pipeline[static_cast<size_t>(i)];
        const DagNodeDef* def = dag_find(s.name);
        int ni = def ? def->num_inputs : 0;
        for (int k = 0; k < ni; ++k) {
            const std::string& sid = s.source_ids[static_cast<size_t>(k)];
            if (sid.empty()) continue;
            auto it = id_to_idx.find(sid);
            if (it == id_to_idx.end()) continue;
            int src = it->second;
            adj[static_cast<size_t>(src)].push_back(i);
            in_degree[static_cast<size_t>(i)]++;
        }
    }

    std::queue<int> q;
    for (int i = 0; i < n; ++i) {
        if (in_degree[static_cast<size_t>(i)] == 0) q.push(i);
    }

    std::vector<int> order;
    order.reserve(static_cast<size_t>(n));
    while (!q.empty()) {
        int u = q.front(); q.pop();
        order.push_back(u);
        for (int v : adj[static_cast<size_t>(u)]) {
            if (--in_degree[static_cast<size_t>(v)] == 0) q.push(v);
        }
    }

    if (static_cast<int>(order.size()) != n) return false; // cycle

    std::vector<DagStep> sorted;
    sorted.reserve(static_cast<size_t>(n));
    for (int idx : order) sorted.push_back(pipeline[static_cast<size_t>(idx)]);

    // Force Output nodes to the back so all their dependencies live at smaller
    // indices (compiler and cycle validator search strictly before the target).
    std::stable_partition(sorted.begin(), sorted.end(), [](const DagStep& s) {
        const DagNodeDef* d = dag_find(s.name);
        return !(d && d->kind == DagKind::Output);
    });

    pipeline = std::move(sorted);
    return true;
}

bool dag_node_editor(std::vector<DagStep>& pipeline) {
    bool changed = false;

    // Ensure UIDs are assigned (e.g. for nodes created before this editor was active)
    for (auto& step : pipeline) {
        if (step.editor_uid == 0) {
            step.editor_uid = s_next_uid++;
        }
    }

    // Create context on first call
    if (!s_ctx) {
        ed::Config cfg;
        cfg.SettingsFile = nullptr; // no disk persistence
        s_ctx = ed::CreateEditor(&cfg);
    }

    // Palette drop: detect without a capturing target (an InvisibleButton would
    // steal clicks from the node editor). Observe the active drag-drop payload
    // and, if the mouse is over this window and the user releases LMB, queue an
    // add at that canvas position.
    static std::string s_pending_add_name;
    static ImVec2      s_pending_add_pos(0, 0);
    static bool        s_pending_add = false;

    const bool window_hovered = ImGui::IsWindowHovered(
        ImGuiHoveredFlags_ChildWindows | ImGuiHoveredFlags_AllowWhenBlockedByActiveItem);
    if (window_hovered) {
        if (const ImGuiPayload* p = ImGui::GetDragDropPayload()) {
            if (p->IsDataType("DAG_NODE_TYPE") && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
                s_pending_add_name.assign(static_cast<const char*>(p->Data),
                                          static_cast<size_t>(p->DataSize));
                s_pending_add_pos = ImGui::GetMousePos();
                s_pending_add = true;
            }
        }
    }

    ed::SetCurrentEditor(s_ctx);
    ed::Begin("dag_editor", ImVec2(0.0f, 0.0f));

    if (s_pending_add) {
        const DagNodeDef* def = dag_find(s_pending_add_name);
        if (def) {
            ImVec2 drop = ed::ScreenToCanvas(s_pending_add_pos);

            // ── Priority 1: drop on an existing cable → splice (src → new → dst).
            // Only valid if the new def actually has an input pin (Op / Blend).
            int splice_src_idx = -1, splice_dst_idx = -1, splice_slot = -1;
            if (def->num_inputs >= 1 && def->kind != DagKind::Output) {
                constexpr float HIT_THRESH = 14.0f; // canvas px
                float best_d = HIT_THRESH;
                for (size_t i = 0; i < pipeline.size(); ++i) {
                    const DagStep& dst = pipeline[i];
                    const DagNodeDef* dd = dag_find(dst.name);
                    if (!dd) continue;
                    for (int k = 0; k < dd->num_inputs; ++k) {
                        const std::string& sid = dst.source_ids[static_cast<size_t>(k)];
                        if (sid.empty()) continue;
                        int src_idx = find_by_id(pipeline, sid);
                        if (src_idx < 0) continue;
                        const DagStep& src = pipeline[static_cast<size_t>(src_idx)];

                        auto out_it = s_pin_canvas_pos.find(output_pin_id(src.editor_uid));
                        auto in_it  = s_pin_canvas_pos.find(input_pin_id(dst.editor_uid, k));
                        if (out_it == s_pin_canvas_pos.end() ||
                            in_it  == s_pin_canvas_pos.end()) continue;
                        ImVec2 A = out_it->second;
                        ImVec2 B = in_it->second;
                        float ctrl = bezier_ctrl(B.x - A.x);
                        ImVec2 P1(A.x + ctrl, A.y);
                        ImVec2 P2(B.x - ctrl, B.y);
                        float d = dist_point_to_bezier(drop, A, P1, P2, B);
                        if (d < best_d) {
                            best_d         = d;
                            splice_src_idx = src_idx;
                            splice_dst_idx = static_cast<int>(i);
                            splice_slot    = k;
                        }
                    }
                }
            }

            // ── Priority 2: drop on an existing node of the same kind → replace.
            int hit_idx = -1;
            if (splice_dst_idx < 0) {
                for (size_t i = 0; i < pipeline.size(); ++i) {
                    auto npos = ed::GetNodePosition(ed::NodeId(node_id(pipeline[i].editor_uid)));
                    auto nsz  = ed::GetNodeSize    (ed::NodeId(node_id(pipeline[i].editor_uid)));
                    if (drop.x >= npos.x && drop.x <= npos.x + nsz.x &&
                        drop.y >= npos.y && drop.y <= npos.y + nsz.y) {
                        hit_idx = static_cast<int>(i);
                        break;
                    }
                }
            }
            const DagNodeDef* hit_def = (hit_idx >= 0)
                ? dag_find(pipeline[static_cast<size_t>(hit_idx)].name) : nullptr;

            if (splice_dst_idx >= 0 && static_cast<int>(pipeline.size()) < MAX_NODES) {
                // Splice: build the new node wired to the existing source, then
                // rewire the existing destination's input to point to it.
                const std::string src_id = pipeline[static_cast<size_t>(splice_src_idx)].id;
                const std::string dst_id = pipeline[static_cast<size_t>(splice_dst_idx)].id;

                uint32_t uid = s_next_uid++;
                DagStep step;
                step.id     = "n" + std::to_string(uid);
                step.name   = def->name;
                step.params = def->param_defaults;
                step.editor_uid = uid;
                step.editor_pos_x = drop.x;
                step.editor_pos_y = drop.y;
                step.source_ids[0] = src_id; // wire src → new

                auto insert_it = pipeline.end();
                for (auto it = pipeline.begin(); it != pipeline.end(); ++it) {
                    const DagNodeDef* d = dag_find(it->name);
                    if (d && d->kind == DagKind::Output) { insert_it = it; break; }
                }
                pipeline.insert(insert_it, step);

                // Re-find dst by id (insertion may have shifted indices) and
                // rewire its slot to the new node.
                int dst_now = find_by_id(pipeline, dst_id);
                if (dst_now >= 0) {
                    pipeline[static_cast<size_t>(dst_now)].source_ids[static_cast<size_t>(splice_slot)] = step.id;
                }
                changed = true;
            } else if (hit_def && hit_def->kind == def->kind && def->kind != DagKind::Output) {
                // Replace path: same-kind node hit. Keep id, editor_uid, pos,
                // source_ids, preview_open. Reset params + clear stale input
                // slots beyond the new def's input count.
                DagStep& tgt = pipeline[static_cast<size_t>(hit_idx)];
                tgt.name   = def->name;
                tgt.params = def->param_defaults;
                for (int k = def->num_inputs; k < 4; ++k) {
                    tgt.source_ids[static_cast<size_t>(k)].clear();
                }
                changed = true;
            } else if (static_cast<int>(pipeline.size()) < MAX_NODES) {
                // Add path: brand-new node, inserted before Output so the sink stays last.
                uint32_t uid = s_next_uid++;
                DagStep step;
                step.id     = "n" + std::to_string(uid);
                step.name   = def->name;
                step.params = def->param_defaults;
                step.editor_uid = uid;
                step.editor_pos_x = drop.x;
                step.editor_pos_y = drop.y;
                auto insert_it = pipeline.end();
                for (auto it = pipeline.begin(); it != pipeline.end(); ++it) {
                    const DagNodeDef* d = dag_find(it->name);
                    if (d && d->kind == DagKind::Output) { insert_it = it; break; }
                }
                pipeline.insert(insert_it, step);
                changed = true;
            }
        }
        s_pending_add = false;
    }

    // ── Live splice candidate detection ─────────────────────────────────────
    // The user can splice into a cable in two ways:
    //   (a) drag a node from the palette  → ImGui drag-drop payload.
    //   (b) drag an existing node by its body → tracked via mouse-down on a node.
    // In both cases, while the drag is active we hit-test against existing
    // cables and remember the candidate so:
    //   1. the link-drawing pass below paints it in SPLICE_COLOR (preview).
    //   2. the release handler farther down rewires the graph.
    static uint32_t s_drag_existing_uid = 0;

    // Start tracking an existing-node drag when the user mouse-down on a node
    // body (not on a pin, not on the Output sink, must have at least one input).
    if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
        ed::PinId  hp = ed::GetHoveredPin();
        ed::NodeId hn = ed::GetHoveredNode();
        if (hp.Get() == 0 && hn.Get() != 0) {
            uint32_t uid = static_cast<uint32_t>(hn.Get());
            int idx = find_by_uid(pipeline, uid);
            if (idx >= 0) {
                const DagNodeDef* d = dag_find(pipeline[static_cast<size_t>(idx)].name);
                if (d && d->num_inputs >= 1 && d->kind != DagKind::Output) {
                    s_drag_existing_uid = uid;
                }
            }
        }
    }

    // Resolve which definition (if any) is the active splice candidate.
    const DagNodeDef* candidate_def       = nullptr;
    const std::string* exclude_node_id    = nullptr;
    int                exclude_node_idx   = -1;
    if (const ImGuiPayload* p = ImGui::GetDragDropPayload()) {
        if (p->IsDataType("DAG_NODE_TYPE")) {
            std::string drag_name(static_cast<const char*>(p->Data),
                                  static_cast<size_t>(p->DataSize));
            candidate_def = dag_find(drag_name);
        }
    }
    if (!candidate_def && s_drag_existing_uid != 0) {
        exclude_node_idx = find_by_uid(pipeline, s_drag_existing_uid);
        if (exclude_node_idx >= 0) {
            candidate_def    = dag_find(pipeline[static_cast<size_t>(exclude_node_idx)].name);
            exclude_node_id  = &pipeline[static_cast<size_t>(exclude_node_idx)].id;
        }
    }

    // Hit-test cables against current mouse position (canvas space).
    uint32_t splice_hl_from_uid = 0;
    uint32_t splice_hl_to_uid   = 0;
    int      splice_hl_slot     = -1;
    if (candidate_def && candidate_def->num_inputs >= 1
        && candidate_def->kind != DagKind::Output) {
        ImVec2 cur = ed::ScreenToCanvas(ImGui::GetMousePos());
        constexpr float HIT_THRESH = 16.0f;
        float best_d = HIT_THRESH;
        for (size_t i = 0; i < pipeline.size(); ++i) {
            const DagStep& dst = pipeline[i];
            // Skip cables that touch the moving node (its own in/out edges).
            if (exclude_node_id && dst.id == *exclude_node_id) continue;
            const DagNodeDef* dd = dag_find(dst.name);
            if (!dd) continue;
            for (int k = 0; k < dd->num_inputs; ++k) {
                const std::string& sid = dst.source_ids[static_cast<size_t>(k)];
                if (sid.empty()) continue;
                if (exclude_node_id && sid == *exclude_node_id) continue;
                int src_idx = find_by_id(pipeline, sid);
                if (src_idx < 0) continue;
                const DagStep& src = pipeline[static_cast<size_t>(src_idx)];
                auto out_it = s_pin_canvas_pos.find(output_pin_id(src.editor_uid));
                auto in_it  = s_pin_canvas_pos.find(input_pin_id(dst.editor_uid, k));
                if (out_it == s_pin_canvas_pos.end() ||
                    in_it  == s_pin_canvas_pos.end()) continue;
                ImVec2 A = out_it->second;
                ImVec2 B = in_it->second;
                float ctrl = bezier_ctrl(B.x - A.x);
                ImVec2 P1(A.x + ctrl, A.y);
                ImVec2 P2(B.x - ctrl, B.y);
                float d = dist_point_to_bezier(cur, A, P1, P2, B);
                if (d < best_d) {
                    best_d             = d;
                    splice_hl_from_uid = src.editor_uid;
                    splice_hl_to_uid   = dst.editor_uid;
                    splice_hl_slot     = k;
                }
            }
        }
    }

    // Release handler for the existing-node-drag splice. Palette splice goes
    // through s_pending_add above; this branch handles "drag node body onto cable".
    if (ImGui::IsMouseReleased(ImGuiMouseButton_Left) && s_drag_existing_uid != 0) {
        uint32_t moving_uid = s_drag_existing_uid;
        s_drag_existing_uid = 0;
        if (splice_hl_to_uid != 0) {
            int mv_idx  = find_by_uid(pipeline, moving_uid);
            int src_idx = find_by_uid(pipeline, splice_hl_from_uid);
            int dst_idx = find_by_uid(pipeline, splice_hl_to_uid);
            if (mv_idx >= 0 && src_idx >= 0 && dst_idx >= 0) {
                const std::string moving_id = pipeline[static_cast<size_t>(mv_idx)].id;
                const std::string src_id    = pipeline[static_cast<size_t>(src_idx)].id;
                // Detach moving node from any existing consumer.
                for (auto& s : pipeline) {
                    for (auto& sid : s.source_ids) {
                        if (sid == moving_id) sid.clear();
                    }
                }
                pipeline[static_cast<size_t>(mv_idx)].source_ids[0] = src_id;
                pipeline[static_cast<size_t>(dst_idx)].source_ids[static_cast<size_t>(splice_hl_slot)] = moving_id;
                changed = true;
            }
        }
    }

    // ── Draw nodes ───────────────────────────────────────────────────────────
    for (int i = 0; i < static_cast<int>(pipeline.size()); ++i) {
        DagStep& step = pipeline[static_cast<size_t>(i)];
        const DagNodeDef* def = dag_find(step.name);
        if (!def) continue;

        ImVec4 col = kind_color(def->kind);

        // Initial position only — after that, the editor owns the node's position
        // and user drags must not be overwritten.
        if (s_positioned.find(step.editor_uid) == s_positioned.end()) {
            if (step.editor_pos_x == 0.0f && step.editor_pos_y == 0.0f) {
                step.editor_pos_x = 50.0f + static_cast<float>(i) * 320.0f;
                step.editor_pos_y = 100.0f;
            }
            ed::SetNodePosition(ed::NodeId(node_id(step.editor_uid)),
                                ImVec2(step.editor_pos_x, step.editor_pos_y));
            s_positioned.insert(step.editor_uid);
        }

        // Zero lateral padding so the input/output pin circles sit flush
        // with the node's left and right edges.
        ed::PushStyleVar(ed::StyleVar_NodePadding, ImVec4(0, 12, 0, 12));
        ed::BeginNode(ed::NodeId(node_id(step.editor_uid)));

        // Header (with horizontal padding so the title doesn't touch the edge)
        ImGui::Dummy(ImVec2(8, 0));
        ImGui::SameLine(0, 0);
        ImGui::PushStyleColor(ImGuiCol_Text, col);
        ImGui::TextUnformatted(def->label.c_str());
        ImGui::PopStyleColor();
        ImGui::Dummy(ImVec2(0, 4));

        int ni = def->num_inputs;
        bool has_output_pin = (def->kind != DagKind::Output);

        // ── Three-column layout: inputs · controls · output ──────────
        ImGui::BeginGroup(); // inputs column (left edge)
        if (ni == 0) {
            ImGui::Dummy(ImVec2(PIN_RADIUS, PIN_DIAMETER));
        }
        for (int k = 0; k < ni; ++k) {
            ed::BeginPin(ed::PinId(input_pin_id(step.editor_uid, k)), ed::PinKind::Input);
            ed::PinPivotAlignment(ImVec2(0.0f, 0.5f));
            ed::PinPivotSize(ImVec2(0, 0));
            ImVec2 cur_screen = ImGui::GetCursorScreenPos();
            ImVec2 center_screen(cur_screen.x, cur_screen.y + PIN_RADIUS);
            s_pin_canvas_pos[input_pin_id(step.editor_uid, k)] =
                ed::ScreenToCanvas(center_screen);
            draw_pin_circle(PinSide::Input);
            ed::EndPin();
        }
        ImGui::EndGroup();

        ImGui::SameLine(0, COL_GAP); // gap between pin column and controls

        ImGui::BeginGroup(); // controls column (centre, with internal padding)
        ImGui::PushID(static_cast<int>(step.editor_uid));
        if (def->controls.empty() && def->kind != DagKind::Output) {
            ImGui::Dummy(ImVec2(CONTROL_WIDTH * 0.5f, PIN_DIAMETER));
        }
        for (size_t ci = 0; ci < def->controls.size(); ++ci) {
            const DagControl& ctrl = def->controls[ci];
            ImGui::SetNextItemWidth(CONTROL_WIDTH);
            char uid_lbl[64];
            std::snprintf(uid_lbl, sizeof(uid_lbl), "%s##%u%zu", ctrl.label.c_str(), step.editor_uid, ci);
            int pcount = static_cast<int>(step.params.size());
            if (ctrl.kind == DagControl::Kind::Slider) {
                int pidx = ctrl.param_idx[0];
                if (pidx >= 0 && pidx < pcount) {
                    ImGui::SliderFloat(uid_lbl, &step.params[static_cast<size_t>(pidx)], ctrl.min, ctrl.max);
                }
            } else if (ctrl.kind == DagControl::Kind::XY) {
                int px = ctrl.param_idx[0], py = ctrl.param_idx[1];
                if (px >= 0 && px < pcount && py >= 0 && py < pcount && py == px + 1) {
                    ImGui::SliderFloat2(uid_lbl, &step.params[static_cast<size_t>(px)], ctrl.min, ctrl.max);
                }
            } else if (ctrl.kind == DagControl::Kind::Color) {
                int pr = ctrl.param_idx[0];
                if (pr >= 0 && pr + 2 < pcount) {
                    ImGui::TextUnformatted(ctrl.label.c_str());
                    ImGui::SameLine();
                    ed::Suspend();
                    ImGui::ColorEdit3(uid_lbl, &step.params[static_cast<size_t>(pr)],
                                      ImGuiColorEditFlags_NoInputs |
                                      ImGuiColorEditFlags_NoLabel   |
                                      ImGuiColorEditFlags_AlphaBar);
                    ed::Resume();
                }
            }
        }

        // Per-node preview thumbnail (off by default).
        if (def->kind != DagKind::Output) {
            char btn_lbl[64];
            std::snprintf(btn_lbl, sizeof(btn_lbl), "%s preview##pv%u",
                          step.preview_open ? "[-]" : "[+]", step.editor_uid);
            if (ImGui::SmallButton(btn_lbl)) {
                step.preview_open = !step.preview_open;
            }
            if (step.preview_open) {
                unsigned tex = dag_preview_texture(step.editor_uid);
                if (tex != 0) {
                    ImGui::Image(static_cast<ImTextureID>(static_cast<intptr_t>(tex)),
                                 ImVec2(96, 64), ImVec2(0, 1), ImVec2(1, 0));
                } else {
                    ImGui::Dummy(ImVec2(96, 64));
                }
            }
        }
        ImGui::PopID();
        ImGui::EndGroup();

        ImGui::SameLine(0, COL_GAP); // gap between controls and output pin

        ImGui::BeginGroup(); // output column (right edge)
        if (has_output_pin) {
            ed::BeginPin(ed::PinId(output_pin_id(step.editor_uid)), ed::PinKind::Output);
            ed::PinPivotAlignment(ImVec2(1.0f, 0.5f));
            ed::PinPivotSize(ImVec2(0, 0));
            ImVec2 cur_screen = ImGui::GetCursorScreenPos();
            ImVec2 center_screen(cur_screen.x + PIN_RADIUS, cur_screen.y + PIN_RADIUS);
            s_pin_canvas_pos[output_pin_id(step.editor_uid)] =
                ed::ScreenToCanvas(center_screen);
            draw_pin_circle(PinSide::Output);
            ed::EndPin();
        } else {
            ImGui::Dummy(ImVec2(PIN_RADIUS, PIN_DIAMETER));
        }
        ImGui::EndGroup();

        ed::EndNode();
        ed::PopStyleVar();
    }

    // ── Draw existing links ──────────────────────────────────────────────────
    for (int i = 0; i < static_cast<int>(pipeline.size()); ++i) {
        const DagStep& step = pipeline[static_cast<size_t>(i)];
        const DagNodeDef* def = dag_find(step.name);
        if (!def) continue;
        for (int k = 0; k < def->num_inputs; ++k) {
            const std::string& sid = step.source_ids[static_cast<size_t>(k)];
            if (sid.empty()) continue;
            int src_idx = find_by_id(pipeline, sid);
            if (src_idx < 0) continue;
            const DagStep& src_step = pipeline[static_cast<size_t>(src_idx)];
            const bool is_splice_preview =
                (src_step.editor_uid == splice_hl_from_uid &&
                 step.editor_uid     == splice_hl_to_uid   &&
                 k == splice_hl_slot);
            ImVec4 link_col   = is_splice_preview ? SPLICE_COLOR     : PIN_COLOR;
            float  link_thick = is_splice_preview ? CABLE_THICK + 2.0f : CABLE_THICK;
            ed::Link(ed::LinkId(link_id(src_step.editor_uid, step.editor_uid, k)),
                     ed::PinId(output_pin_id(src_step.editor_uid)),
                     ed::PinId(input_pin_id(step.editor_uid, k)),
                     link_col, link_thick);
        }
    }

    // ── Right-click on a link deletes it directly ──────────────────────────
    {
        ed::Suspend();
        ed::LinkId ctx_lid;
        if (ed::ShowLinkContextMenu(&ctx_lid)) {
            uintptr_t raw = ctx_lid.Get();
            uint32_t to_uid = static_cast<uint32_t>((raw >> 8) & 0xFFFu);
            int slot = static_cast<int>(raw & 0xFFu);
            int to_idx = find_by_uid(pipeline, to_uid);
            if (to_idx >= 0 && slot >= 0 && slot < 4) {
                pipeline[static_cast<size_t>(to_idx)].source_ids[static_cast<size_t>(slot)].clear();
                changed = true;
            }
        }
        ed::Resume();
    }

    // ── Double right-click on a node deletes it (Output is protected) ──────
    if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Right)) {
        ed::NodeId hovered = ed::GetHoveredNode();
        uint32_t uid = static_cast<uint32_t>(hovered.Get());
        if (uid != 0) {
            int idx = find_by_uid(pipeline, uid);
            if (idx >= 0) {
                const DagNodeDef* d = dag_find(pipeline[static_cast<size_t>(idx)].name);
                if (d && d->kind != DagKind::Output) {
                    const std::string del_id = pipeline[static_cast<size_t>(idx)].id;
                    for (auto& s : pipeline) {
                        for (auto& sid : s.source_ids) {
                            if (sid == del_id) sid.clear();
                        }
                    }
                    pipeline.erase(pipeline.begin() + idx);
                    changed = true;
                }
            }
        }
    }

    // ── Right-click on a pin clears all connections of that pin ────────────
    {
        ed::Suspend();
        ed::PinId ctx_pid;
        if (ed::ShowPinContextMenu(&ctx_pid)) {
            uintptr_t raw = ctx_pid.Get();
            uint32_t uid = uid_from_pin(raw);
            if (is_output_pin(raw)) {
                // Output pin: clear every source_ids entry pointing to this node
                int idx = find_by_uid(pipeline, uid);
                if (idx >= 0) {
                    const std::string source_id = pipeline[static_cast<size_t>(idx)].id;
                    for (auto& s : pipeline) {
                        for (auto& sid : s.source_ids) {
                            if (sid == source_id) { sid.clear(); changed = true; }
                        }
                    }
                }
            } else {
                // Input pin: clear that single slot
                int slot = slot_from_input_pin(raw);
                int idx  = find_by_uid(pipeline, uid);
                if (idx >= 0 && slot >= 0 && slot < 4) {
                    auto& sid = pipeline[static_cast<size_t>(idx)].source_ids[static_cast<size_t>(slot)];
                    if (!sid.empty()) { sid.clear(); changed = true; }
                }
            }
        }
        ed::Resume();
    }

    // ── Handle link creation ─────────────────────────────────────────────────
    if (ed::BeginCreate()) {
        ed::PinId start_pin, end_pin;
        if (ed::QueryNewLink(&start_pin, &end_pin)) {
            uintptr_t sp = start_pin.Get();
            uintptr_t ep = end_pin.Get();

            // Normalise: start must be output, end must be input
            if (!is_output_pin(sp) && is_output_pin(ep)) {
                std::swap(sp, ep);
            }

            bool valid = is_output_pin(sp) && !is_output_pin(ep);
            if (valid) {
                uint32_t from_uid = uid_from_pin(sp);
                uint32_t to_uid   = uid_from_pin(ep);
                int slot          = slot_from_input_pin(ep);

                int from_idx = find_by_uid(pipeline, from_uid);
                int to_idx   = find_by_uid(pipeline, to_uid);

                // Real cycle check: would the new edge from->to introduce a path
                // from `from` back to itself? It does iff `from` already (transitively)
                // depends on `to`. We walk source_ids of `from` and reject if we
                // ever hit `to`. Vector index order is irrelevant — topo_sort runs
                // at end-of-frame and reorders the pipeline.
                bool cycle = false;
                if (from_uid == to_uid) {
                    cycle = true;
                } else if (from_idx >= 0 && to_idx >= 0) {
                    const std::string to_id = pipeline[static_cast<size_t>(to_idx)].id;
                    std::function<bool(const std::string&)> depends_on_to;
                    depends_on_to = [&](const std::string& node_id) -> bool {
                        if (node_id == to_id) return true;
                        int idx = -1;
                        for (int i = 0; i < static_cast<int>(pipeline.size()); ++i) {
                            if (pipeline[static_cast<size_t>(i)].id == node_id) { idx = i; break; }
                        }
                        if (idx < 0) return false;
                        const DagStep& s = pipeline[static_cast<size_t>(idx)];
                        const DagNodeDef* d = dag_find(s.name);
                        int n = d ? d->num_inputs : 0;
                        for (int k = 0; k < n; ++k) {
                            const std::string& sid = s.source_ids[static_cast<size_t>(k)];
                            if (!sid.empty() && depends_on_to(sid)) return true;
                        }
                        return false;
                    };
                    cycle = depends_on_to(pipeline[static_cast<size_t>(from_idx)].id);
                }

                if (cycle) {
                    ed::RejectNewItem(ImVec4(1, 0.3f, 0.3f, 1));
                    valid = false;
                }

                if (valid && from_idx >= 0 && to_idx >= 0 && slot >= 0) {
                    if (ed::AcceptNewItem()) {
                        pipeline[static_cast<size_t>(to_idx)].source_ids[static_cast<size_t>(slot)] =
                            pipeline[static_cast<size_t>(from_idx)].id;
                        changed = true;
                    }
                }
            } else {
                ed::RejectNewItem(ImVec4(0.5f, 0.5f, 0.5f, 1));
            }
        }
    }
    ed::EndCreate();

    // ── Handle deletion ──────────────────────────────────────────────────────
    if (ed::BeginDelete()) {
        ed::LinkId del_lid;
        while (ed::QueryDeletedLink(&del_lid)) {
            if (ed::AcceptDeletedItem()) {
                uintptr_t raw = del_lid.Get();
                uint32_t to_uid = static_cast<uint32_t>((raw >> 8) & 0xFFF);
                int      slot   = static_cast<int>(raw & 0xFF);
                int to_idx = find_by_uid(pipeline, to_uid);
                if (to_idx >= 0 && slot < 4) {
                    pipeline[static_cast<size_t>(to_idx)].source_ids[static_cast<size_t>(slot)].clear();
                    changed = true;
                }
            }
        }

        ed::NodeId del_nid;
        while (ed::QueryDeletedNode(&del_nid)) {
            uint32_t uid = static_cast<uint32_t>(del_nid.Get());
            int idx = find_by_uid(pipeline, uid);
            // Refuse to delete the Output node (it's the sink)
            const DagNodeDef* ddef = (idx >= 0) ? dag_find(pipeline[static_cast<size_t>(idx)].name) : nullptr;
            if (ddef && ddef->kind == DagKind::Output) {
                ed::RejectDeletedItem();
                continue;
            }
            if (ed::AcceptDeletedItem()) {
                if (idx >= 0) {
                    const std::string& del_step_id = pipeline[static_cast<size_t>(idx)].id;
                    for (auto& step : pipeline) {
                        for (auto& sid : step.source_ids) {
                            if (sid == del_step_id) sid.clear();
                        }
                    }
                    pipeline.erase(pipeline.begin() + idx);
                    changed = true;
                }
            }
        }
    }
    ed::EndDelete();

    // ── Save node positions back to steps ────────────────────────────────────
    for (auto& step : pipeline) {
        auto pos = ed::GetNodePosition(ed::NodeId(node_id(step.editor_uid)));
        step.editor_pos_x = pos.x;
        step.editor_pos_y = pos.y;
    }

    ed::End();
    ed::SetCurrentEditor(nullptr);

    // ── Topological sort after topology change ───────────────────────────────
    if (changed) {
        if (!topo_sort(pipeline)) {
            // Cycle detected — should not happen given BeginCreate validation
            // but log a warning and leave order as-is
            std::fprintf(stderr, "dag_node_editor: cycle detected, skipping topo sort\n");
        }
    }

    return changed;
}

void dag_node_editor_destroy() {
    if (s_ctx) {
        ed::DestroyEditor(s_ctx);
        s_ctx = nullptr;
    }
}

} // namespace fn::gfx
