// VelaPanel client-side helpers

// File Manager upload — POSTs each selected file to /api/files/upload via XHR so the
// bytes bypass the Blazor Server SignalR circuit (which caps messages at 32 KB and
// hangs on multi-hundred-MB uploads). Progress is reported back to the Blazor page
// via a DotNetObjectReference so the existing MudProgressLinear bars keep working.
window.velaFileUpload = {
    trigger: function (inputId) {
        const el = document.getElementById(inputId);
        if (el) el.click();
    },

    // Returns the currently-selected file list to the .NET side for pre-flight validation
    // (size + extension) BEFORE any bytes go over the wire. Catches the 90% case where the
    // user picks a 500 MB file on a panel with a 250 MB limit — they get an immediate error
    // instead of waiting for the upload to complete and get a 413.
    listSelected: function (inputId) {
        const el = document.getElementById(inputId);
        if (!el || !el.files) return [];
        return Array.from(el.files).map(f => ({ name: f.name, size: f.size }));
    },

    clearSelection: function (inputId) {
        const el = document.getElementById(inputId);
        if (el) el.value = '';
    },

    // dotNetRef: DotNetObjectReference exposing OnUploadProgress, OnUploadResult, OnUploadFinished
    upload: async function (inputId, domainId, relativePath, dotNetRef) {
        const input = document.getElementById(inputId);
        if (!input || !input.files || input.files.length === 0) {
            safeInvoke(dotNetRef, 'OnUploadFinished');
            return;
        }
        const files = Array.from(input.files);
        input.value = ''; // allow re-selecting the same file next time

        // Fetch a fresh antiforgery token once per upload session (covers all selected files).
        // The upload endpoint requires it via X-CSRF-TOKEN header — defense-in-depth on top of
        // SameSite=Strict, matching CLAUDE.md rule 14. If this fails we abort — never silently
        // bypass CSRF.
        let csrfToken;
        try {
            const csrfResp = await fetch('/api/files/csrf', { credentials: 'include' });
            if (!csrfResp.ok) throw new Error('HTTP ' + csrfResp.status);
            csrfToken = (await csrfResp.json()).token;
        } catch (e) {
            for (const file of files) {
                safeInvoke(dotNetRef, 'OnUploadResult', file.name, false,
                    'Failed to obtain CSRF token: ' + (e.message || e));
            }
            safeInvoke(dotNetRef, 'OnUploadFinished');
            return;
        }

        for (const file of files) {
            await new Promise((resolve) => {
                const xhr = new XMLHttpRequest();
                const form = new FormData();
                form.append('domainId', domainId);
                form.append('relativePath', relativePath || '');
                form.append('file', file);

                // Throttle progress callbacks — XHR fires these dozens of times per second on a
                // fast LAN; each is a SignalR round-trip + StateHasChanged. 4 Hz is plenty for a
                // human-facing progress bar and keeps the circuit responsive.
                let lastReport = 0;
                xhr.upload.onprogress = function (e) {
                    if (!e.lengthComputable) return;
                    const now = Date.now();
                    const pct = Math.min(100, Math.round((e.loaded * 100) / e.total));
                    if (pct === 100 || (now - lastReport) > 250) {
                        lastReport = now;
                        safeInvoke(dotNetRef, 'OnUploadProgress', file.name, pct);
                    }
                };
                xhr.onload = function () {
                    const ok = xhr.status >= 200 && xhr.status < 300;
                    // Map common failure codes to actionable messages. 413 in particular is
                    // almost always the IIS reverse-proxy site's requestLimits/maxAllowedContentLength
                    // (default 30 MB) — the panel itself is set to 2 GB, so if 413 comes back the
                    // fix lives in the OUTER IIS site, not this app.
                    let msg;
                    if (ok) msg = '';
                    else if (xhr.status === 413)
                        msg = 'Server rejected the upload as too large. If you have an IIS reverse ' +
                              'proxy in front of the panel, raise its <requestLimits maxAllowedContentLength> ' +
                              'in web.config (default 30 MB) and reload IIS.';
                    else if (xhr.status === 400)
                        msg = 'Rejected: ' + (xhr.responseText || 'validation failed (blocked extension, magic byte, or path)');
                    else if (xhr.status === 401 || xhr.status === 403)
                        msg = 'Not authorized. Session may have expired — please reload the page.';
                    else if (xhr.status === 429)
                        msg = 'Rate limit hit (max 10 uploads/minute). Wait a minute and retry.';
                    else if (xhr.status === 0)
                        msg = 'Upload interrupted (connection closed). Check the reverse-proxy or network.';
                    else if (xhr.status >= 500)
                        msg = 'Server error (HTTP ' + xhr.status + '). Check the panel EventLog / Serilog file. ' +
                              (xhr.responseText ? 'Response: ' + xhr.responseText.substring(0, 200) : '');
                    else
                        msg = 'HTTP ' + xhr.status + (xhr.responseText ? ': ' + xhr.responseText.substring(0, 200) : '');
                    safeInvoke(dotNetRef, 'OnUploadResult', file.name, ok, msg);
                    resolve();
                };
                xhr.onerror = function () {
                    safeInvoke(dotNetRef, 'OnUploadResult', file.name, false, 'Network error');
                    resolve();
                };
                xhr.open('POST', '/api/files/upload');
                xhr.withCredentials = true;             // send auth cookie
                xhr.setRequestHeader('X-CSRF-TOKEN', csrfToken);
                xhr.send(form);
            });
        }
        safeInvoke(dotNetRef, 'OnUploadFinished');
    }
};

// Swallows "There is no tracked object with id..." errors that occur if the user navigates
// away mid-upload and the Blazor DotNetObjectReference is disposed before callbacks fire.
function safeInvoke(ref, method, ...args) {
    try { ref.invokeMethodAsync(method, ...args).catch(() => { /* circuit gone */ }); }
    catch { /* ref disposed */ }
}


// Open a URL in a new browser tab (used by Software Installer download buttons)
window.openUrl = function (url) { window.open(url, '_blank', 'noopener,noreferrer'); };

// Smoothly bring an element into view by id. Used by the Software Installer: expanding a
// single-version card makes it full-width, so MudGrid reflows it onto its own row — this
// scrolls the expanded card + its install form back under the user's attention. The short
// delay lets Blazor finish rendering the expanded form before we scroll to it.
window.velaScrollIntoView = function (id) {
    setTimeout(function () {
        const el = document.getElementById(id);
        if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' });
    }, 60);
};

// Called from Blazor login component — fetches from the browser so the auth
// cookie is set on the browser's connection, not on an internal server request.
window.velaLogin = async function (email, password, rememberMe, captchaToken, captchaAnswer) {
    try {
        const resp = await fetch('/api/auth/login', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ email, password, rememberMe, captchaToken, captchaAnswer }),
            credentials: 'include'
        });
        const text = await resp.text();
        let data = {};
        try { data = JSON.parse(text); } catch { }
        if (resp.ok) {
            return { success: true, redirectTo: data.redirectTo ?? '/dashboard', error: '' };
        } else {
            return { success: false, redirectTo: '', error: text || 'Login failed.' };
        }
    } catch (e) {
        return { success: false, redirectTo: '', error: e.message };
    }
};

// MFA challenge — called from MfaChallengePage; submits code to the controller
// which re-issues the auth cookie with MfaPassed=true, then redirects.
window.velaMfaVerify = async function (code, isBackupCode) {
    try {
        const resp = await fetch('/api/auth/mfa-verify', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ code, isBackupCode }),
            credentials: 'include'
        });
        const text = await resp.text();
        let data = {};
        try { data = JSON.parse(text); } catch { }
        if (resp.ok) {
            window.location.href = data.redirectTo ?? '/dashboard';
            return { success: true, error: '' };
        } else if (resp.status === 423) {
            window.location.href = data.redirectTo ?? '/login?locked=true';
            return { success: false, error: 'locked' };
        } else {
            return { success: false, error: text || 'Invalid code.' };
        }
    } catch (e) {
        return { success: false, error: e.message };
    }
};

// Impersonation helpers
window.velaImpersonate = async function (userId) {
    const resp = await fetch('/api/auth/impersonate/' + userId, {
        method: 'POST',
        credentials: 'include'
    });
    if (resp.ok) {
        const data = await resp.json();
        window.location.href = data.redirectTo || '/dashboard';
    }
};

window.velaStopImpersonation = async function () {
    const resp = await fetch('/api/auth/stop-impersonation', {
        method: 'POST',
        credentials: 'include'
    });
    if (resp.ok) {
        const data = await resp.json();
        window.location.href = data.redirectTo || '/admin/users';
    }
};

// Dark mode persistence
window.velaDarkMode = {
    get: () => localStorage.getItem('darkMode') === 'true',
    set: (val) => localStorage.setItem('darkMode', val ? 'true' : 'false')
};

// Command palette — global Ctrl/Cmd-K handler bound to a .NET component instance.
window.velaCmdK = {
    _handler: null,
    _ref: null,
    register: function (dotnetRef) {
        this.unregister();
        this._ref = dotnetRef;
        this._handler = function (e) {
            if ((e.ctrlKey || e.metaKey) && (e.key === 'k' || e.key === 'K')) {
                e.preventDefault();
                dotnetRef.invokeMethodAsync('Toggle');
            }
        };
        document.addEventListener('keydown', this._handler);
    },
    open: function () { if (this._ref) { this._ref.invokeMethodAsync('Toggle'); } },
    unregister: function () {
        if (this._handler) {
            document.removeEventListener('keydown', this._handler);
            this._handler = null;
        }
        this._ref = null;
    },
    focus: function (el) { if (el) { setTimeout(function () { el.focus(); }, 30); } }
};

// Clipboard helper with textarea fallback for non-HTTPS / restricted contexts.
window.velaClipboard = {
    copy: async function (text) {
        if (navigator.clipboard && window.isSecureContext) {
            await navigator.clipboard.writeText(text);
        } else {
            // Fallback: create an off-screen textarea and execCommand
            const ta = document.createElement('textarea');
            ta.value = text;
            ta.style.position = 'fixed';
            ta.style.left = '-9999px';
            ta.style.top = '-9999px';
            document.body.appendChild(ta);
            ta.focus();
            ta.select();
            const ok = document.execCommand('copy');
            document.body.removeChild(ta);
            if (!ok) throw new Error('execCommand copy failed');
        }
    }
};

// Trigger a file download from an authenticated GET endpoint (auth cookie rides along).
// CSP-safe: builds a real anchor element rather than using eval().
window.velaDownload = function (url, filename) {
    const a = document.createElement('a');
    a.href = url;
    if (filename) a.download = filename;
    a.style.display = 'none';
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
};
