// shared/zddc-source.js — source abstraction for tools that handle
// directory trees (classifier, mdedit, transmittal, browse, archive).
//
// Two backends:
//
//   1. Local — wraps a real FileSystemDirectoryHandle from the
//      File System Access API. Reads + writes go through the
//      FS Access API directly.
//
//   2. HTTP — talks to zddc-server's directory listing JSON
//      (Accept: application/json) for reads and the file API
//      (PUT/DELETE/POST X-ZDDC-Op) for writes. Implements a
//      polyfill of the FS Access API surface area the tools
//      use (kind, name, values(), getFileHandle, getDirectoryHandle,
//      removeEntry, getFile, createWritable, queryPermission /
//      requestPermission) so existing code works unchanged.
//
// The polyfill makes auto-load possible: when zddc-server serves
// a tool at /<dir>/<tool>.html, the tool detects HTTP mode at
// startup, builds an HttpDirectoryHandle for the tool's containing
// directory, and hands it to the existing openDirectory(handle)
// flow without ever showing the file picker.
//
// Renames inside a tool today are typically done as
// "write new + remove old". With HTTP-backed handles this becomes
// PUT + DELETE — non-atomic. Tools that prefer the atomic server
// MOVE should call window.zddc.source.moveFile(srcUrl, dstUrl)
// directly instead of going through the polyfill.
(function () {
    'use strict';

    if (!window.zddc) window.zddc = {};
    var FA = window.FileSystemDirectoryHandle || null;

    // -----------------------------------------------------------------
    // HTTP file API helpers
    // -----------------------------------------------------------------

    function joinUrl(base, name, isDir) {
        if (!base.endsWith('/')) base = base + '/';
        return base + encodeURIComponent(name) + (isDir ? '/' : '');
    }

    // Server returns directory entries with a trailing "/" on names.
    // Strip it for the FS Access API name surface.
    function stripSlash(name) {
        return name.endsWith('/') ? name.slice(0, -1) : name;
    }

    async function httpListing(url) {
        var resp = await fetch(url, {
            headers: { 'Accept': 'application/json' },
            credentials: 'same-origin'
        });
        if (!resp.ok) {
            var err = new Error('listing ' + url + ': HTTP ' + resp.status);
            err.status = resp.status;
            throw err;
        }
        var data = await resp.json();
        if (!Array.isArray(data)) {
            throw new Error('listing ' + url + ': non-array body');
        }
        return data;
    }

    async function httpExists(url) {
        try {
            var r = await fetch(url, { method: 'HEAD', credentials: 'same-origin' });
            return r.ok;
        } catch (_) {
            return false;
        }
    }

    // -----------------------------------------------------------------
    // HttpFileHandle — FileSystemFileHandle polyfill
    // -----------------------------------------------------------------

    function makeFile(blob, name, modTime) {
        return new File([blob], name, {
            type: blob.type,
            lastModified: modTime ? modTime.getTime() : Date.now()
        });
    }

    function HttpFileHandle(url, name, size, modTime) {
        this.kind = 'file';
        this.name = name;
        this._url = url;
        this._size = size || 0;
        this._modTime = modTime || null;
        this._etag = null;
    }
    HttpFileHandle.prototype.getFile = async function () {
        var resp = await fetch(this._url, { credentials: 'same-origin' });
        if (!resp.ok) {
            throw new Error('GET ' + this._url + ': ' + resp.status);
        }
        var etag = resp.headers.get('ETag');
        if (etag) this._etag = etag.replace(/"/g, '');
        var lm = resp.headers.get('Last-Modified');
        var modTime = lm ? new Date(lm) : this._modTime;
        var blob = await resp.blob();
        return makeFile(blob, this.name, modTime);
    };
    HttpFileHandle.prototype.createWritable = async function () {
        var chunks = [];
        var handle = this;
        return {
            async write(data) {
                if (data == null) return;
                if (typeof data === 'object' && data && 'type' in data && data.type === 'write') {
                    chunks.push(data.data);
                    return;
                }
                if (typeof data === 'object' && data && 'type' in data) {
                    // seek/truncate not supported by HTTP backend
                    throw new Error('HttpFileHandle write op not supported: ' + data.type);
                }
                chunks.push(data);
            },
            async close() {
                var blob = new Blob(chunks);
                var resp = await fetch(handle._url, {
                    method: 'PUT',
                    body: blob,
                    credentials: 'same-origin'
                });
                if (!resp.ok) {
                    var body = '';
                    try { body = await resp.text(); } catch (_) { /* ignore */ }
                    throw new Error('PUT ' + handle._url + ': ' + resp.status + ' ' + body);
                }
                var et = resp.headers.get('ETag');
                if (et) handle._etag = et.replace(/"/g, '');
                handle._size = blob.size;
            },
            async abort() { chunks = []; }
        };
    };
    HttpFileHandle.prototype.queryPermission = async function () { return 'granted'; };
    HttpFileHandle.prototype.requestPermission = async function () { return 'granted'; };
    HttpFileHandle.prototype.isHttp = true;
    HttpFileHandle.prototype.url = function () { return this._url; };

    // -----------------------------------------------------------------
    // HttpDirectoryHandle — FileSystemDirectoryHandle polyfill
    // -----------------------------------------------------------------

    function HttpDirectoryHandle(url, name) {
        this.kind = 'directory';
        if (!url.endsWith('/')) url = url + '/';
        this._url = url;
        this.name = name || guessNameFromUrl(url);
    }
    function guessNameFromUrl(url) {
        var u = url.replace(/\/+$/, '');
        var slash = u.lastIndexOf('/');
        return slash >= 0 ? decodeURIComponent(u.substring(slash + 1)) : u;
    }
    HttpDirectoryHandle.prototype.values = function () {
        var url = this._url;
        return (async function* () {
            var entries;
            try {
                entries = await httpListing(url);
            } catch (e) {
                return;
            }
            for (var i = 0; i < entries.length; i++) {
                var e = entries[i];
                var rawName = stripSlash(e.name);
                // Listing entries can carry an explicit URL for virtual
                // links (e.g. the reviewing-aggregator's received/+staged/
                // entries point to canonical archive/+staging paths).
                // Use it when present so navigation follows the listing's
                // own routing rather than computing a synthetic child URL
                // off the parent. Caddy-shape listings don't set url
                // (or set it to a relative form) — joinUrl handles those.
                var childUrl;
                if (e.url && /^https?:\/\/|^\//.test(e.url)) {
                    // Absolute or root-relative: use as-is, normalised against origin.
                    var u = e.url;
                    if (u[0] === '/') {
                        u = location.origin + u;
                    }
                    childUrl = u;
                } else {
                    childUrl = joinUrl(url, rawName, e.is_dir);
                }
                if (e.is_dir) {
                    yield new HttpDirectoryHandle(childUrl, rawName);
                } else {
                    var modTime = e.mod_time ? new Date(e.mod_time) : null;
                    yield new HttpFileHandle(childUrl, rawName, e.size || 0, modTime);
                }
            }
        })();
    };
    HttpDirectoryHandle.prototype.entries = function () {
        var iter = this.values();
        return (async function* () {
            for (;;) {
                var step = await iter.next();
                if (step.done) return;
                yield [step.value.name, step.value];
            }
        })();
    };
    HttpDirectoryHandle.prototype.keys = function () {
        var iter = this.values();
        return (async function* () {
            for (;;) {
                var step = await iter.next();
                if (step.done) return;
                yield step.value.name;
            }
        })();
    };
    HttpDirectoryHandle.prototype.getFileHandle = async function (name, opts) {
        opts = opts || {};
        var url = joinUrl(this._url, name, false);
        var exists = await httpExists(url);
        if (!exists && !opts.create) {
            var err = new Error('NotFoundError: ' + name);
            err.name = 'NotFoundError';
            throw err;
        }
        return new HttpFileHandle(url, name, 0, null);
    };
    HttpDirectoryHandle.prototype.getDirectoryHandle = async function (name, opts) {
        opts = opts || {};
        var url = joinUrl(this._url, name, true);
        if (opts.create) {
            var resp = await fetch(url, {
                method: 'POST',
                headers: { 'X-ZDDC-Op': 'mkdir' },
                credentials: 'same-origin'
            });
            if (!resp.ok && resp.status !== 200 && resp.status !== 201) {
                throw new Error('mkdir ' + url + ': ' + resp.status);
            }
        }
        return new HttpDirectoryHandle(url, name);
    };
    HttpDirectoryHandle.prototype.removeEntry = async function (name, opts) {
        opts = opts || {};
        // Probe listing to discover whether name is a file or directory.
        var entries;
        try {
            entries = await httpListing(this._url);
        } catch (e) {
            throw new Error('removeEntry probe failed: ' + e.message);
        }
        var match = null;
        for (var i = 0; i < entries.length; i++) {
            if (stripSlash(entries[i].name) === name) {
                match = entries[i];
                break;
            }
        }
        if (!match) {
            var err = new Error('NotFoundError: ' + name);
            err.name = 'NotFoundError';
            throw err;
        }
        if (match.is_dir && !opts.recursive) {
            // Server doesn't expose a recursive-delete endpoint yet,
            // and FS Access API requires recursive=true to remove a
            // non-empty directory anyway. Reject explicitly so the
            // caller doesn't silently leave a stale tree behind.
            var derr = new Error('Removing directories over HTTP is not supported');
            derr.name = 'InvalidStateError';
            throw derr;
        }
        var url = joinUrl(this._url, name, match.is_dir);
        var resp = await fetch(url, { method: 'DELETE', credentials: 'same-origin' });
        if (!resp.ok && resp.status !== 204) {
            throw new Error('DELETE ' + url + ': ' + resp.status);
        }
    };
    HttpDirectoryHandle.prototype.queryPermission = async function () { return 'granted'; };
    HttpDirectoryHandle.prototype.requestPermission = async function () { return 'granted'; };
    HttpDirectoryHandle.prototype.isHttp = true;
    HttpDirectoryHandle.prototype.url = function () { return this._url; };

    // -----------------------------------------------------------------
    // Top-level helpers
    // -----------------------------------------------------------------

    // Strip a trailing tool .html (e.g. classifier.html) from a path
    // to land on the "directory the tool was opened in".
    function pathToDir(pathname) {
        if (!pathname) return '/';
        if (pathname.endsWith('/')) return pathname;
        var slash = pathname.lastIndexOf('/');
        return slash >= 0 ? pathname.substring(0, slash + 1) : '/';
    }

    // Probe the server-mode root for the current page. Returns:
    //
    //   { handle: HttpDirectoryHandle, status: 200 }   — server reachable, listing returned
    //   { handle: null,                status: 403 }   — server reachable but listing forbidden
    //   { handle: null,                status: 0   }   — not http(s), or server unreachable / non-JSON
    //
    // Tools that auto-load on startup distinguish 403 (show "no
    // permission to list this directory" message) from 0 (fall back
    // to local-mode welcome screen).
    //
    // Tool init pattern:
    //   if (location.protocol !== 'file:') {
    //       const r = await zddc.source.detectServerRoot();
    //       if (r.handle) await openDirectory(r.handle);
    //       else if (r.status === 403) showNoPermissionMessage();
    //       else showWelcome();
    //   } else { showWelcome(); }
    async function detectServerRoot() {
        if (typeof location === 'undefined') {
            return { handle: null, status: 0 };
        }
        if (location.protocol !== 'http:' && location.protocol !== 'https:') {
            return { handle: null, status: 0 };
        }
        var dirPath = pathToDir(location.pathname);
        var url = location.origin + dirPath;
        try {
            await httpListing(url);
        } catch (e) {
            if (e && e.status === 403) {
                return { handle: null, status: 403 };
            }
            return { handle: null, status: 0 };
        }
        return {
            handle: new HttpDirectoryHandle(url, guessNameFromUrl(url)),
            status: 200,
        };
    }

    // Atomic file move. Path arguments are absolute URL paths
    // (starting with /). Honors the file API's POST /op=move
    // contract. Returns the new ETag.
    async function moveFile(srcUrlPath, dstUrlPath, opts) {
        opts = opts || {};
        var headers = {
            'X-ZDDC-Op': 'move',
            'X-ZDDC-Destination': dstUrlPath
        };
        if (opts.ifMatch) headers['If-Match'] = opts.ifMatch;
        var resp = await fetch(srcUrlPath, {
            method: 'POST',
            headers: headers,
            credentials: 'same-origin'
        });
        if (!resp.ok) {
            var body = '';
            try { body = await resp.text(); } catch (_) { /* ignore */ }
            throw new Error('move ' + srcUrlPath + ' → ' + dstUrlPath + ': ' + resp.status + ' ' + body);
        }
        var et = resp.headers.get('ETag');
        return et ? et.replace(/"/g, '') : null;
    }

    // Detect at construction time whether a directory handle is the
    // HTTP polyfill or a real FS Access API handle. Useful for tools
    // that want to take the optimized path (e.g. atomic moveFile)
    // when in HTTP mode rather than the FS-API copy+remove fallback.
    function isHttpHandle(handle) {
        return !!(handle && handle.isHttp === true);
    }

    window.zddc.source = {
        HttpDirectoryHandle: HttpDirectoryHandle,
        HttpFileHandle: HttpFileHandle,
        detectServerRoot: detectServerRoot,
        moveFile: moveFile,
        isHttpHandle: isHttpHandle,
        // Lower-level helpers exposed for tools that want to call the
        // server directly without going through the polyfill.
        httpListing: httpListing,
        joinUrl: joinUrl,
        stripSlash: stripSlash
    };
})();
