/*
 * Release Planning — hierarchy & dependency planning on the release detail page.
 * Backed by /api/Release/GetReleasePlan, /SetParent, /AddDependency, /RemoveDependency.
 * Spec: product-planning/product-specifications/release-planning-feature-spec.md
 * OQ-1: Managers edit, Standard read-only. OQ-5: links scoped to the release project (enforced server-side).
 */
(function () {
    "use strict";

    var MODULE = "rmUserType";
    var TYPE_COLORS = {
        "Epic": "#ff7b00", "Feature": "#773b93",
        "User Story": "#009ccc", "Product Backlog Item": "#009ccc",
        "Task": "#f2cb1d", "Bug": "#cc293d", "Issue": "#b4009e"
    };

    var releaseId = null;
    var items = [];               // { id, rev, title, type, state, who, start, end, parentId, preds:[] }
    var canEdit = false;
    var currentView = "list";
    var collapsed = {};
    var undoStack = [];
    var loading = false;
    var planOrgName = "";
    var planProjectName = "";

    function byId(id) { return items.filter(function (i) { return i.id === id; })[0]; }
    function children(pid) { return items.filter(function (i) { return i.parentId === pid; }); }
    function successorsOf(id) { return items.filter(function (i) { return i.preds.indexOf(id) >= 0; }).map(function (i) { return i.id; }); }
    function typeColor(t) { return TYPE_COLORS[t] || "#8a8886"; }
    var typeIcons = {};
    var typeIconsLoaded = false;
    async function loadTypeIcons() {
        if (typeIconsLoaded) return;
        try {
            var res = await IndexLibrary.GetRequest({ endpoint: "/api/AzureDevOps/GetAllWorkItemTypes" });
            if (res && res.ok) {
                var types = await res.json();
                (types || []).forEach(function (t) { if (t && t.name && t.iconUrl) typeIcons[t.name] = t.iconUrl; });
                typeIconsLoaded = true;
            }
        } catch (e) { /* fall back to the colored badge below */ }
    }
    function typeBadge(t) {
        var url = typeIcons[t];
        if (url) return '<img class="plan-badge" src="' + esc(url) + '" alt="' + esc(t) + '" title="' + esc(t) +
            '" style="width:14px;height:14px;min-width:14px;object-fit:contain;background:none;border-radius:0;vertical-align:middle" />';
        return '<span class="plan-badge" style="background:' + typeColor(t) + '"></span>';
    }
    function esc(s) { return (s == null ? "" : String(s)).replace(/[&<>"]/g, function (c) { return ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]; }); }
    function toast(type, msg) { try { IndexLibrary.ShowToast(type, msg); } catch (e) { console.log(type, msg); } }

    /* ---------------- lifecycle ---------------- */
    window.ShowPlanningView = ShowPlanningView;
    window.RefreshPlanningView = ShowPlanningView;
    window.removePlanItem = removePlanItem;

    var toolbarWired = false;

    async function ShowPlanningView() {
        try {
            var params = new URLSearchParams(window.location.search);
            releaseId = params.get("rId") || (window.editReleaseId || null);

            var cfg = await IndexLibrary.GetExtensionConfig();
            var enabled = !cfg.features || !cfg.features.releasePlanning || cfg.features.releasePlanning === "enable";
            if (!enabled) return;

            var view = document.getElementById("planningView");
            if (!view) return;

            var selCount = (window.selectedWorkItems || []).length;
            if (!releaseId && selCount === 0) { view.style.display = "none"; return; }

            if (!toolbarWired) { wireToolbar(); toolbarWired = true; }
            view.style.display = "block";
            await loadPlan();
        } catch (e) { console.error("planning init failed", e); }
    }

    function wireToolbar() {
        var vt = document.getElementById("planViewToggle");
        if (vt) vt.addEventListener("click", function (e) {
            var b = e.target.closest("button"); if (!b) return;
            Array.prototype.forEach.call(vt.children, function (x) { x.classList.remove("on"); });
            b.classList.add("on");
            currentView = b.getAttribute("data-view");
            render();
        });
        var undo = document.getElementById("planUndoBtn");
        if (undo) undo.addEventListener("click", doUndo);
        var refresh = document.getElementById("planRefreshBtn");
        if (refresh) refresh.addEventListener("click", loadPlan);
    }

    /* ---------------- data ---------------- */
    async function loadPlan() {
        if (loading) return;
        loading = true;
        showLoading(true);
        try {
            var type = null;
            try { type = await IndexLibrary.GetUserLicensesType(MODULE); } catch (e) { }
            canEdit = (type === "1" || type === 1);

            await loadTypeIcons();
            planOrgName = await IndexLibrary.GetAccountNameAsync();

            if (releaseId) {
                var res = await IndexLibrary.GetRequest({ endpoint: "/api/Release/GetReleasePlan/" + releaseId });
                if (!res.ok) { toast("error", "Failed to load plan (" + res.status + ")"); return; }
                var plan = await res.json();
                planProjectName = plan.projectName || "";
                items = (plan.items || []).map(function (d) {
                    return {
                        id: d.workItemId, rev: d.rev,
                        title: d.title, type: d.type, state: d.state, who: d.assignedTo,
                        start: d.startDate, end: d.endDate,
                        parentId: (d.parentId == null ? null : d.parentId),
                        preds: (d.predecessorIds || []).slice()
                    };
                });
            } else {
                var details = window.selectedWorkItemDetails || {};
                planProjectName = "";
                items = (window.selectedWorkItems || []).map(function (id) {
                    var d = details[id] || {};
                    if (!planProjectName && d.project) planProjectName = d.project;
                    return {
                        id: id, rev: 0,
                        title: d.title || ("#" + id), type: d.type || "", state: d.state || "", who: d.assignedTo || "",
                        start: (d.start && d.start !== "Unavailable") ? d.start : null,
                        end: (d.end && d.end !== "Unavailable") ? d.end : null,
                        parentId: null, preds: []
                    };
                });
            }
            undoStack = [];
            updateUndoBtn();
            applyRoleBar();
            render();
        } catch (e) {
            console.error(e); toast("error", "Failed to load plan");
        } finally {
            loading = false; showLoading(false);
        }
    }

    function showLoading(on) {
        var el = document.getElementById("planLoading");
        if (el) el.style.display = on ? "" : "none";
        var lv = document.getElementById("planListView");
        var tv = document.getElementById("planTreeView");
        var tl = document.getElementById("planTimelineView");
        if (on) { if (lv) lv.style.display = "none"; if (tv) tv.style.display = "none"; if (tl) tl.style.display = "none"; }
    }

    function applyRoleBar() {
        var bar = document.getElementById("planRoleBar");
        var hint = document.getElementById("planHint");
        var undo = document.getElementById("planUndoBtn");
        if (!bar) return;
        if (!canEdit) {
            bar.className = "plan-rolebar show";
            bar.textContent = "\uD83D\uDC41 Read-only \u2014 Standard license (editing needs a Release Manager license)";
            if (hint) hint.style.display = "none";
            if (undo) undo.style.display = "none";
        } else {
            bar.className = "plan-rolebar";
            // hint + undo only relevant on Tree tab
            if (hint) hint.style.display = currentView === "tree" ? "" : "none";
            if (undo) undo.style.display = currentView === "list" ? "none" : "";
        }
    }

    /* ---------------- remove work item from release (List view … menu) ---------------- */
    function removePlanItem(workItemId) {
        if (!canEdit) { toast("warning", "Read-only \u2014 a Release Manager license is required."); return; }
        // Sync with create-release.js selectedWorkItems global
        if (window.selectedWorkItems) {
            var idx = window.selectedWorkItems.indexOf(parseInt(workItemId));
            if (idx >= 0) window.selectedWorkItems.splice(idx, 1);
        }
        // Optimistic remove from plan view
        items = items.filter(function (i) { return i.id !== workItemId; });
        render();
        // Persist: reuse SaveRelease from create-release.js
        if (typeof window.SaveRelease === "function" && window.selectedWorkItems) {
            window.SaveRelease(window.selectedWorkItems);
        }
    }
    function isAncestor(ancestorId, nodeId) {
        var cur = byId(nodeId); var seen = {};
        while (cur && cur.parentId != null) {
            if (seen[cur.parentId]) break; seen[cur.parentId] = true;
            if (cur.parentId === ancestorId) return true;
            cur = byId(cur.parentId);
        }
        return false;
    }
    function validateParent(childId, parentId) {
        if (parentId == null) return null;
        if (childId === parentId) return "An item cannot be its own parent.";
        if (byId(childId).parentId === parentId) return null;
        if (isAncestor(childId, parentId)) return "That would create a hierarchy cycle.";
        return null;
    }
    function depReachable(fromId, toId) {
        var stack = [fromId], seen = {};
        while (stack.length) {
            var n = stack.pop(); if (n === toId) return true; if (seen[n]) continue; seen[n] = true;
            successorsOf(n).forEach(function (s) { stack.push(s); });
        }
        return false;
    }
    function validateDependency(predId, succId) {
        if (predId === succId) return "An item cannot depend on itself.";
        if (byId(succId).preds.indexOf(predId) >= 0) return "That dependency already exists.";
        if (depReachable(succId, predId)) return "That would create a dependency cycle.";
        return null;
    }

    /* ---------------- writes (optimistic + API + rollback) ---------------- */
    function guardEdit() {
        if (!canEdit) { toast("warning", "Read-only \u2014 a Release Manager license is required to edit."); return false; }
        return true;
    }

    async function setParent(childId, parentId, opts) {
        opts = opts || {};
        if (!guardEdit()) return false;
        var err = validateParent(childId, parentId);
        if (err) { toast("error", err); return false; }
        var item = byId(childId);
        var oldParent = item.parentId;
        item.parentId = parentId; render();
        try {
            var res = await IndexLibrary.PostRequest({
                endpoint: "/api/Release/SetParent",
                headers: { "UserEmail": IndexLibrary.GetUserEmail() },
                body: { releaseId: Number(releaseId), childWorkItemId: childId, parentWorkItemId: parentId, expectedRev: item.rev }
            });
            var result = await res.json().catch(function () { return {}; });
            if (res.ok && result.success) {
                if (result.newRev != null) item.rev = result.newRev;
                if (!opts.isUndo) pushUndo(function () { return setParent(childId, oldParent, { isUndo: true }); });
                toast("success", parentId == null ? "#" + childId + " moved to top level" : "#" + childId + " nested under #" + parentId);
                return true;
            }
            return await handleWriteFailure(res, result, function () { item.parentId = oldParent; });
        } catch (e) { item.parentId = oldParent; render(); toast("error", "Save failed"); return false; }
    }

    async function addDependency(predId, succId, opts) {
        opts = opts || {};
        if (!guardEdit()) return false;
        var err = validateDependency(predId, succId);
        if (err) { toast("error", err); return false; }
        var succ = byId(succId);
        succ.preds.push(predId); render();
        try {
            var res = await IndexLibrary.PostRequest({
                endpoint: "/api/Release/AddDependency",
                headers: { "UserEmail": IndexLibrary.GetUserEmail() },
                body: { releaseId: Number(releaseId), predecessorWorkItemId: predId, successorWorkItemId: succId, expectedRev: succ.rev }
            });
            var result = await res.json().catch(function () { return {}; });
            if (res.ok && result.success) {
                if (result.newRev != null) succ.rev = result.newRev;
                if (!opts.isUndo) pushUndo(function () { return removeDependency(predId, succId, { isUndo: true }); });
                if (!opts.silent) toast("success", "#" + predId + " is now a predecessor of #" + succId);
                return true;
            }
            return await handleWriteFailure(res, result, function () { succ.preds = succ.preds.filter(function (p) { return p !== predId; }); });
        } catch (e) { succ.preds = succ.preds.filter(function (p) { return p !== predId; }); render(); toast("error", "Save failed"); return false; }
    }

    async function removeDependency(predId, succId, opts) {
        opts = opts || {};
        if (!guardEdit()) return false;
        var succ = byId(succId);
        var had = succ.preds.indexOf(predId) >= 0;
        succ.preds = succ.preds.filter(function (p) { return p !== predId; }); render();
        try {
            var res = await IndexLibrary.PostRequest({
                endpoint: "/api/Release/RemoveDependency",
                headers: { "UserEmail": IndexLibrary.GetUserEmail() },
                body: { releaseId: Number(releaseId), predecessorWorkItemId: predId, successorWorkItemId: succId, expectedRev: succ.rev }
            });
            var result = await res.json().catch(function () { return {}; });
            if (res.ok && result.success) {
                if (result.newRev != null) succ.rev = result.newRev;
                if (!opts.isUndo) pushUndo(function () { return addDependency(predId, succId, { isUndo: true }); });
                toast("success", "Dependency #" + predId + "\u2192#" + succId + " removed");
                return true;
            }
            return await handleWriteFailure(res, result, function () { if (had) succ.preds.push(predId); });
        } catch (e) { if (had) succ.preds.push(predId); render(); toast("error", "Save failed"); return false; }
    }

    async function handleWriteFailure(res, result, revert) {
        revert(); 
        if (res.status === 409) { toast("warning", "This item changed elsewhere \u2014 reloading."); await loadPlan(); return false; }
        toast("error", (result && result.message) ? result.message : "Change rejected (" + res.status + ")");
        render();
        return false;
    }

    /* ---------------- undo ---------------- */
    function pushUndo(fn) { undoStack.push(fn); if (undoStack.length > 50) undoStack.shift(); updateUndoBtn(); }
    function updateUndoBtn() { var b = document.getElementById("planUndoBtn"); if (b) b.disabled = undoStack.length === 0; }
    async function doUndo() { var fn = undoStack.pop(); updateUndoBtn(); if (fn) await fn(); }

    /* ---------------- render: list (default) / tree / timeline ---------------- */
    function render() {
        var lv = document.getElementById("planListView");
        var tv = document.getElementById("planTreeView");
        var tl = document.getElementById("planTimelineView");
        if (currentView === "list") {
            if (lv) lv.style.display = ""; if (tv) tv.style.display = "none"; if (tl) tl.style.display = "none";
            renderList();
        } else if (currentView === "tree") {
            if (lv) lv.style.display = "none"; if (tv) tv.style.display = ""; if (tl) tl.style.display = "none";
            renderTree();
        } else {
            if (lv) lv.style.display = "none"; if (tv) tv.style.display = "none"; if (tl) tl.style.display = "";
            renderTimeline();
        }
    }

    /* ---------------- render: list ---------------- */
    function renderList() {
        var host = document.getElementById("planListView");
        if (!host) return;
        if (!items.length) {
            host.innerHTML = '<p class="plan-hint" style="padding:20px;text-align:center">No work items in this release.</p>';
            return;
        }
        function orderedFlat() {
            var out = [], seen = {};
            function walk(pid, depth) { children(pid).forEach(function (c) { if (seen[c.id]) return; seen[c.id] = true; out.push({ it: c, depth: depth }); walk(c.id, depth + 1); }); }
            items.filter(function (i) { return i.parentId == null; }).forEach(function (r) { if (seen[r.id]) return; seen[r.id] = true; out.push({ it: r, depth: 0 }); walk(r.id, 1); });
            return out;
        }
        var rows = orderedFlat();
        function stCls(s) { return s === "Done" ? "plan-state done" : (s === "Doing" ? "plan-state doing" : "plan-state todo"); }
        function esc(s) { return (s == null ? "" : String(s)).replace(/[&<>"]/g, function (c) { return ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]; }); }
        function indent(d) {
            var s = ""; for (var i = 0; i < d; i++) s += "\u21b3 ";
            return d > 0 ? '<span class="plan-list-indent">' + s.trim() + '</span> ' : "";
        }
        function wiUrl(id) {
            return 'https://dev.azure.com/' + encodeURIComponent(planOrgName) + '/' + encodeURIComponent(planProjectName) + '/_workitems/edit/' + id;
        }
        function deps(it) {
            var h = "";
            it.preds.forEach(function (p) { h += '<span class="plan-pill pred">\u21a4 #' + p + '</span>'; });
            successorsOf(it.id).forEach(function (s) { h += '<span class="plan-pill">#' + s + ' \u21a6</span>'; });
            return h;
        }
        function menuBtn(id) {
            if (!canEdit) return "";
            return '<span class="plan-row-menu" style="flex:none">' +
                '<div class="dropdown">' +
                '<button class="btn dropdown-toggle caret-off" type="button" data-bs-toggle="dropdown" aria-expanded="false" ' +
                'style="border-radius:0;background:var(--da-surface-alt);color:var(--da-text-secondary);border:0;font-size:13px;padding:2px 8px;line-height:1">...</button>' +
                '<div class="dropdown-menu" style="padding-left:8px;border-radius:0">' +
                '<span style="cursor:pointer;display:flex;align-items:center;font-size:12px" onclick="window.removePlanItem(' + id + ')">' +
                '<i class="bi bi-trash icon-81"></i>' +
                '<span class="dropdown-item">Delete</span>' +
                '</span></div></div></span>';
        }
        function dateCell(d) { return d ? '<span style="font-size:12px;color:var(--plan-muted);font-variant-numeric:tabular-nums">' + d + '</span>' : '<span style="color:#c8c6c4;font-size:12px">&mdash;</span>'; }
        function initials(name) {
            var n = (name || "").trim();
            if (!n) return "";
            var parts = n.split(/\s+/);
            var a = parts[0].charAt(0);
            var b = parts.length > 1 ? parts[parts.length - 1].charAt(0) : "";
            return (a + b).toUpperCase();
        }
        function avatarCell(name) {
            var nm = (name || "").trim();
            if (!nm) return '<span class="plan-avatar plan-avatar-empty" title="Unassigned"><i class="bi bi-person"></i></span>';
            return '<span class="plan-avatar" title="' + esc(nm) + '">' + esc(initials(nm)) + '</span>';
        }
        var tbody = rows.map(function (r) {
            var it = r.it;
            return '<tr>' +
                '<td>' + typeBadge(it.type) + ' <span style="font-size:12px;color:var(--plan-muted)">' + esc(it.type) + '</span></td>' +
                '<td class="plan-wid"><a href="' + wiUrl(it.id) + '" target="_blank" rel="noopener noreferrer" style="color:#0078d4;text-decoration:none;font-variant-numeric:tabular-nums">' + indent(r.depth) + '#' + it.id + '</a></td>' +
                '<td><div style="display:flex;align-items:center;gap:8px">' +
                    '<a href="' + wiUrl(it.id) + '" target="_blank" rel="noopener noreferrer" style="color:var(--da-text);text-decoration:none;font-weight:600;flex:1">' + esc(it.title) + '</a>' +
                    menuBtn(it.id) +
                '</div></td>' +
                '<td><span class="' + stCls(it.state) + '">' + esc(it.state) + '</span></td>' +
                '<td>' + avatarCell(it.who) + '</td>' +
                '<td>' + dateCell(it.start) + '</td>' +
                '<td>' + dateCell(it.end) + '</td>' +
                '<td><span class="plan-deps">' + deps(it) + '</span></td>' +
                '</tr>';
        }).join("");
        host.innerHTML =
            '<p class="plan-list-count">' + items.length + ' work item' + (items.length === 1 ? '' : 's') + ' in this release</p>' +
            '<div class="plan-list-wrap"><table class="plan-list-table">' +
            '<thead><tr><th>Type</th><th>ID</th><th>Title</th><th>State</th><th>Assignee</th><th>Start</th><th>End</th><th>Dependencies</th></tr></thead>' +
            '<tbody>' + tbody + '</tbody></table></div>';
    }

    function depSummary(it) {
        var html = "";
        it.preds.forEach(function (p) { html += '<span class="plan-pill pred" title="predecessor">\u21a4 #' + p + '</span>'; });
        successorsOf(it.id).forEach(function (s) { html += '<span class="plan-pill" title="successor">#' + s + ' \u21a6</span>'; });
        return html;
    }
    var _treeSeen = null;
    function rowHtml(it) {
        if (_treeSeen) { if (_treeSeen[it.id]) return ""; _treeSeen[it.id] = true; }
        var kids = children(it.id);
        var hasKids = kids.length > 0;
        var isCol = !!collapsed[it.id];
        return '' +
            '<div class="plan-node" data-node="' + it.id + '">' +
            '<div class="plan-row" draggable="false" data-id="' + it.id + '">' +
            '<span class="plan-caret ' + (hasKids ? "" : "hidden") + '" data-caret="' + it.id + '">' + (isCol ? "\u25b6\ufe0e" : "\u25bc\ufe0e") + '</span>' +
            typeBadge(it.type) +
            '<span class="plan-wid">#' + it.id + '</span>' +
            '<span class="plan-title">' + esc(it.title) + '</span>' +
            '<span class="plan-type">' + esc(it.type) + '</span>' +
            '<span class="plan-state">' + esc(it.state) + '</span>' +
            '<span class="plan-deps">' + depSummary(it) + '</span>' +
            '<span class="plan-actions">' +
            (canEdit ? '<span class="plan-dep-handle" data-dep="' + it.id + '" title="Drag to another item to add a dependency (predecessor \u2192 successor)"><i class="bi bi-record-circle"></i></span>' : '') +
            '<button type="button" class="plan-icon-btn" data-links="' + it.id + '" title="Relationships">\uD83D\uDD17</button>' +
            '</span>' +
            '</div>' +
            (hasKids && !isCol ? '<div class="plan-children">' + kids.map(rowHtml).join("") + '</div>' : '') +
            '</div>';
    }
    function renderTree() {
        var root = document.getElementById("planRootzone");
        if (!root) return;
        var roots = items.filter(function (i) { return i.parentId == null; });
        _treeSeen = {};
        root.innerHTML = roots.length ? roots.map(rowHtml).join("") :
            '<p class="plan-hint" style="padding:20px;text-align:center">No work items in this release. Add items to the release first.</p>';
        _treeSeen = null;
        wireTree();
    }

    /* ---------------- tree interactions (pointer-events — smooth in Chrome/Edge/Firefox) ---------------- */
    var dragId = null, dropState = null;
    var rowGhost = null, ghostOffX = 0, ghostOffY = 0;

    function clearDropHints() {
        Array.prototype.forEach.call(
            document.querySelectorAll("#planningView .drop-target,#planningView .drop-invalid,#planningView .insert-before,#planningView .insert-after"),
            function (r) { r.classList.remove("drop-target", "drop-invalid", "insert-before", "insert-after"); }
        );
    }

    function startRowDrag(e) {
        if (!canEdit || depDragging) return;
        if (e.target.closest && (e.target.closest("[data-dep]") || e.target.closest(".plan-icon-btn") || e.target.closest("[data-caret]"))) return;
        if (e.button !== 0) return;
        e.preventDefault();

        var row = e.currentTarget;
        var rb  = row.getBoundingClientRect();
        dragId    = Number(row.getAttribute("data-id"));
        ghostOffX = e.clientX - rb.left;
        ghostOffY = e.clientY - rb.top;

        rowGhost = row.cloneNode(true);
        rowGhost.classList.add("plan-row-ghost");
        rowGhost.style.top   = rb.top   + "px";
        rowGhost.style.left  = rb.left  + "px";
        rowGhost.style.width = rb.width + "px";
        document.body.appendChild(rowGhost);

        row.classList.add("dragging");
        window.addEventListener("pointermove", onRowMove);
        window.addEventListener("pointerup",   onRowUp, { once: true });
    }

    function onRowMove(e) {
        if (dragId == null) return;
        rowGhost.style.left = (e.clientX - ghostOffX) + "px";
        rowGhost.style.top  = (e.clientY - ghostOffY) + "px";

        clearDropHints();
        var rz = document.getElementById("planRootzone");
        if (rz) rz.classList.remove("detach-armed");

        rowGhost.style.visibility = "hidden";
        var el = document.elementFromPoint(e.clientX, e.clientY);
        rowGhost.style.visibility = "";

        var targetRow = el && el.closest ? el.closest(".plan-row") : null;
        if (!targetRow) {
            if (rz && byId(dragId) && byId(dragId).parentId != null) rz.classList.add("detach-armed");
            dropState = null;
            return;
        }

        var targetId = Number(targetRow.getAttribute("data-id"));
        if (targetId === dragId) { dropState = null; return; }

        var zone = dropZoneImpl(e.clientY, targetRow.getBoundingClientRect());
        dropState = { targetId: targetId, zone: zone };

        if (zone === "child") {
            var err = validateParent(dragId, targetId);
            targetRow.classList.add(err ? "drop-invalid" : "drop-target");
        } else {
            var newParent = byId(targetId) ? (byId(targetId).parentId != null ? byId(targetId).parentId : null) : null;
            var err2 = (byId(dragId) && byId(dragId).parentId !== newParent) ? validateParent(dragId, newParent) : null;
            targetRow.classList.add(err2 ? "drop-invalid" : (zone === "before" ? "insert-before" : "insert-after"));
        }
    }

    function onRowUp(e) {
        window.removeEventListener("pointermove", onRowMove);

        if (rowGhost) { rowGhost.remove(); rowGhost = null; }
        var draggedRow = document.querySelector("#planningView .plan-row[data-id='" + dragId + "']");
        if (draggedRow) draggedRow.classList.remove("dragging");

        var rz = document.getElementById("planRootzone");
        var wasArmed = rz && rz.classList.contains("detach-armed");
        clearDropHints();
        if (rz) rz.classList.remove("detach-armed");

        if (dragId != null) {
            if (dropState && dropState.targetId !== dragId) {
                if (dropState.zone === "child") setParent(dragId, dropState.targetId, {});
                else reorderRow(dragId, dropState.targetId, dropState.zone);
            } else if (wasArmed) {
                setParent(dragId, null, {});
            }
        }
        dragId = null; dropState = null;
    }

    /* middle third = nest as child, top/bottom edges = reorder within parent */
    function dropZoneImpl(y, rect) {
        if (y < rect.top + rect.height * 0.28) return "before";
        if (y > rect.top + rect.height * 0.72) return "after";
        return "child";
    }

    /* reorder within parent: move dragId before or after targetId */
    function reorderRow(dId, tId, pos) {
        if (!canEdit) { toast("warning", "Read-only \u2014 a Release Manager license is required to edit."); return; }
        if (dId === tId) return;
        var drag = byId(dId), target = byId(tId);
        if (!drag || !target) return;
        var newParent = target.parentId != null ? target.parentId : null;
        var parentChanged = drag.parentId !== newParent;
        if (parentChanged) {
            var err = validateParent(dId, newParent);
            if (err) { toast("error", err); return; }
        }
        items.splice(items.indexOf(drag), 1);
        var idx = items.indexOf(target);
        if (pos === "after") idx += 1;
        items.splice(idx, 0, drag);

        // Infer predecessor → successor from drop position:
        //   drop BEFORE target → drag is predecessor of target
        //   drop AFTER  target → target is predecessor of drag
        var predId = pos === "before" ? dId : tId;
        var succId = pos === "before" ? tId : dId;
        var depErr = validateDependency(predId, succId);

        render();

        if (parentChanged) setParent(dId, newParent, {});
        if (!depErr) {
            addDependency(predId, succId, { silent: true });
            toast("success", "#" + predId + " set to run before #" + succId);
        } else if (!parentChanged) {
            var why = depErr.indexOf("already") >= 0 ? "dependency already set" : "sibling order isn't saved";
            toast("info", "No change saved \u2014 " + why + ".");
        }
    }

    function wireTree() {
        var rows = document.querySelectorAll("#planningView .plan-row");
        Array.prototype.forEach.call(rows, function (row) {
            row.addEventListener("pointerdown", startRowDrag);
        });
        Array.prototype.forEach.call(document.querySelectorAll("#planningView [data-caret]"), function (c) {
            c.addEventListener("click", function (e) {
                e.stopPropagation(); var id = Number(c.getAttribute("data-caret"));
                collapsed[id] = !collapsed[id]; renderTree();
            });
        });
        Array.prototype.forEach.call(document.querySelectorAll("#planningView [data-dep]"), function (h) {
            h.addEventListener("pointerdown", startDepDrag);
        });
        Array.prototype.forEach.call(document.querySelectorAll("#planningView [data-links]"), function (b) {
            b.addEventListener("click", function (e) { e.stopPropagation(); openLinks(Number(b.getAttribute("data-links"))); });
        });
        // root detach handled by onRowUp
    }

    /* dependency pointer-drag */
    var depDragging = false, depFrom = null;
    function depLine() { return document.getElementById("planDepLine"); }
    function startDepDrag(e) {
        e.preventDefault(); e.stopPropagation();
        if (!canEdit) return;
        depDragging = true; depFrom = Number(e.currentTarget.getAttribute("data-dep"));
        var r = e.currentTarget.getBoundingClientRect(); var ln = depLine();
        ln.style.display = ""; ln.setAttribute("x1", r.left + r.width / 2); ln.setAttribute("y1", r.top + r.height / 2);
        moveDepLine(e);
        window.addEventListener("pointermove", moveDepLine);
        window.addEventListener("pointerup", endDepDrag, { once: true });
    }
    function moveDepLine(e) {
        var ln = depLine(); ln.setAttribute("x2", e.clientX); ln.setAttribute("y2", e.clientY);
        Array.prototype.forEach.call(document.querySelectorAll("#planningView .dep-target"), function (r) { r.classList.remove("dep-target"); });
        var el = document.elementFromPoint(e.clientX, e.clientY);
        var row = el && el.closest ? el.closest(".plan-row") : null;
        if (row && Number(row.getAttribute("data-id")) !== depFrom) row.classList.add("dep-target");
    }
    function endDepDrag(e) {
        depDragging = false; depLine().style.display = "none";
        window.removeEventListener("pointermove", moveDepLine);
        Array.prototype.forEach.call(document.querySelectorAll("#planningView .dep-target"), function (r) { r.classList.remove("dep-target"); });
        var el = document.elementFromPoint(e.clientX, e.clientY);
        var row = el && el.closest ? el.closest(".plan-row") : null;
        if (row) { var target = Number(row.getAttribute("data-id")); if (target !== depFrom) addDependency(depFrom, target, {}); }
        depFrom = null;
    }

    /* ---------------- relationships modal (accessible editor) ---------------- */
    function ensureModal() {
        var bd = document.getElementById("planModalBackdrop");
        if (bd) return bd;
        bd = document.createElement("div"); bd.id = "planModalBackdrop"; bd.className = "plan-modal-backdrop";
        bd.innerHTML = '<div class="plan-modal"><h3>\uD83D\uDD17 Relationships \u2014 <span id="planModalTitle"></span></h3>' +
            '<div class="plan-body" id="planModalBody"></div>' +
            '<div class="plan-foot"><button type="button" class="plan-btn" id="planModalClose">Close</button></div></div>';
        document.body.appendChild(bd);
        bd.addEventListener("click", function (e) { if (e.target.id === "planModalBackdrop") bd.classList.remove("show"); });
        bd.querySelector("#planModalClose").addEventListener("click", function () { bd.classList.remove("show"); });
        return bd;
    }
    function openLinks(id) {
        var bd = ensureModal();
        var it = byId(id);
        document.getElementById("planModalTitle").textContent = "#" + id + " " + (it.title || "");
        var parent = it.parentId != null ? byId(it.parentId) : null;
        var kids = children(id);
        var preds = it.preds.map(byId);
        var succs = successorsOf(id).map(byId);
        var ro = !canEdit;
        function rm(attr, val, title) { return ro ? "" : '<button class="x" data-' + attr + '="' + val + '" title="' + title + '">\u00d7</button>'; }
        function opt(arr) { return arr.map(function (o) { return '<option value="' + o.id + '">#' + o.id + " " + esc(o.title) + "</option>"; }).join(""); }
        var parentCandidates = items.filter(function (o) { return o.id !== id && !validateParent(id, o.id); });
        var predCandidates = items.filter(function (o) { return o.id !== id && !validateDependency(o.id, id); });

        document.getElementById("planModalBody").innerHTML =
            (ro ? '<div class="plan-ro-note">\uD83D\uDC41 Read-only (Standard license) \u2014 editing requires a Release Manager license.</div>' : '') +
            '<div class="plan-rel-group"><h4>Parent</h4>' +
            (parent ? '<div class="plan-rel-item">' + typeBadge(parent.type) + '<span class="plan-title">#' + parent.id + " " + esc(parent.title) + "</span>" + rm("rmparent", id, "Remove parent") + "</div>"
                : '<div class="plan-hint">No parent (top-level).</div>') +
            (ro ? "" : '<select class="plan-picker" id="planSetParent"><option value="">Set parent\u2026</option>' + opt(parentCandidates) + "</select>") +
            "</div>" +
            '<div class="plan-rel-group"><h4>Children</h4>' +
            (kids.length ? kids.map(function (k) { return '<div class="plan-rel-item">' + typeBadge(k.type) + '<span class="plan-title">#' + k.id + " " + esc(k.title) + "</span>" + rm("detach", k.id, "Detach child") + "</div>"; }).join("") : '<div class="plan-hint">None.</div>') +
            "</div>" +
            '<div class="plan-rel-group"><h4>Predecessors (must finish before this)</h4>' +
            (preds.length ? preds.map(function (p) { return '<div class="plan-rel-item">' + typeBadge(p.type) + '<span class="plan-title">#' + p.id + " " + esc(p.title) + "</span>" + rm("rmpred", p.id, "Remove") + "</div>"; }).join("") : '<div class="plan-hint">None.</div>') +
            (ro ? "" : '<select class="plan-picker" id="planAddPred"><option value="">Add predecessor\u2026</option>' + opt(predCandidates) + "</select>") +
            "</div>" +
            '<div class="plan-rel-group"><h4>Successors (start after this)</h4>' +
            (succs.length ? succs.map(function (s) { return '<div class="plan-rel-item">' + typeBadge(s.type) + '<span class="plan-title">#' + s.id + " " + esc(s.title) + "</span>" + rm("rmsucc", s.id, "Remove") + "</div>"; }).join("") : '<div class="plan-hint">None.</div>') +
            "</div>";

        var body = document.getElementById("planModalBody");
        var sp = body.querySelector("#planSetParent");
        if (sp) sp.addEventListener("change", function (e) { if (e.target.value) setParent(id, Number(e.target.value), {}).then(function (ok) { if (ok) openLinks(id); }); });
        var ap = body.querySelector("#planAddPred");
        if (ap) ap.addEventListener("change", function (e) { if (e.target.value) addDependency(Number(e.target.value), id, {}).then(function (ok) { if (ok) openLinks(id); }); });
        bindX(body, "rmparent", function () { setParent(id, null, {}).then(function (ok) { if (ok) openLinks(id); }); });
        bindX(body, "detach", function (v) { setParent(v, null, {}).then(function (ok) { if (ok) openLinks(id); }); });
        bindX(body, "rmpred", function (v) { removeDependency(v, id, {}).then(function (ok) { if (ok) openLinks(id); }); });
        bindX(body, "rmsucc", function (v) { removeDependency(id, v, {}).then(function (ok) { if (ok) openLinks(id); }); });

        bd.classList.add("show");
    }
    function bindX(body, attr, fn) {
        Array.prototype.forEach.call(body.querySelectorAll("[data-" + attr + "]"), function (b) {
            b.addEventListener("click", function () { fn(Number(b.getAttribute("data-" + attr))); });
        });
    }

    /* ---------------- render: timeline ---------------- */
    var DAY = 86400000;
    function orderedRows() {
        var out = [], seen = {};
        function walk(pid, depth) { children(pid).forEach(function (c) { if (seen[c.id]) return; seen[c.id] = true; out.push({ it: c, depth: depth }); walk(c.id, depth + 1); }); }
        items.filter(function (i) { return i.parentId == null; }).forEach(function (r) { if (seen[r.id]) return; seen[r.id] = true; out.push({ it: r, depth: 0 }); walk(r.id, 1); });
        return out;
    }
    function renderTimeline() {
        var host = document.getElementById("planTimelineView");
        if (!host) return;
        var dated = items.filter(function (i) { return i.start && i.end; });
        if (!dated.length) { host.innerHTML = '<p class="plan-hint" style="padding:20px">No scheduled dates on these work items. Set Start/End dates to see the timeline.</p>'; return; }
        var allMs = [];
        dated.forEach(function (i) { allMs.push(new Date(i.start).getTime(), new Date(i.end).getTime()); });
        var min = Math.min.apply(null, allMs), max = Math.max.apply(null, allMs);
        var pad = Math.max(1, Math.round(((max - min) / DAY) * 0.06)) * DAY;
        min -= pad; max += pad;
        var LABEL_W = 280, PX_PER_DAY = 42;
        var trackW = Math.max(760, Math.round(((max - min) / DAY) * PX_PER_DAY));
        var innerW = LABEL_W + trackW;
        function pct(d) { return (new Date(d).getTime() - min) / (max - min) * 100; }

        var months = []; var m = new Date(min); m = new Date(m.getFullYear(), m.getMonth(), 1);
        while (m.getTime() <= max) { months.push(new Date(m)); m = new Date(m.getFullYear(), m.getMonth() + 1, 1); }

        var conflicts = {};
        items.forEach(function (s) { s.preds.forEach(function (pid) { var p = byId(pid); if (p && p.end && s.start && new Date(s.start) < new Date(p.end)) conflicts[s.id] = true; }); });

        var rows = orderedRows();
        var html = '<div class="plan-tl"><div class="plan-tl-inner" style="width:' + innerW + 'px">' +
            '<div class="plan-tl-row"><div class="plan-tl-label"><b>Work item</b></div><div class="plan-tl-months">' +
            months.map(function (mo) { return '<div class="plan-tl-month">' + mo.toLocaleString("en", { month: "short" }) + " '" + String(mo.getFullYear()).slice(2) + "</div>"; }).join("") +
            '</div></div><div id="planTlBody" style="position:relative">';
        rows.forEach(function (r) {
            var it = r.it; var hasDates = it.start && it.end;
            var bar = hasDates ? '<div class="plan-tl-bar ' + (conflicts[it.id] ? "conflict" : "") + '" title="' + esc(it.title) + '" style="left:' + pct(it.start) + '%; width:' + Math.max(2, pct(it.end) - pct(it.start)) + '%; background:' + typeColor(it.type) + '">' + esc(it.title) + '</div>' : '';
            html += '<div class="plan-tl-row" data-tl="' + it.id + '"><div class="plan-tl-label" style="padding-left:' + (12 + r.depth * 16) + 'px">' + typeBadge(it.type) + ' #' + it.id + " " + esc(it.title) + '</div><div class="plan-tl-track">' + bar + '</div></div>';
        });
        html += '<svg id="planTlArrows" style="position:absolute; inset:0; pointer-events:none;"></svg></div></div></div>';
        host.innerHTML = html;
        setTimeout(function () { drawArrows(conflicts); }, 0);
    }
    function drawArrows(conflicts) {
        var svg = document.getElementById("planTlArrows");
        var body = document.getElementById("planTlBody");
        if (!svg || !body) return;
        var hb = body.getBoundingClientRect();
        svg.setAttribute("width", body.scrollWidth); svg.setAttribute("height", body.scrollHeight);
        function rect(id) { var el = body.querySelector('[data-tl="' + id + '"] .plan-tl-bar'); if (!el) return null; var b = el.getBoundingClientRect(); return { x1: b.left - hb.left, x2: b.right - hb.left, y: b.top - hb.top + b.height / 2 }; }
        items.forEach(function (s) {
            s.preds.forEach(function (pid) {
                var a = rect(pid), b = rect(s.id); if (!a || !b) return;
                var sx = a.x2, sy = a.y, tx = b.x1, ty = b.y, mx = sx + Math.max(18, (tx - sx) / 2);
                var p = document.createElementNS("http://www.w3.org/2000/svg", "path");
                p.setAttribute("d", "M " + sx + " " + sy + " C " + mx + " " + sy + ", " + mx + " " + ty + ", " + tx + " " + ty);
                if (conflicts[s.id]) p.setAttribute("class", "conflict");
                svg.appendChild(p);
            });
        });
    }
})();
