(function (app) {
    'use strict';

    // load() resolves to the table context the rest of the app renders:
    //   { title?, description?, columns, rows, defaults? }
    //
    // Two paths:
    //
    //   1. Inline JSON (test seam, and also any host that wants to
    //      pre-render a context server-side): if #table-context parses
    //      to a non-empty object, return it as-is.
    //
    //   2. File-backed walk (the real-world path served by zddc-server):
    //      page is at /<dir>/table.html — fetch <dir>/table.yaml,
    //      list every other *.yaml in <dir> as a row file (filtering
    //      out table.yaml and form.yaml so they don't appear as rows),
    //      parse each, and assemble the same shape. The whole table
    //      lives in one directory.
    //
    // file:// mode without a directory handle is unsupported in v1 — the
    // walk only runs against http(s). file:// users must either inject an
    // inline context (tests) or open the page through zddc-server.
    async function load() {
        const inline = readInlineContext();
        // A fully pre-assembled context (columns + rows) is used as-is — the
        // test seam, or any host that renders the whole table server-side.
        if (inline && Array.isArray(inline.columns)) {
            return inline;
        }
        // Otherwise the inline context may still carry the server-injected
        // SPEC ({spec, rowSchema}) sourced from <dir>/.zddc.d/ — pass it to
        // walkServer, which uses it instead of fetching the spec and still
        // walks the directory for row files.
        if (typeof location !== 'undefined' &&
            (location.protocol === 'http:' || location.protocol === 'https:')) {
            try {
                const walked = await walkServer(inline || {});
                if (walked) {
                    return walked;
                }
            } catch (err) {
                console.error('[tables] failed to load table from server', err);
                showStatus('Could not load table: ' + (err && err.message ? err.message : err));
            }
        }
        return {};
    }

    function readInlineContext() {
        const el = document.getElementById('table-context');
        if (!el) {
            return null;
        }
        try {
            return JSON.parse(el.textContent || '{}');
        } catch (err) {
            console.error('[tables] failed to parse #table-context', err);
            return null;
        }
    }

    function showStatus(msg) {
        const el = document.getElementById('table-status');
        if (!el) return;
        el.textContent = msg;
        el.hidden = false;
    }

    async function walkServer(injected) {
        injected = injected || {};
        const source = window.zddc && window.zddc.source;
        if (!source) {
            throw new Error('zddc.source not available');
        }
        const tableName = tableNameFromUrl(location.pathname);
        if (!tableName) {
            throw new Error('Unrecognized table URL: ' + location.pathname);
        }
        const probe = await source.detectServerRoot();
        if (!probe.handle) {
            throw new Error(probe.status === 403
                ? 'No permission to list this directory'
                : 'Server unreachable');
        }
        const dir = probe.handle;

        // Spec: prefer the server-injected #table-context.spec (sourced from
        // <dir>/.zddc.d/table.yaml). Falling back, read the spec from the
        // supporting-files reserve, then the legacy directory root — the
        // FS-Access path, where there's no server to inject.
        let spec = (injected.spec && Array.isArray(injected.spec.columns))
            ? injected.spec : null;
        if (!spec) {
            spec = await readYamlFirst(dir, ['.zddc.d/table.yaml', 'table.yaml']);
        }
        if (!spec || !Array.isArray(spec.columns)) {
            throw new Error('Spec table.yaml missing columns[]');
        }

        // Row schema: prefer the injected #table-context.rowSchema, else read
        // <dir>/.zddc.d/form.yaml (then legacy root). Best-effort — a table
        // with no row schema still renders with plain-text cells.
        let rowSchema = injected.rowSchema || null;
        if (!rowSchema) {
            try {
                const formSpec = await readYamlFirst(dir, ['.zddc.d/form.yaml', 'form.yaml']);
                if (formSpec && formSpec.schema) {
                    rowSchema = formSpec.schema;
                }
            } catch (_) {
                // form.yaml missing or unreadable; carry on without it.
            }
        }

        // Rows are every *.yaml in <currentdir> EXCEPT the spec
        // (table.yaml) and the row-edit form (form.yaml). They live
        // in the same directory by design — copying the directory
        // copies the whole table.
        const rows = await readRows(dir, '', tableName);

        return {
            title: spec.title,
            description: spec.description,
            columns: spec.columns,
            defaults: spec.defaults,
            // addable defaults to true; tables can opt out with
            // `addable: false` (used by project-rollup MDL/RSK where the
            // party affiliation of a new row is ambiguous — add at the
            // per-party path instead).
            addable: spec.addable !== false,
            rowSchema: rowSchema,
            rows: rows
        };
    }

    function tableNameFromUrl(pathname) {
        // Two URL shapes resolve to a table page:
        //   Form A — /<…>/<rowsdir>/table.html (legacy/explicit
        //            entry-point; the tool was opened via the
        //            literal file URL).
        //   Form B — /<…>/<rowsdir>  or  /<…>/<rowsdir>/  (served
        //            by the cascade's `default_tool: tables` at
        //            archive/<party>/mdl; the URL is the directory
        //            itself, no trailing filename).
        // In both cases the table name is the rows-directory basename.
        const a = String(pathname || '').match(/\/([^\/]+)\/table\.html$/);
        if (a) return a[1];
        const trimmed = String(pathname || '').replace(/\/$/, '');
        const b = trimmed.match(/\/([^\/]+)$/);
        return b ? b[1] : null;
    }

    function stripDotSlash(p) {
        let out = String(p || '');
        if (out.startsWith('./')) out = out.slice(2);
        if (out.startsWith('/')) out = out.slice(1);
        if (out.endsWith('/')) out = out.slice(0, -1);
        return out;
    }

    async function readYaml(dir, relPath) {
        const fileHandle = await resolveFile(dir, relPath);
        const file = await fileHandle.getFile();
        const text = await file.text();
        if (!window.jsyaml) {
            throw new Error('js-yaml not loaded');
        }
        return window.jsyaml.load(text);
    }

    // readYamlFirst tries each relPath in order, returning the first that
    // resolves + parses. Used to read a spec from the supporting-files
    // reserve (.zddc.d/<name>) with a fallback to the legacy directory root.
    async function readYamlFirst(dir, relPaths) {
        let lastErr = null;
        for (var i = 0; i < relPaths.length; i++) {
            try {
                return await readYaml(dir, relPaths[i]);
            } catch (err) {
                lastErr = err;
            }
        }
        if (lastErr) throw lastErr;
        return null;
    }

    // Walk a "/"-separated relative path under dir, returning the
    // FileSystemFileHandle (or HttpFileHandle) at the leaf.
    async function resolveFile(dir, relPath) {
        const parts = relPath.split('/').filter(Boolean);
        if (parts.length === 0) {
            throw new Error('Empty file path');
        }
        const fileName = parts.pop();
        let cur = dir;
        for (let i = 0; i < parts.length; i++) {
            cur = await cur.getDirectoryHandle(parts[i]);
        }
        return cur.getFileHandle(fileName);
    }

    async function resolveDirectory(dir, relPath) {
        const parts = relPath.split('/').filter(Boolean);
        let cur = dir;
        for (let i = 0; i < parts.length; i++) {
            cur = await cur.getDirectoryHandle(parts[i]);
        }
        return cur;
    }

    function isRowFile(name) {
        return name.endsWith('.yaml') && name !== 'table.yaml' && name !== 'form.yaml';
    }

    // readRows reads a table's rows from rowsDir. A flat directory
    // (a per-party register like mdl/<party>/ or the ssr/ registry)
    // yields one row per *.yaml file. An aggregate peer root (mdl/ ,
    // rsk/) instead contains party SUBDIRS — we recurse ONE level so the
    // peer root renders the cross-party table. relName carries the
    // <party>/ prefix for those rows so reads + edit URLs hit the real
    // per-party path; $party is derived from that prefix (and matches the
    // server-injected value online). Works in both online + offline modes.
    async function readRows(rowsDir, _rowsRel, _tableName) {
        const rows = [];

        async function pushRow(handle, relName) {
            try {
                const file = await handle.getFile();
                const data = window.jsyaml.load(await file.text()) || {};
                const slash = relName.indexOf('/');
                if (slash > 0 && typeof data === 'object' && data.$party === undefined) {
                    data['$party'] = relName.slice(0, slash);
                }
                rows.push({
                    url: rowEditUrl(relName),
                    // Underlying YAML URL — strip the trailing .html from
                    // the form-mode re-edit URL. PUTs go here with
                    // If-Match: <etag> for optimistic concurrency.
                    yamlUrl: rowEditUrl(relName).replace(/\.html$/, ''),
                    data: data,
                    etag: handle._etag || null,
                    editable: true
                });
            } catch (err) {
                console.warn('[tables] skipping unparseable row', relName, err);
            }
        }

        for await (const entry of rowsDir.values()) {
            if (entry.kind === 'file') {
                if (!isRowFile(entry.name)) continue;
                await pushRow(await rowsDir.getFileHandle(entry.name), entry.name);
                continue;
            }
            if (entry.kind === 'directory') {
                let sub;
                try { sub = await rowsDir.getDirectoryHandle(entry.name); }
                catch (_e) { continue; }
                for await (const child of sub.values()) {
                    if (child.kind !== 'file' || !isRowFile(child.name)) continue;
                    await pushRow(await sub.getFileHandle(child.name), entry.name + '/' + child.name);
                }
            }
        }
        return rows;
    }

    // Re-edit URL for one row. The page directory is the same
    // directory the rows live in, regardless of which URL shape
    // (Form A `…/table.html` vs Form B bare `…/<rowsdir>`) we were
    // opened with — see tableNameFromUrl.
    function rowEditUrl(rowFileName) {
        let pageDir = location.pathname.replace(/\/table\.html$/, '/');
        if (!pageDir.endsWith('/')) pageDir += '/';
        return pageDir + rowFileName + '.html';
    }

    app.modules.context = { load: load };
})(window.tablesApp);
