components_version_history_dialog.js

import { ConfirmDialog } from "./confirm_dialog.js";

/**
 * Modal dialog for browsing, creating, and restoring project version snapshots.
 */
export class VersionHistoryDialog {
    /**
     * @param {string} projectId - ID of the project whose versions are displayed.
     * @param {string} accessLevel - 'viewer' | 'editor' | 'owner'
     * @param {object} server - Server instance
     * @param {object} callbacks - Callback functions for version actions.
     * @param {function} callbacks.onRevertCurrent - Called with the full version object to revert in place
     * @param {function} callbacks.onOpenAsNew - Called with the full version object to open as a new project
     * @param {function} [callbacks.onSaveVersion] - Called with label string to save a named version; if omitted the Save button is hidden
     */
    constructor(projectId, accessLevel, server, { onRevertCurrent, onOpenAsNew, onSaveVersion } = {}) {
        this.projectId = projectId;
        this.accessLevel = accessLevel;
        this.server = server;
        this._onRevertCurrent = onRevertCurrent ?? (() => {});
        this._onOpenAsNew = onOpenAsNew ?? (() => {});
        this._onSaveVersion = onSaveVersion ?? null;
        this._canEdit = accessLevel === 'editor' || accessLevel === 'owner';

        this._buildDOM();
        document.body.appendChild(this.scrim);
        document.body.appendChild(this.root);
        this._load();
    }

    /** Removes the dialog and backdrop from the DOM. */
    close() {
        this.scrim.remove();
        this.root.remove();
    }

    // ── DOM ──────────────────────────────────────────────────────────────────

    /** Builds and attaches the dialog's DOM structure. */
    _buildDOM() {
        this.scrim = document.createElement('div');
        this.scrim.className = 'share-scrim';
        this.scrim.addEventListener('click', () => this.close());

        this.root = document.createElement('div');
        this.root.className = 'share-dialog';
        this.root.style.cssText = 'width:520px;max-height:70vh;display:flex;flex-direction:column;';

        // Header
        const header = document.createElement('div');
        header.className = 'share-dialog-header';
        header.style.cssText = 'display:flex;align-items:center;justify-content:space-between;gap:0.5rem;flex-shrink:0;';

        const title = document.createElement('h2');
        title.className = 'share-dialog-title';
        title.textContent = 'Version History';
        header.appendChild(title);

        const headerActions = document.createElement('div');
        headerActions.style.cssText = 'display:flex;align-items:center;gap:0.5rem;';

        if (this._canEdit && this._onSaveVersion) {
            this._saveBtn = document.createElement('button');
            this._saveBtn.className = 'sample-btn';
            this._saveBtn.textContent = 'Save Version';
            this._saveBtn.addEventListener('click', () => this._openSaveVersionUI());
            headerActions.appendChild(this._saveBtn);
        }

        const closeBtn = document.createElement('button');
        closeBtn.className = 'share-close-btn';
        closeBtn.title = 'Close';
        closeBtn.innerHTML = '✕';
        closeBtn.addEventListener('click', () => this.close());
        headerActions.appendChild(closeBtn);

        header.appendChild(headerActions);
        this.root.appendChild(header);

        // Save version inline form (hidden by default)
        this._saveForm = document.createElement('div');
        this._saveForm.className = 'share-dialog-body';
        this._saveForm.style.cssText = 'display:none;flex-shrink:0;padding:0.75rem 1.25rem;border-bottom:1px solid var(--border);';
        this._saveForm.innerHTML = `
            <div style="display:flex;gap:0.5rem;align-items:center;">
                <input class="project-title-input" id="versionLabelInput" placeholder="Version label (optional)"
                    style="flex:1;display:inline-block;" />
                <button class="sample-btn" id="versionSaveConfirm">Save</button>
                <button class="sample-btn" id="versionSaveCancel">Cancel</button>
            </div>`;
        this.root.appendChild(this._saveForm);

        // Version list
        this._list = document.createElement('div');
        this._list.className = 'share-dialog-body';
        this._list.style.cssText = 'flex:1;overflow-y:auto;padding:0;';
        this.root.appendChild(this._list);
    }

    // ── Loading ───────────────────────────────────────────────────────────────

    /** Fetches the version list from the server and renders it. */
    async _load() {
        this._list.innerHTML = '<div style="padding:1.25rem;color:var(--text-muted);font-size:var(--fs-caption);">Loading…</div>';
        try {
            const versions = await this.server.listVersions(this.projectId);
            this._renderList(versions);
        } catch (e) {
            this._list.innerHTML = `<div style="padding:1.25rem;color:var(--danger);">${e.message}</div>`;
        }
    }

    /** @param {object[]} versions - Array of version index entries to render. */
    _renderList(versions) {
        this._list.innerHTML = '';
        if (!versions.length) {
            this._list.innerHTML = '<div style="padding:1.25rem;color:var(--text-muted);font-size:var(--fs-caption);">No versions saved yet.</div>';
            return;
        }
        for (const v of versions) {
            this._list.appendChild(this._buildVersionRow(v));
        }
    }

    /**
     * @param {object} v - Version index entry to render as a list row.
     * @returns {HTMLElement} The constructed row element.
     */
    _buildVersionRow(v) {
        const row = document.createElement('div');
        row.style.cssText = 'padding:0.75rem 1.25rem;border-bottom:1px solid var(--border);display:flex;flex-direction:column;gap:0.3rem;';

        const topLine = document.createElement('div');
        topLine.style.cssText = 'display:flex;align-items:baseline;gap:0.5rem;';

        const labelEl = document.createElement('span');
        labelEl.style.cssText = 'font-family:var(--font-mono);font-size:var(--fs-label);font-weight:600;';
        labelEl.textContent = v.label ?? 'Auto';
        topLine.appendChild(labelEl);

        const dateEl = document.createElement('span');
        dateEl.style.cssText = 'font-size:var(--fs-caption);color:var(--text-muted);';
        dateEl.textContent = new Date(v.created_at).toLocaleString();
        topLine.appendChild(dateEl);

        row.appendChild(topLine);

        const byEl = document.createElement('div');
        byEl.style.cssText = 'font-size:var(--fs-caption);color:var(--text-muted);';
        byEl.textContent = v.created_by?.name ?? '';
        row.appendChild(byEl);

        if (this._canEdit) {
            const actions = document.createElement('div');
            actions.style.cssText = 'display:flex;gap:0.4rem;margin-top:0.25rem;';

            const openBtn = document.createElement('button');
            openBtn.className = 'sample-btn';
            openBtn.textContent = 'Open as New Project';
            openBtn.addEventListener('click', () => this._openAsNew(v.id, openBtn));
            actions.appendChild(openBtn);

            const revertBtn = document.createElement('button');
            revertBtn.className = 'sample-btn';
            revertBtn.textContent = 'Revert Current';
            revertBtn.addEventListener('click', () => this._revertCurrent(v.id, revertBtn));
            actions.appendChild(revertBtn);

            if (v.label !== null && v.label !== undefined) {
                const delBtn = document.createElement('button');
                delBtn.className = 'sample-btn';
                delBtn.style.cssText = 'color:var(--danger-muted);border-color:var(--danger-border);';
                delBtn.textContent = 'Delete';
                delBtn.addEventListener('click', () => this._deleteVersion(v.id, row));
                actions.appendChild(delBtn);
            }

            row.appendChild(actions);
        } else {
            const actions = document.createElement('div');
            actions.style.cssText = 'display:flex;gap:0.4rem;margin-top:0.25rem;';

            const openBtn = document.createElement('button');
            openBtn.className = 'sample-btn';
            openBtn.textContent = 'Open as New Project';
            openBtn.addEventListener('click', () => this._openAsNew(v.id, openBtn));
            actions.appendChild(openBtn);

            row.appendChild(actions);
        }

        return row;
    }

    // ── Actions ───────────────────────────────────────────────────────────────

    /** Shows the inline save-version form and wires up its confirm/cancel handlers. */
    _openSaveVersionUI() {
        this._saveForm.style.display = '';
        this._saveBtn.disabled = true;
        const input = this._saveForm.querySelector('#versionLabelInput');
        const confirmBtn = this._saveForm.querySelector('#versionSaveConfirm');
        const cancelBtn = this._saveForm.querySelector('#versionSaveCancel');
        input.value = '';
        input.focus();

        const cleanup = () => {
            this._saveForm.style.display = 'none';
            this._saveBtn.disabled = false;
        };

        const doSave = async () => {
            confirmBtn.disabled = true;
            confirmBtn.textContent = '…';
            try {
                await this._onSaveVersion(input.value.trim() || null);
                cleanup();
                this._load();
            } catch (e) {
                confirmBtn.textContent = 'Save';
                confirmBtn.disabled = false;
                alert(`Could not save version: ${e.message}`);
            }
        };

        confirmBtn.onclick = doSave;
        input.onkeydown = (e) => { if (e.key === 'Enter') doSave(); if (e.key === 'Escape') cleanup(); };
        cancelBtn.onclick = cleanup;
    }

    /**
     * @param {string} versionId - ID of the version to open.
     * @param {HTMLButtonElement} btn - Button element to show loading state on.
     */
    async _openAsNew(versionId, btn) {
        const orig = btn.textContent;
        btn.disabled = true;
        btn.textContent = '…';
        try {
            const version = await this.server.getVersion(this.projectId, versionId);
            this.close();
            await this._onOpenAsNew(version);
        } catch (e) {
            btn.textContent = orig;
            btn.disabled = false;
            alert(`Could not open version: ${e.message}`);
        }
    }

    /**
     * @param {string} versionId - ID of the version to revert to.
     * @param {HTMLButtonElement} btn - Button element to show loading state on.
     */
    async _revertCurrent(versionId, btn) {
        const orig = btn.textContent;
        btn.disabled = true;
        btn.textContent = '…';
        try {
            const version = await this.server.getVersion(this.projectId, versionId);
            this.close();
            this._onRevertCurrent(version);
        } catch (e) {
            btn.textContent = orig;
            btn.disabled = false;
            alert(`Could not load version: ${e.message}`);
        }
    }

    /**
     * @param {string} versionId - ID of the version to delete.
     * @param {HTMLElement} row - Row element to remove on success.
     */
    async _deleteVersion(versionId, row) {
        new ConfirmDialog(
            'Delete this version?',
            {
                onConfirm: async () => {
                    try {
                        await this.server.deleteVersion(this.projectId, versionId);
                        row.remove();
                    } catch (e) {
                        alert(`Could not delete version: ${e.message}`);
                    }
                },
            },
            'This cannot be undone.',
            'Delete',
        );
    }
}