// hovercard.js — rich-metadata tooltip for tree rows.
//
// Replaces the native title="…" attribute with a custom card that
// surfaces every field we know about for a row: parsed ZDDC fields
// (trackingNumber / revision / status / title / date), type, size,
// modTime, on-server path, and URL. A delayed reveal (~350 ms) keeps
// the card out of the way during fast traversal; it dismisses on
// any click, right-click, scroll, or row change.
//
// Singleton DOM element appended to <body>; positioned fixed.
(function () {
    'use strict';

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

    var SHOW_DELAY_MS = 350;
    // Grace period after the cursor leaves the row before the card
    // hides. Lets the user move INTO the card to select / copy text;
    // the card cancels this timer on mouseenter.
    var HIDE_DELAY_MS = 200;

    var state = window.app.state;
    var card = null;
    var showTimer = null;
    var hideTimer = null;
    var currentRow = null;

    function ensureCard() {
        if (card) return card;
        card = document.createElement('div');
        card.className = 'tree-hovercard';
        card.setAttribute('aria-hidden', 'true');
        // Mouse interaction inside the card: cancel any pending hide
        // so the user can stay in it as long as they want, then re-
        // schedule on leave. Pointer-events:auto in the CSS lets the
        // mouse enter; user-select:text (default) lets them drag a
        // selection; right-click inside fires the browser's native
        // Copy menu since we never call preventDefault for it here.
        card.addEventListener('mouseenter', cancelHide);
        card.addEventListener('mouseleave', scheduleHide);
        document.body.appendChild(card);
        return card;
    }

    function cancelHide() {
        if (hideTimer) { clearTimeout(hideTimer); hideTimer = null; }
    }

    function scheduleHide() {
        cancelHide();
        hideTimer = setTimeout(hide, HIDE_DELAY_MS);
    }

    function hide() {
        if (showTimer) { clearTimeout(showTimer); showTimer = null; }
        cancelHide();
        if (card) card.classList.remove('is-visible');
        currentRow = null;
    }

    // ── Formatting ──

    var escapeHtml = window.app.modules.util.escapeHtml;
    var fmtSize = window.app.modules.util.fmtSize;

    function fmtDate(d) {
        if (!d) return '';
        var pad = function (n) { return n < 10 ? '0' + n : '' + n; };
        return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate())
            + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes());
    }

    function typeLabelFor(node) {
        if (node.isDir) return 'Folder';
        if (node.isZip) return 'Zip archive';
        if (node.ext) return node.ext.toUpperCase() + ' file';
        return 'File';
    }

    var VERB_NAMES = { r: 'read', w: 'write', c: 'create', d: 'delete', a: 'admin' };
    function verbsLabel(verbs) {
        return ['r', 'w', 'c', 'd', 'a']
            .filter(function (v) { return verbs.indexOf(v) !== -1; })
            .map(function (v) { return VERB_NAMES[v]; })
            .join(', ');
    }
    // permsValue renders the per-entry verb set the principal holds here.
    // Server mode: node.verbs ("rwcda" subset). Offline (FS-API) mode has
    // no ACL — access is whatever the filesystem grants.
    function permsValue(verbs) {
        if (typeof verbs !== 'string') {
            return state.source === 'fs' ? 'local folder (filesystem)' : 'unknown';
        }
        if (!verbs) return 'none (read-only)';
        return verbsLabel(verbs) + ' (' + verbs + ')';
    }

    function buildRowsHtml(node) {
        var tree = window.app.modules.tree;
        var z = window.zddc;
        var parsed = null;
        if (z) {
            parsed = node.isDir
                ? z.parseFolder(node.name)
                : z.parseFilename(node.name);
        }

        var html = '';

        // ZDDC fields first when the basename parses.
        if (parsed && parsed.valid) {
            if (parsed.date) html += kv('Date', parsed.date, true);
            if (parsed.trackingNumber) html += kv('Tracking number', parsed.trackingNumber, true);
            if (parsed.revision) html += kv('Revision', parsed.revision, true);
            if (parsed.status) html += kv('Status', parsed.status, true);
            if (parsed.title) html += kv('Title', parsed.title);

            // Archive references — the /<project>/.archive/<tracking>.html
            // URL is the latest issued version (highest base rev), and
            // /<project>/.archive/<tracking>_<rev>.html pins the exact
            // revision the user is currently hovering. The dispatcher
            // canonicalises both forms to project-root so links work
            // from any depth.
            if (parsed.trackingNumber) {
                var fullPath = tree ? tree.pathFor(node) : '';
                var rel = fullPath.replace(/^\/+|\/+$/g, '');
                var firstSeg = rel ? rel.split('/')[0] : '';
                if (firstSeg) {
                    var encProject  = encodeURIComponent(firstSeg);
                    var encTracking = encodeURIComponent(parsed.trackingNumber);
                    var latestUrl   = '/' + encProject + '/.archive/' + encTracking + '.html';
                    var latestLbl   = '.archive/' + parsed.trackingNumber + '.html';
                    html += kvLink('Latest', latestUrl, latestLbl);
                    if (!node.isDir && parsed.revision) {
                        var encRev      = encodeURIComponent(parsed.revision);
                        var inspectUrl  = '/' + encProject + '/.archive/' + encTracking + '_' + encRev + '.html';
                        var inspectLbl  = '.archive/' + parsed.trackingNumber + '_' + parsed.revision + '.html';
                        html += kvLink('This revision', inspectUrl, inspectLbl);
                    }
                }
            }

            html += '<div class="tree-hovercard__sep"></div>';
        } else if (node.displayName) {
            // Operator-supplied display name — only useful as info if
            // it differs from the on-disk name.
            html += kv('Display name', node.displayName);
        }

        html += kv('Type', typeLabelFor(node));
        if (!node.isDir) html += kv('Filename', node.name, true);
        if (!node.isDir && node.size != null) html += kv('Size', fmtSize(node.size));
        if (node.modTime) html += kv('Modified', fmtDate(node.modTime));
        if (node.virtual) html += kv('Virtual', 'Not yet created on disk');

        // ── Effective access for the current principal at this location ──
        // "Your permissions" is the per-entry verb set (sync, from the
        // listing). "Your roles" is cascade-scoped — it can differ by
        // location — so it needs a path-scoped fetch; render a placeholder
        // that fillRoles() updates once /.profile/access?path= resolves.
        html += '<div class="tree-hovercard__sep"></div>';
        html += kv('Your permissions', permsValue(node.verbs));
        if (state.source === 'server') {
            html += '<span class="tree-hovercard__key">Your roles</span>'
                + '<span class="tree-hovercard__val" id="hc-roles">…</span>';
        }

        // URL last (longest, most likely to wrap) — rendered as a clickable
        // link the user can open or right-click to copy. The on-disk path is
        // intentionally omitted; the URL is the shareable reference.
        if (node.url) html += kvLink('URL', node.url, node.url);

        return html;
    }

    function kv(key, val, mono) {
        return '<span class="tree-hovercard__key">' + escapeHtml(key) + '</span>'
            + '<span class="tree-hovercard__val'
            + (mono ? ' tree-hovercard__val--mono' : '')
            + '">' + escapeHtml(val) + '</span>';
    }

    // kvLink — value rendered as an <a> the user can click (opens in
    // a new tab so the hover context isn't lost) or right-click to
    // copy. Used for the .archive references on ZDDC files.
    function kvLink(key, href, label) {
        return '<span class="tree-hovercard__key">' + escapeHtml(key) + '</span>'
            + '<span class="tree-hovercard__val tree-hovercard__val--mono">'
            +   '<a href="' + escapeHtml(href) + '" target="_blank" rel="noopener">'
            +     escapeHtml(label)
            +   '</a>'
            + '</span>';
    }

    function render(node) {
        var z = window.zddc;
        var parsed = z
            ? (node.isDir ? z.parseFolder(node.name) : z.parseFilename(node.name))
            : null;

        var primary, secondary = '';
        if (parsed && parsed.valid) {
            primary = parsed.title;
            var parts = node.isDir
                ? [parsed.date, parsed.trackingNumber, parsed.status]
                : [parsed.trackingNumber, parsed.revision, parsed.status];
            secondary = parts.filter(Boolean).join(' · ');
        } else if (node.displayName) {
            primary = node.displayName;
        } else {
            primary = node.name;
        }

        card.innerHTML = ''
            + '<div class="tree-hovercard__header">'
            +   '<div class="tree-hovercard__title">' + escapeHtml(primary) + '</div>'
            +   (secondary
                    ? '<div class="tree-hovercard__sub">' + escapeHtml(secondary) + '</div>'
                    : '')
            + '</div>'
            + '<div class="tree-hovercard__list">' + buildRowsHtml(node) + '</div>';
    }

    function position(row) {
        // Two-pass measure: temporarily make visible-but-invisible so
        // we can read offsetWidth / offsetHeight, compute placement,
        // then reveal at the final coordinates.
        card.style.left = '0px';
        card.style.top = '0px';
        card.style.visibility = 'hidden';
        card.classList.add('is-visible');
        var cw = card.offsetWidth;
        var ch = card.offsetHeight;
        var rect = row.getBoundingClientRect();
        var GAP = 8;
        var x = rect.right + GAP;
        if (x + cw > window.innerWidth - GAP) {
            x = rect.left - cw - GAP;
        }
        if (x < GAP) {
            // Fallback: anchor under the row (last resort when the
            // pane is wide enough that neither side fits).
            x = Math.max(GAP, Math.min(rect.left, window.innerWidth - cw - GAP));
        }
        var y = rect.top;
        if (y + ch > window.innerHeight - GAP) {
            y = Math.max(GAP, window.innerHeight - ch - GAP);
        }
        if (y < GAP) y = GAP;
        card.style.left = x + 'px';
        card.style.top = y + 'px';
        card.style.visibility = '';
    }

    function showFor(row, node) {
        ensureCard();
        render(node);
        position(row);
        card.classList.add('is-visible');
        fillRoles(row, node);
    }

    // Async-fill the "Your roles" row from the path-scoped access view
    // (zddc.cap.at memoises per path, so repeat hovers are instant).
    // Bails if the card has moved to another row before the fetch lands.
    async function fillRoles(row, node) {
        if (state.source !== 'server') return;
        if (!window.zddc || !window.zddc.cap) return;
        var tree = window.app.modules.tree;
        var path = tree ? tree.pathFor(node) : '';
        if (!path) return;
        var view;
        try { view = await window.zddc.cap.at(path); } catch (_e) { return; }
        if (currentRow !== row) return;
        var el = card && card.querySelector('#hc-roles');
        if (!el) return;
        var roles = (view && Array.isArray(view.path_roles)) ? view.path_roles : [];
        el.textContent = roles.length ? roles.join(', ') : 'none';
    }

    function init() {
        var treeBody = document.getElementById('treeBody');
        if (!treeBody) return;

        treeBody.addEventListener('mouseover', function (e) {
            // Returning to the tree from the card cancels any pending
            // hide; the show logic below handles row changes.
            cancelHide();
            var row = e.target.closest('.tree-row');
            if (row === currentRow) return;
            // Row → row or row → empty space — reset.
            if (showTimer) { clearTimeout(showTimer); showTimer = null; }
            if (card) card.classList.remove('is-visible');
            currentRow = row || null;
            if (!row) return;
            showTimer = setTimeout(function () {
                if (currentRow !== row) return;
                var id = parseInt(row.dataset.id, 10);
                var node = state.nodes.get(id);
                if (node) showFor(row, node);
            }, SHOW_DELAY_MS);
        });

        // Leaving the tree schedules a hide rather than hiding
        // immediately, so the cursor has time to traverse the gap to
        // the card. The card's own mouseenter cancels the hide.
        treeBody.addEventListener('mouseleave', scheduleHide);
        treeBody.addEventListener('contextmenu', hide);
        window.addEventListener('scroll', hide, true);
        window.addEventListener('resize', hide);
        document.addEventListener('keydown', function (e) {
            if (e.key === 'Escape') hide();
        });

        // Click anywhere outside the card dismisses it. Clicks INSIDE
        // the card are allowed through so the user can drag-select
        // text, right-click for the browser's native Copy menu, or
        // hit Ctrl/Cmd-C.
        document.addEventListener('mousedown', function (e) {
            if (!card || !card.classList.contains('is-visible')) return;
            if (card.contains(e.target)) return;
            hide();
        }, true);
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }

    window.app.modules.hovercard = { hide: hide };
})();
