// loader.js — fetches directory entries for either source mode.
//
// Server mode: GET <urlPath> with Accept: application/json. zddc-server
// (and Caddy's built-in browse, which we mirror) returns an array of
// FileInfo {name, size, url, mod_time, mode, is_dir, is_symlink}.
//
// FS-API mode: enumerate a FileSystemDirectoryHandle's children. No
// network involved; works on local folders the user picked.
(function () {
    'use strict';

    var state = window.app.state;

    // Lowercased extension (no leading dot), '' for dotfiles / no-ext /
    // trailing-dot names. Delegates to the shared parser so the rule
    // stays in one place (CLAUDE.md: all extension handling goes through
    // window.zddc).
    function splitExt(name) {
        return window.zddc.splitExtension(name).extension;
    }

    // Build a raw entry from the server's FileInfo shape.
    function fromServerEntry(e) {
        // Server returns directory names with a trailing "/". Strip
        // it for display; the is_dir flag is the canonical signal.
        var name = e.is_dir ? e.name.replace(/\/$/, '') : e.name;
        // displayName is the friendlier label set by the parent .zddc
        // `display:` map (when present). The on-disk basename stays in
        // .name so URL composition (pathFor) and the chevron's title
        // attribute still reflect the real folder name.
        var displayName = (typeof e.display_name === 'string' && e.display_name)
            ? e.display_name
            : '';
        return {
            name: name,
            displayName: displayName,
            isDir: e.is_dir,
            size: e.size || 0,
            modTime: e.mod_time ? new Date(e.mod_time) : null,
            ext: e.is_dir ? '' : splitExt(name),
            url: e.url || null,
            // Server-computed write authority — true if the policy
            // decider would allow a PUT for the calling principal.
            // Absent / false means "save will 403"; preview editors
            // read this to mount in read-only mode. Superseded by
            // verbs (below); kept in lockstep during the transition.
            writable: !!e.writable,
            // Server-computed verb set: canonical "rwcda" subset the
            // calling principal holds at this entry's URL. Per-entry
            // gating in the context menu (Rename/Delete) reads this
            // through zddc.cap.has(node, 'w'|'d').
            //
            //   "rw…"     — zddc-server emitted explicit grant.
            //   ""        — zddc-server emitted explicit zero grant
            //               (rare; usually the entry would have been
            //               filtered before reaching the client).
            //   undefined — the server didn't emit a verbs field at
            //               all (Caddy or any non-zddc backend).
            //               cap.has and the events.js gates treat
            //               this as "verbs unknown" and skip the
            //               per-entry cascade gate; canMutate +
            //               whatever the server enforces on the
            //               actual PUT/DELETE still apply.
            verbs: typeof e.verbs === 'string' ? e.verbs : undefined,
            // Server-computed: true when this file lives in a history:true
            // cascade subtree, so every save is versioned and
            // GET <url>?history lists prior versions. Drives the "History…"
            // context-menu affordance (server mode only — offline has no
            // authenticated identity to attribute saves to).
            history: !!e.history,
            // FS-API specific (null in server mode):
            handle: null
        };
    }

    // Build a raw entry from a FileSystemHandle.
    async function fromHandle(handle) {
        var name = handle.name;
        var isDir = handle.kind === 'directory';
        var size = 0;
        var modTime = null;
        if (!isDir) {
            try {
                var f = await handle.getFile();
                size = f.size;
                modTime = new Date(f.lastModified);
            } catch (_e) {
                // permission lost; leave size/modTime defaults
            }
        }
        return {
            name: name,
            isDir: isDir,
            size: size,
            modTime: modTime,
            ext: isDir ? '' : splitExt(name),
            url: null,
            handle: handle
        };
    }

    // Fetch children of a directory in server mode.
    // path must end with '/' so the request hits the directory route.
    //
    // 404 is treated as "empty directory" rather than a hard error.
    // A directory that doesn't exist on the server (e.g. a fresh
    // project's working/ before any drafts have been created, or a
    // dir deleted between listing and expand) is functionally
    // indistinguishable from an empty one for tree-rendering purposes.
    // Server-side, zddc-server already returns 200 + [] for canonical
    // project folders that are missing on disk; this fallback covers
    // the same UX for anything else and for non-zddc-server backends.
    async function fetchServerChildren(path) {
        if (!path.endsWith('/')) path += '/';
        // ?hidden=1 surfaces .-prefixed and _-prefixed entries when the
        // user has flipped the "Show hidden" toggle. The server still
        // ACL-gates per-entry, so this is purely additive — anyone
        // without read on the parent dir already sees nothing.
        var url = path;
        if (window.app && window.app.state && window.app.state.showHidden) {
            url += (url.indexOf('?') === -1 ? '?' : '&') + 'hidden=1';
        }
        var resp = await fetch(url, {
            headers: { 'Accept': 'application/json' },
            credentials: 'same-origin'
        });
        // Capture cascade-resolved scope flags from response headers
        // before bailing on 404. zddc-server emits X-ZDDC-Drop-Target
        // for directories the cascade marks as upload destinations
        // (see zddc/internal/zddc/lookups.go DropTargetAt). The flag
        // is leaf-only — it describes THIS path, not its descendants
        // — so a rescope or popstate re-reads it from the new listing.
        var dropTargetHdr = (resp.headers.get('X-ZDDC-Drop-Target') || '').toLowerCase();
        window.app.state.scopeDropTarget = dropTargetHdr === 'true';
        // X-ZDDC-Default-Tool surfaces the cascade-resolved default
        // tool name for the current path. Browse uses it to decide
        // grid-mode auto-activation (when default_tool==classifier)
        // without re-implementing the cascade client-side.
        window.app.state.scopeDefaultTool =
            (resp.headers.get('X-ZDDC-Default-Tool') || '').toLowerCase();
        // X-ZDDC-On-Plan-Review surfaces whether the cascade above
        // this path has an on_plan_review block. Drives visibility of
        // the "Plan Review" right-click menu item on received/<tracking>/
        // folders.
        window.app.state.scopeOnPlanReview =
            (resp.headers.get('X-ZDDC-On-Plan-Review') || '').toLowerCase() === 'true';
        // X-ZDDC-Canonical-Folder names the canonical project-layout
        // slot this directory occupies — "incoming", "received",
        // "working", "staging", etc. Drives scope-aware menu items:
        // Accept Transmittal (folders under incoming), Stage/Unstage
        // (files under working/staging), Create Transmittal folder
        // (right-click in staging).
        window.app.state.scopeCanonicalFolder =
            (resp.headers.get('X-ZDDC-Canonical-Folder') || '').toLowerCase();
        if (resp.status === 404) {
            return [];
        }
        if (!resp.ok) {
            throw new Error('HTTP ' + resp.status + ' fetching ' + path);
        }
        var data = await resp.json();
        if (!Array.isArray(data)) {
            throw new Error('Unexpected response shape from ' + path);
        }
        return data.map(fromServerEntry);
    }

    // Enumerate a FileSystemDirectoryHandle's immediate children.
    async function fetchFsChildren(dirHandle) {
        var entries = [];
        for await (var [_name, handle] of dirHandle.entries()) {
            entries.push(await fromHandle(handle));
        }
        return entries;
    }

    // Probe whether THIS page is being served by zddc-server (or any
    // server that responds to JSON listing requests). If so, switch to
    // server mode automatically and load the current directory.
    async function autoDetectServerMode() {
        // Only attempt when running over http(s) and the location's
        // path looks like a directory. Probing on file:// is pointless.
        if (location.protocol !== 'http:' && location.protocol !== 'https:') {
            return false;
        }
        // Strip any /<tool>.html from the path to get the directory.
        var path = location.pathname;
        // If the URL points at the browse.html itself, the directory
        // is the parent. If it's a directory ending in '/', use it.
        var dirPath;
        if (path.endsWith('/')) {
            dirPath = path;
        } else {
            // e.g. '/some/dir/browse.html' → '/some/dir/'
            var slash = path.lastIndexOf('/');
            dirPath = slash >= 0 ? path.substring(0, slash + 1) : '/';
        }

        try {
            var entries = await fetchServerChildren(dirPath);
            state.source = 'server';
            state.currentPath = dirPath;
            return { entries: entries, path: dirPath };
        } catch (_e) {
            // Not a server-backed page (e.g. opened via file://).
            return null;
        }
    }

    // JSZip is vendored into the bundle (shared/vendor/jszip.min.js
    // is concatenated ahead of init.js by build.sh), so it's always
    // already attached to window.JSZip by the time any tree code runs.
    // We keep the helper because tree.js calls it before reaching for
    // window.JSZip; if the bundle is ever rebuilt without the vendor
    // copy this will throw a clear error rather than silently failing.
    function ensureJSZip() {
        if (window.JSZip) return Promise.resolve();
        return Promise.reject(new Error(
            'JSZip not bundled — rebuild browse with shared/vendor/jszip.min.js'));
    }

    // Public API
    window.app.modules.loader = {
        fetchServerChildren: fetchServerChildren,
        fetchFsChildren: fetchFsChildren,
        autoDetectServerMode: autoDetectServerMode,
        ensureJSZip: ensureJSZip
    };
})();
