// menu-model.js — the declarative source of truth for the browse tool's
// action menus (right-click row menu, right-click pane menu, the keyboard
// menu key, and the hover kebab).
//
// Every action is declared ONCE as a descriptor. The row/pane menus are
// projections over that list, filtered by surface + an `appliesTo` TYPE
// predicate and annotated with an `enabled` CAPABILITY predicate:
//
//   appliesTo(ctx) === false  → the item is OMITTED (it doesn't make sense
//                               for this target — e.g. "New folder" on a
//                               file row, "Expand" on a file).
//   appliesTo true, enabled
//     (ctx) === false         → the item is SHOWN DISABLED with a tooltip
//                               naming what's required (write access /
//                               create access / project-admin / site-admin).
//
// That hybrid realizes the cumulative guest ⊂ project-team ⊂ sub-admin ⊂
// admin menus: a lower tier SEES higher-tier actions greyed and learns they
// exist, while type-irrelevant noise is hidden.
//
// Roles are NOT hardcoded: ordinary actions gate on the verbs the server
// returns per entry (node.verbs) or per scope (cap.at → path_verbs), so any
// operator-defined role works. Only two intrinsically-special tiers are
// recognised by name — site admin (is_super_admin / IsAdmin) and project /
// subtree admin (path_is_admin / IsSubtreeAdmin) — because they govern
// administration itself and can't be expressed as a plain verb bundle.
//
// Deliberately data-shaped so a future server-sourced manifest (zddc.zip)
// can supply or extend the descriptors without touching the tool code.
(function () {
    'use strict';

    if (!window.app || !window.app.modules) return;

    var state = window.app.state;

    // Action implementations are injected by events.init() via configure()
    // to avoid an events ↔ menu-model circular dependency. Everything else
    // (tree, preview, download, workflow modules) is reached through
    // window.app.modules at call time.
    var act = {};
    function configure(a) { act = a || {}; }

    // ── Predicates ────────────────────────────────────────────────────────

    function isServer() { return state.source === 'server'; }
    function appliesToFolderLike(node) { return !!(node && (node.isDir || node.isZip)); }
    function appliesToFile(node) { return !!(node && !node.isDir && !node.isZip); }

    // The Export submenu's convertible-format set comes from the download
    // module's canonical matrix (download.exportTargets), which mirrors the
    // server's conversion matrix — the single source of truth shared with the
    // markdown editor's DOCX/HTML/PDF buttons. exportTargets(ext) returns the
    // target formats for a source extension (e.g. md → docx, html, pdf), or []
    // when the extension isn't a convertible source.
    function exportTargets(ext) {
        var d = window.app.modules.download;
        return (d && d.exportTargets) ? d.exportTargets(ext) : [];
    }
    function cap() { return window.zddc && window.zddc.cap; }

    function canVerb(node, verb) {
        return !!(node && cap() && cap().has(node, verb));
    }
    function pathHasVerb(access, verb) {
        return !!(access && typeof access.path_verbs === 'string'
            && access.path_verbs.indexOf(verb) !== -1);
    }
    function isSiteAdmin(access) { return !!(access && access.is_super_admin); }
    function isSubtreeAdminHere(access) { return !!(access && access.path_is_admin); }

    // Create / mutate / admin actions are HIDDEN when the user can't perform
    // them (capability folded into appliesTo), so these gates only need the
    // boolean — the `missing` field is retained for potential future tooltips.

    // Rename/Delete gate — preserves today's compose exactly: canMutate rules
    // out un-writable sources (offline FS without a handle, zip members,
    // virtual placeholders) with no tooltip; when the server cascade reports
    // verbs, the per-entry ACL bit gates with a tooltip. FS / Caddy (no verbs
    // field) fall back to canMutate alone. Returns { enabled, missing }.
    function verbGate(node, verb) {
        var up = window.app.modules.upload;
        if (!up || !up.canMutate(node)) return { enabled: false, missing: '' };
        if (!isServer() || !cap()) return { enabled: true, missing: '' };
        if (typeof node.verbs !== 'string') return { enabled: true, missing: '' };
        if (cap().has(node, verb)) return { enabled: true, missing: '' };
        return { enabled: false, missing: verb };
    }

    // Create gate (New folder / New file). canCreateHere() rules out the
    // no-target case (offline FS without a picked handle) — no tooltip there.
    // In server mode, gate on the 'c' verb: per-node for a folder row, per
    // scope for the pane. Unknown verbs → optimistic (server is the final
    // arbiter, surfacing 403 via cap.handleForbidden, exactly as today).
    function createGate(ctx) {
        if (!act.canCreateHere || !act.canCreateHere()) return { enabled: false, missing: '' };
        if (!isServer()) return { enabled: true, missing: '' };
        if (ctx.node) { // folder-row create → inside this folder
            if (typeof ctx.node.verbs === 'string') {
                return canVerb(ctx.node, 'c')
                    ? { enabled: true, missing: '' }
                    : { enabled: false, missing: 'c' };
            }
            return { enabled: true, missing: '' };
        }
        // pane create → current scope
        if (ctx.access && typeof ctx.access.path_verbs === 'string') {
            return pathHasVerb(ctx.access, 'c')
                ? { enabled: true, missing: '' }
                : { enabled: false, missing: 'c' };
        }
        return { enabled: true, missing: '' };
    }

    // "Edit access rules" (.zddc) — the sub-admin / site-admin tier item.
    // Enabled per-node when the entry grants the admin verb 'a', else by the
    // scope's subtree-admin / site-admin status (admin authority cascades
    // down a subtree). Returns { enabled, missing }.
    function manageAccessGate(ctx) {
        if (ctx.node && canVerb(ctx.node, 'a')) return { enabled: true, missing: '' };
        if (isSubtreeAdminHere(ctx.access) || isSiteAdmin(ctx.access)) return { enabled: true, missing: '' };
        return { enabled: false, missing: 'subtree-admin' };
    }

    function insideZip(node) {
        // Creating inside a zip member is impossible — the server can't PUT
        // into an archive. Mirror tree.zipNestedInsideZip's URL heuristic.
        if (!node) return false;
        if (node.url && /\.zip\//i.test(node.url)) return true;
        if (node.handle && node.handle.isZipEntry) return true;
        return false;
    }

    // ── Descriptors ─────────────────────────────────────────────────────────
    // group order = visual order; a separator is inserted on each group change
    // among the items that actually render (context-menu.js collapses extras).
    var DESCRIPTORS = [
        // ── open ──
        {
            id: 'open', group: 'open', surfaces: ['row'],
            label: function (ctx) {
                if (ctx.node.isDir) return 'Open';
                if (ctx.node.isZip) return 'Open archive';
                return 'Preview';
            },
            appliesTo: function (ctx) { return !ctx.node.virtual; },
            action: function (ctx) {
                if (ctx.node.isDir) {
                    // Open = navigate into the folder (rescope). Inline
                    // expand stays on single-click / chevron / arrow keys.
                    if (act.navigateIntoFolder) act.navigateIntoFolder(ctx.node);
                } else if (ctx.node.isZip) {
                    // A zip can't be navigated into — expand it inline.
                    var t = window.app.modules.tree;
                    if (t) t.toggleFolder(ctx.node.id);
                } else {
                    var p = window.app.modules.preview;
                    if (p) p.showFilePreview(ctx.node);
                }
            }
        },
        {
            id: 'open-new-tab', group: 'open', surfaces: ['row'],
            label: 'Open in new tab', accel: 'Ctrl+Click',
            appliesTo: function (ctx) { return !!ctx.node.url; },
            action: function (ctx) { window.open(ctx.node.url, '_blank', 'noopener'); }
        },
        {
            id: 'popout', group: 'open', surfaces: ['row'],
            label: 'Pop out preview',
            appliesTo: function (ctx) { return appliesToFile(ctx.node) && !ctx.node.virtual; },
            action: function (ctx) {
                var p = window.app.modules.preview;
                if (p) p.showFilePreview(ctx.node, { popup: true });
            }
        },

        // ── io ──
        {
            id: 'download', group: 'io', surfaces: ['row'],
            label: function (ctx) { return ctx.node.isDir ? 'Download ZIP' : 'Download'; },
            appliesTo: function (ctx) { return !ctx.node.virtual; },
            action: function (ctx) {
                var d = window.app.modules.download;
                if (!d) return;
                if (ctx.node.isDir) d.downloadFolder(ctx.node);
                else d.downloadFile(ctx.node);
            }
        },
        {
            // Export submenu: a folder offers ".zip" (both modes); a convertible
            // file (md/docx/html) offers its server-side conversion targets —
            // md → docx/html/pdf, docx → md/html, html → md/docx (server mode
            // only). A zip is already an archive — no Export.
            id: 'export', group: 'io', surfaces: ['row'],
            label: 'Export',
            appliesTo: function (ctx) {
                var n = ctx.node;
                if (!n || n.virtual) return false;
                if (n.isDir) return true;
                if (n.isZip) return false;
                return isServer() && exportTargets(n.ext).length > 0;
            },
            items: function (ctx) {
                var n = ctx.node;
                var d = window.app.modules.download;
                if (!d) return [];
                if (n.isDir) {
                    return [{ label: '.zip', action: function () { d.downloadFolder(n); } }];
                }
                // exportTargets already excludes the source format.
                return exportTargets(n.ext).map(function (fmt) {
                    return { label: '.' + fmt, action: function () { d.exportFile(n, fmt); } };
                });
            }
        },

        // ── create (folder rows + pane; NOT file rows) ──
        // Create actions are HIDDEN unless the user can create here (the
        // capability is folded into appliesTo, not greyed). On a row they
        // apply to folders only (create inside); on the pane, to the scope.
        {
            id: 'new-folder', group: 'create', surfaces: ['row', 'pane'],
            label: 'New folder',
            appliesTo: function (ctx) {
                var typeOk = ctx.surface === 'pane'
                    || (appliesToFolderLike(ctx.node) && !insideZip(ctx.node));
                return typeOk && createGate(ctx).enabled;
            },
            action: function (ctx) { if (act.createInDir) act.createInDir(ctx.dir, 'folder'); }
        },
        {
            id: 'new-file', group: 'create', surfaces: ['row', 'pane'],
            label: 'New file',
            appliesTo: function (ctx) {
                var typeOk = ctx.surface === 'pane'
                    || (appliesToFolderLike(ctx.node) && !insideZip(ctx.node));
                return typeOk && createGate(ctx).enabled;
            },
            action: function (ctx) { if (act.createInDir) act.createInDir(ctx.dir, 'markdown'); }
        },
        {
            id: 'create-transmittal', group: 'create', surfaces: ['pane'],
            label: 'Create Transmittal folder…',
            appliesTo: function () { return isServer() && state.scopeCanonicalFolder === 'staging'; },
            action: function () {
                var ct = window.app.modules.createTransmittal;
                if (ct) ct.invoke();
            }
        },

        // ── mutate (HIDDEN unless permitted — capability folded into appliesTo) ──
        {
            id: 'rename', group: 'mutate', surfaces: ['row'],
            label: 'Rename…',
            appliesTo: function (ctx) { return !ctx.node.virtual && verbGate(ctx.node, 'w').enabled; },
            action: function (ctx) { if (act.renameNode) act.renameNode(ctx.node); }
        },
        {
            id: 'delete', group: 'mutate', surfaces: ['row'], danger: true,
            label: 'Delete…',
            appliesTo: function (ctx) { return !ctx.node.virtual && verbGate(ctx.node, 'd').enabled; },
            action: function (ctx) { if (act.deleteNode) act.deleteNode(ctx.node); }
        },

        // ── treeops (folder/zip rows only) ──
        {
            id: 'expand-subtree', group: 'treeops', surfaces: ['row'],
            label: 'Expand subtree', accel: 'Shift+Click',
            appliesTo: function (ctx) { return appliesToFolderLike(ctx.node); },
            action: function (ctx) {
                var t = window.app.modules.tree;
                if (t) t.expandSubtree(ctx.node.id);
            }
        },
        {
            id: 'collapse-subtree', group: 'treeops', surfaces: ['row'],
            label: 'Collapse subtree',
            appliesTo: function (ctx) { return appliesToFolderLike(ctx.node); },
            action: function (ctx) {
                var t = window.app.modules.tree;
                if (t) t.collapseSubtree(ctx.node.id);
            }
        },

        // ── workflow (already type+scope gated → omitted when N/A) ──
        {
            id: 'plan-review', group: 'workflow', surfaces: ['row'],
            label: 'Plan Review…',
            appliesTo: function (ctx) {
                if (!isServer() || !state.scopeOnPlanReview) return false;
                var pr = window.app.modules.planReview;
                return !!(pr && pr.isReceivedTrackingFolder(ctx.node));
            },
            action: function (ctx) {
                var pr = window.app.modules.planReview;
                if (pr) pr.invoke(ctx.node);
            }
        },
        {
            id: 'accept-transmittal', group: 'workflow', surfaces: ['row'],
            label: 'Accept Transmittal…',
            appliesTo: function (ctx) {
                if (!isServer()) return false;
                var at = window.app.modules.acceptTransmittal;
                return !!(at && at.isAcceptableTransmittalFolder(ctx.node));
            },
            action: function (ctx) {
                var at = window.app.modules.acceptTransmittal;
                if (at) at.invoke(ctx.node);
            }
        },
        {
            id: 'stage', group: 'workflow', surfaces: ['row'],
            label: 'Stage to…',
            appliesTo: function (ctx) {
                if (!isServer()) return false;
                var s = window.app.modules.stage;
                return !!(s && s.isStageableFile(ctx.node));
            },
            action: function (ctx) {
                var s = window.app.modules.stage;
                if (s) s.invokeStage(ctx.node);
            }
        },
        {
            id: 'unstage', group: 'workflow', surfaces: ['row'],
            label: 'Unstage to working/',
            appliesTo: function (ctx) {
                if (!isServer()) return false;
                var s = window.app.modules.stage;
                return !!(s && s.isUnstageableFile(ctx.node));
            },
            action: function (ctx) {
                var s = window.app.modules.stage;
                if (s) s.invokeUnstage(ctx.node);
            }
        },
        {
            id: 'history', group: 'workflow', surfaces: ['row'],
            label: 'History…',
            appliesTo: function (ctx) {
                if (!isServer()) return false;
                var n = ctx.node;
                return appliesToFile(n) && !n.virtual && !!n.history;
            },
            action: function (ctx) {
                var h = window.app.modules.history;
                if (h) h.open(ctx.node);
            }
        },

        // ── admin / sub-admin tier ──
        {
            // HIDDEN unless the user can actually edit access rules here
            // (admin verb 'a', or subtree/site admin) — not shown greyed.
            id: 'manage-access', group: 'admin', surfaces: ['row', 'pane'],
            label: 'Edit access rules…',
            appliesTo: function (ctx) {
                if (!isServer()) return false; // server-only tier
                var typeOk = ctx.surface === 'pane' || appliesToFolderLike(ctx.node);
                return typeOk && manageAccessGate(ctx).enabled;
            },
            action: function (ctx) { openZddcEditor(ctx.dir); }
        },

        // ── view (pane) ──
        {
            id: 'refresh', group: 'view', surfaces: ['pane'],
            label: 'Refresh', accel: 'F5',
            action: function () { if (act.refreshListing) act.refreshListing(); }
        }
    ];

    // Open the `.zddc` for `dir` in the YAML editor. Prefer an existing tree
    // node (carries verbs/virtual flags) else synthesize one; the yaml plugin
    // recognises name === '.zddc' and gates the save on the admin verb 'a'.
    function openZddcEditor(dir) {
        var url = (dir || '/');
        if (!url.endsWith('/')) url += '/';
        url += '.zddc';
        var found = null;
        var t = window.app.modules.tree;
        state.nodes.forEach(function (n) {
            if (found || n.name !== '.zddc' || !t) return;
            if (t.pathFor(n) === url) found = n;
        });
        var node = found || { url: url, name: '.zddc', ext: '' };
        var p = window.app.modules.preview;
        if (p) p.showFilePreview(node);
    }

    // ── Projection ────────────────────────────────────────────────────────

    function resolve(v, ctx) { return typeof v === 'function' ? v(ctx) : v; }
    function resolveBool(v, ctx, dflt) {
        if (v === undefined) return dflt;
        return !!(typeof v === 'function' ? v(ctx) : v);
    }

    function toMenuItem(d, ctx) {
        var item = {
            label: resolve(d.label, ctx),
            accel: d.accel,
            danger: d.danger,
            // disabled / tooltip ignore the menu's own context arg — ctx is
            // already captured here with the richer browse context.
            disabled: function () { return !resolveBool(d.enabled, ctx, true); },
            tooltip: function () {
                return resolveBool(d.enabled, ctx, true) ? '' : (resolve(d.tooltip, ctx) || '');
            }
        };
        // A descriptor with `items` becomes a submenu (resolved against the
        // captured browse ctx); otherwise it's a normal action row.
        if (d.items) {
            item.items = function () { return resolve(d.items, ctx); };
        } else {
            item.action = function () { if (d.action) d.action(ctx); };
        }
        return item;
    }

    function project(surface, ctx) {
        var out = [];
        var lastGroup = null;
        for (var i = 0; i < DESCRIPTORS.length; i++) {
            var d = DESCRIPTORS[i];
            if (d.surfaces.indexOf(surface) === -1) continue;
            if (!resolveBool(d.appliesTo, ctx, true)) continue;
            if (lastGroup !== null && d.group !== lastGroup) out.push({ separator: true });
            lastGroup = d.group;
            out.push(toMenuItem(d, ctx));
        }
        return out; // context-menu.js collapses leading/trailing/dup separators
    }

    function buildRowItems(node, row, access) {
        var dir = act.parentDirFor ? act.parentDirFor(node) : (state.currentPath || '/');
        return project('row', { node: node, row: row, surface: 'row', dir: dir, access: access });
    }
    function buildPaneItems(access) {
        var dir = state.currentPath || '/';
        return project('pane', { node: null, row: null, surface: 'pane', dir: dir, access: access });
    }

    window.app.modules.menuModel = {
        configure: configure,
        buildRowItems: buildRowItems,
        buildPaneItems: buildPaneItems,
        DESCRIPTORS: DESCRIPTORS // exposed for tests
    };
})();
