(function (app) {
    'use strict';

    async function init() {
        // Both apps (table + form) ship in the same bundle. Skip if
        // mode dispatcher said this isn't our mode — form-mode requests
        // are handled by formApp.
        if (window.zddcMode === 'form') {
            return;
        }
        const ctx = await app.modules.context.load();
        app.context = ctx;

        const titleEl = document.getElementById('table-title');
        if (ctx.title && titleEl) {
            titleEl.textContent = ctx.title;
            document.title = 'ZDDC — ' + ctx.title;
        }

        const descEl = document.getElementById('table-description');
        if (descEl && ctx.description) {
            descEl.textContent = ctx.description;
            descEl.hidden = false;
        }

        const tableEl = document.getElementById('table-root');
        const theadEl = tableEl.querySelector('thead');
        const tbodyEl = tableEl.querySelector('tbody');
        const emptyEl = document.getElementById('table-empty');
        const countEl = document.getElementById('table-rowcount');
        const clearBtn = document.getElementById('table-clear-filters');
        const addRowBtn = document.getElementById('table-add-row');
        const exportBtn = document.getElementById('table-export-csv');
        const saveBtn = document.getElementById('table-save');

        // Add-row button: appends a draft row inline. Save fires on
        // row-blur, which POSTs to <dir>/form.html and swaps the
        // synthetic row id for the server's response. The button shows
        // whenever the page is a real table view (http(s) + a table
        // context loaded with columns) — the test-fixture inline-context
        // harness opens tables.html directly with no URL shape, so we
        // gate on having a column list AND running over http(s).
        // Save: explicit flush of every dirty row. The button is
        // hidden until a draft exists; save.onDraftsChanged() (called
        // from editor.setDraft / clearDraftField) toggles visibility +
        // updates the count label. Backstop for the row-blur trigger,
        // which only fires when the user navigates to a different
        // ROW in the table — clicking outside the grid entirely never
        // fired a save without this.
        if (saveBtn) {
            saveBtn.addEventListener('click', function () {
                const save = app.modules.save;
                if (save && typeof save.flushAll === 'function') {
                    save.flushAll();
                }
            });
        }

        // Ctrl+S (Cmd+S on mac) flushes all dirty rows. Capturing
        // phase so we beat the browser's "Save Page As" default.
        window.addEventListener('keydown', function (ev) {
            if ((ev.ctrlKey || ev.metaKey) && (ev.key === 's' || ev.key === 'S')) {
                const save = app.modules.save;
                if (save && typeof save.dirtyCount === 'function' && save.dirtyCount() > 0) {
                    ev.preventDefault();
                    save.flushAll();
                }
            }
        });

        // Auto-save when focus leaves the grid entirely (the user
        // clicked a header link, the URL bar, etc. without moving to
        // another row first). focusout fires for cell-to-cell moves
        // too — relatedTarget being outside #table-root distinguishes.
        //
        // Deferred to next tick (setTimeout 0): the editor's commit
        // path tears down its input element and then refocuses the
        // owning cell. The remove fires focusout BEFORE the refocus
        // runs, with relatedTarget=null (body briefly), so the naive
        // sync check would mis-detect a "left the grid" event and
        // fire flushAll redundantly alongside the selection-change
        // save. Checking document.activeElement on the next tick
        // gives the refocus time to settle.
        const tableRoot = document.getElementById('table-root');
        if (tableRoot) {
            tableRoot.addEventListener('focusout', function (ev) {
                const next = ev.relatedTarget;
                if (next && tableRoot.contains(next)) return;
                setTimeout(function () {
                    if (tableRoot.contains(document.activeElement)) return;
                    const save = app.modules.save;
                    if (save && typeof save.flushAll === 'function' && save.dirtyCount() > 0) {
                        save.flushAll();
                    }
                }, 0);
            });
        }

        // Export CSV: client-side build of the current view (filtered +
        // sorted columns + values). No server round-trip, no auth gate
        // — the user already has the data on screen. Shown on every
        // table that loaded with columns, regardless of HTTP/file://.
        if (exportBtn) {
            const hasCols = Array.isArray(ctx.columns) && ctx.columns.length > 0;
            if (hasCols) {
                exportBtn.hidden = false;
                exportBtn.addEventListener('click', function () {
                    const exp = app.modules.exportCsv;
                    if (exp && typeof exp.invoke === 'function') {
                        exp.invoke();
                    }
                });
            }
        }

        if (addRowBtn) {
            const onHttp = location.protocol === 'http:' || location.protocol === 'https:';
            const hasCols = Array.isArray(ctx.columns) && ctx.columns.length > 0;
            // ctx.addable === false suppresses the affordance entirely.
            // Used by project-rollup tables where the row's party
            // affiliation is ambiguous (add at the per-party path).
            const allowAdd = ctx.addable !== false;
            if (onHttp && hasCols && allowAdd) {
                addRowBtn.hidden = false;
                addRowBtn.removeAttribute('href');
                addRowBtn.setAttribute('role', 'button');
                addRowBtn.setAttribute('tabindex', '0');
                addRowBtn.style.cursor = 'pointer';
                const handleAdd = function (ev) {
                    ev.preventDefault();
                    const addRow = app.modules.addRow;
                    if (addRow && typeof addRow.invoke === 'function') {
                        addRow.invoke();
                    }
                };
                addRowBtn.addEventListener('click', handleAdd);
                addRowBtn.addEventListener('keydown', function (ev) {
                    if (ev.key === 'Enter' || ev.key === ' ') handleAdd(ev);
                });

                // Permission gate: fetch the path-scoped verbs for the
                // current directory and disable + Add row when the
                // cascade denies create. Async — the button shows up
                // optimistically and disables a tick later if the
                // server says no, which is the same race window every
                // path-scoped fetch has. Server still gates the POST,
                // so the worst case is a 403 toast on click.
                if (window.zddc && window.zddc.cap) {
                    window.zddc.cap.at(location.pathname).then(function (view) {
                        if (!view) return;
                        var verbs = view.path_verbs || '';
                        if (verbs.indexOf('c') === -1) {
                            addRowBtn.classList.add('is-disabled');
                            addRowBtn.setAttribute('aria-disabled', 'true');
                            addRowBtn.title = "You don't have create access in this folder.";
                            // Swallow clicks so the no-op feedback is the
                            // tooltip, not a 403 toast on submission.
                            addRowBtn.addEventListener('click', function (ev) {
                                if (addRowBtn.classList.contains('is-disabled')) {
                                    ev.preventDefault();
                                    ev.stopPropagation();
                                }
                            }, true);
                        }
                    });
                }
            }
        }

        const columns = Array.isArray(ctx.columns) ? ctx.columns : [];
        const allRows = Array.isArray(ctx.rows) ? ctx.rows : [];

        const state = app.state;
        state.rows = allRows;
        state.sort = app.modules.sort.defaultsFromContext(ctx);
        state.filter = {};

        // Seed default filters from context.defaults.filter (per-column).
        if (ctx.defaults && ctx.defaults.filter && typeof ctx.defaults.filter === 'object') {
            for (let i = 0; i < columns.length; i++) {
                const col = columns[i];
                const seeded = ctx.defaults.filter[col.field];
                if (seeded == null) {
                    continue;
                }
                // Filter UI is uniformly text-contains. If the spec
                // seeds an array (legacy enum-style), coerce to a
                // comma-joined contains string — partial match on any
                // listed value still narrows the table sensibly.
                const seedStr = Array.isArray(seeded) ? seeded.join(',') : String(seeded);
                state.filter[col.field] = { kind: 'contains', value: seedStr };
            }
        }

        function anyFilterActive() {
            const filters = app.modules.filters;
            const keys = Object.keys(state.filter);
            for (let i = 0; i < keys.length; i++) {
                if (!filters.isEmpty(state.filter[keys[i]])) {
                    return true;
                }
            }
            return false;
        }

        function paint() {
            const filtered = app.modules.filters.apply(state.rows, columns, state.filter, app.modules.util.resolveField);
            const sorted = app.modules.sort.apply(filtered, state.sort, columns, app.modules.util);
            app.modules.render.header(theadEl, columns, state.sort, state.filter, onHeaderClick, onFilterChange);
            app.modules.render.body(tbodyEl, sorted, columns);
            app.modules.render.rowCount(countEl, sorted.length, state.rows.length);
            if (emptyEl) {
                emptyEl.hidden = sorted.length > 0 || state.rows.length === 0;
            }
            if (clearBtn) {
                clearBtn.hidden = !anyFilterActive();
            }
            // Restore the editor's selection across re-paints so a sort
            // or filter change doesn't dump the user out of the cell
            // they were on. Selected coords clamp to the new bounds in
            // setSelected; if the row vanished (filter excluded it),
            // we land on the last valid cell instead of clearing.
            const editor = app.modules.editor;
            if (editor) {
                editor.attachToTable();
                if (state.selected) {
                    editor.setSelected(state.selected.row, state.selected.col, { noFocus: true });
                }
            }
            // Row context menu re-attaches each paint — renderBody wipes
            // the tbody, taking listeners with it.
            const rowOps = app.modules.rowOps;
            if (rowOps && typeof rowOps.attach === 'function') {
                rowOps.attach();
            }
            // Re-apply Phase-3 dirty-row markers — tbody.innerHTML='' in
            // renderBody wiped them.
            const save = app.modules.save;
            if (save && typeof save.markAllDirtyRows === 'function') {
                save.markAllDirtyRows();
            }
            // Refresh the Save button visibility + count after every
            // paint — save flow may have settled drafts in the meantime.
            if (save && typeof save.updateSaveButton === 'function') {
                save.updateSaveButton();
            }
        }

        // Public re-paint entry point so other modules (save.useMine /
        // save.reload) can request a refresh after they mutate row state.
        app.repaint = paint;

        function onHeaderClick(field, shiftKey) {
            state.sort = app.modules.sort.cycle(state.sort, field, shiftKey);
            paint();
        }

        function onFilterChange(field, value) {
            state.filter[field] = value;
            paint();
        }

        if (clearBtn) {
            clearBtn.addEventListener('click', function () {
                state.filter = {};
                paint();
            });
        }

        paint();
    }

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