// shared/cap.js — client-side capability helpers for permission gating.
//
// Three small helpers, exposed under window.zddc.cap, that wrap the
// server's verbs / /.profile/access?path / 403 missing_verb surface:
//
//   zddc.cap.at(path)            — Promise<AccessView|null>. Fetches
//                                  /.profile/access?path=<urlpath> and
//                                  memoises per-path for the session.
//                                  Used by tools to gate top-of-page
//                                  affordances (Publish, +Add row,
//                                  +New folder) on PathVerbs.
//   zddc.cap.has(node, verb)     — boolean. Reads node.verbs (string
//                                  "rwcda"-subset) for the listed verb.
//                                  Transition: falls back to
//                                  node.writable for 'w' when verbs
//                                  is absent, so the legacy field still
//                                  drives gating on old listings.
//   zddc.cap.handleForbidden(resp, opts) — given a 403 fetch Response,
//                                  parses the JSON body for
//                                  missing_verb and renders a toast.
//                                  Offers "Elevate" when the path's
//                                  /.profile/access?path= reports a
//                                  path_can_elevate_grant covering the
//                                  missing verb.
//
// Tools using this module must concat shared/cap.js AFTER shared/
// toast.js (toast dependency) and shared/elevation.js (cookie shape).
(function () {
    'use strict';

    if (!window.zddc) window.zddc = {};
    if (window.zddc.cap) return;

    var pathCache = new Map(); // path → AccessView (or null sentinel)

    async function fetchAccess(path) {
        // file:// pages have no server to fetch /.profile/access from;
        // calling fetch() there logs a browser-level error before our
        // catch even runs. Short-circuit so offline tools (browse on
        // a picked folder, form opened from a file URL) silently
        // degrade to "no path-scoped info, fall back to existing
        // gating signals".
        if (location.protocol !== 'http:' && location.protocol !== 'https:') {
            return null;
        }
        try {
            var url = '/.profile/access';
            if (path) url += '?path=' + encodeURIComponent(path);
            var resp = await fetch(url, {
                headers: { 'Accept': 'application/json' },
                credentials: 'same-origin',
                cache: 'no-cache'
            });
            if (!resp.ok) return null;
            return await resp.json();
        } catch (_e) {
            return null;
        }
    }

    // at(path) — fetch path-scoped access view, memoised per path
    // within the page session. Cache is page-scoped: any elevation
    // toggle forces a hard reload (see shared/elevation.js), which
    // resets the cache so stale-after-elevation isn't a concern. Pass
    // null/undefined for the global view (no ?path=).
    async function at(path) {
        var key = path || '';
        if (pathCache.has(key)) return pathCache.get(key);
        var view = await fetchAccess(path);
        pathCache.set(key, view);
        return view;
    }

    // has(node, verb) — check a per-entry verbs string for a single
    // verb. Verb is a one-character string ('r'|'w'|'c'|'d'|'a').
    // Transition shim: when node.verbs is absent, fall back to
    // node.writable for 'w' so the legacy field keeps editor save
    // buttons working on old listings — drop this fallback once every
    // tool's loader sets node.verbs unconditionally.
    function has(node, verb) {
        if (!node) return false;
        if (typeof node.verbs === 'string') {
            return node.verbs.indexOf(verb) !== -1;
        }
        if (verb === 'w' && typeof node.writable === 'boolean') {
            return node.writable;
        }
        return false;
    }

    // VERB_LABELS — human-readable phrases for the 403 toast. "create"
    // covers both new-file PUT and mkdir; "admin" includes .zddc edits.
    var VERB_LABELS = {
        r: 'read',
        w: 'write',
        c: 'create',
        d: 'delete',
        a: 'edit access rules'
    };

    // handleForbidden(resp, opts) — render a 403 toast naming the
    // missing verb. opts.path (optional) is the URL the failed request
    // hit; when provided, the helper consults /.profile/access?path= to
    // decide whether to offer an Elevate action. opts.context is an
    // optional string prefix shown before the verb message ("Save",
    // "Delete", etc.) — purely cosmetic.
    //
    // Best-effort: when the body isn't JSON or missing_verb is
    // absent, falls back to a plain "Forbidden" toast. Returns the
    // Promise so callers can await before chaining.
    async function handleForbidden(resp, opts) {
        opts = opts || {};
        var missing = '';
        try {
            var body = await resp.clone().json();
            if (body && typeof body.missing_verb === 'string') {
                missing = body.missing_verb;
            }
        } catch (_e) { /* non-JSON body */ }

        var prefix = opts.context ? (opts.context + ': ') : '';
        var verbLabel = VERB_LABELS[missing] || missing || '';
        var msg;
        if (verbLabel) {
            msg = prefix + 'You do not have ' + verbLabel + ' access here.';
        } else {
            msg = prefix + 'Forbidden.';
        }

        // Optional elevate offer: only when the caller supplied a
        // path AND the path-scoped access view reports an elevation
        // grant covering the missing verb. Render as a clickable
        // action appended to the toast message; clicking sets the
        // elevation cookie and reloads, matching the header toggle.
        var canOffer = false;
        if (opts.path && missing) {
            var view = await at(opts.path);
            if (view && typeof view.path_can_elevate_grant === 'string'
                && view.path_can_elevate_grant.indexOf(missing) !== -1) {
                canOffer = true;
            }
        }

        var toastFn = (window.zddc && window.zddc.toast) || function () {};
        var el = toastFn(msg, 'error', { durationMs: 8000 });
        if (canOffer && el && el.appendChild) {
            var btn = document.createElement('button');
            btn.type = 'button';
            btn.className = 'zddc-toast__action';
            btn.textContent = 'Elevate';
            btn.addEventListener('click', function (ev) {
                ev.stopPropagation(); // don't dismiss the toast
                if (window.zddc.elevation && window.zddc.elevation.setElevated) {
                    window.zddc.elevation.setElevated(true);
                    window.location.reload();
                }
            });
            el.appendChild(btn);
        }
    }

    window.zddc.cap = { at: at, has: has, handleForbidden: handleForbidden };
})();
