/**
 * BotGuard client verification engine.
 *
 * Responsibilities:
 *   1. Collect a browser fingerprint (screen, timezone, canvas, WebGL, audio,
 *      hardware, fonts, plugins) and derive a stable hash.
 *   2. Run JavaScript capability probes (ES6, async, Promise, event loop) and
 *      read the navigator.webdriver automation flag.
 *   3. Verify cookies + localStorage/sessionStorage, including persistence of the
 *      server-set probe cookie.
 *   4. Passively record behavior: mouse dynamics, keyboard cadence, scrolling.
 *   5. Present the slider puzzle (or a remote CAPTCHA widget) and measure the solve.
 *   6. POST the whole signal bundle to /api/verify and act on the decision.
 *
 * All collection is best-effort and defensive: a failing probe degrades to a
 * conservative value rather than throwing.
 */
(function () {
    'use strict';

    const BG = window.__BG || {};
    const loadStart = performance.now();
    const state = {
        firstInteractionAt: 0,
        mouse: [],
        clicks: [],
        keys: [],
        keyEvents: { paste: false, backspaces: 0 },
        scroll: [],
        links: { hovers: [], instantClicks: 0, doubleClicks: 0, touch: 0 },
        retries: 0,
        puzzleStart: 0,
        solveTime: 0,
        puzzlePosition: -9999,
        captchaToken: '',
    };

    // ---- small stats helpers -------------------------------------------
    const mean = (a) => (a.length ? a.reduce((s, x) => s + x, 0) / a.length : 0);
    const stddev = (a) => {
        if (a.length < 2) return 0;
        const m = mean(a);
        return Math.sqrt(mean(a.map((x) => (x - m) ** 2)));
    };
    const hashString = (str) => {
        let h = 5381;
        for (let i = 0; i < str.length; i++) h = ((h << 5) + h + str.charCodeAt(i)) >>> 0;
        return h.toString(16);
    };

    // ---- checklist UI ---------------------------------------------------
    const checklistEl = document.getElementById('checklist');
    const items = {};
    function addCheck(key, label) {
        const row = document.createElement('div');
        row.className = 'check-row';
        row.innerHTML =
            '<span class="spinner-border spinner-border-sm text-secondary" role="status"></span>' +
            '<span class="flex-grow-1 small">' + label + '</span>' +
            '<span class="badge text-bg-secondary">…</span>';
        checklistEl.appendChild(row);
        items[key] = row;
    }
    function markCheck(key, ok) {
        const row = items[key];
        if (!row) return;
        row.querySelector('.spinner-border').outerHTML = ok
            ? '<i class="bi bi-check-circle-fill text-success"></i>'
            : '<i class="bi bi-exclamation-circle-fill text-warning"></i>';
        const badge = row.querySelector('.badge');
        badge.className = 'badge ' + (ok ? 'text-bg-success' : 'text-bg-warning');
        badge.textContent = ok ? 'OK' : 'Check';
    }

    ['js', 'fp', 'cookies', 'behavior', 'challenge'].forEach((k) =>
        addCheck(k, {
            js: 'JavaScript engine',
            fp: 'Browser fingerprint',
            cookies: 'Cookies & storage',
            behavior: 'Human behavior',
            challenge: 'Challenge',
        }[k])
    );

    // ---- 1. JavaScript probes ------------------------------------------
    async function collectJs() {
        const probes = { enabled: true, es6: false, async: false, promise: false, event_loop: false, webdriver: false };
        try {
            probes.es6 = (() => { try { eval('const x=(a=1)=>a; class Y{}; return true;'); } catch (e) { return false; } })();
            probes.promise = typeof Promise === 'function';
            probes.async = (async () => true) instanceof Function;
            // Event loop: microtask should run before a 0ms macrotask.
            probes.event_loop = await new Promise((res) => {
                let order = '';
                Promise.resolve().then(() => (order += 'm'));
                setTimeout(() => { order += 't'; res(order === 'mt'); }, 0);
            });
            probes.webdriver = navigator.webdriver === true;
        } catch (e) { /* leave defaults */ }
        markCheck('js', probes.enabled && !probes.webdriver);
        return probes;
    }

    // ---- 2. Fingerprint -------------------------------------------------
    function canvasHash() {
        try {
            const c = document.createElement('canvas');
            c.width = 240; c.height = 60;
            const ctx = c.getContext('2d');
            ctx.textBaseline = 'top';
            ctx.font = '16px "Arial"';
            ctx.fillStyle = '#f60'; ctx.fillRect(10, 10, 100, 30);
            ctx.fillStyle = '#069'; ctx.fillText('BotGuard \u{1F512} 1a2b', 12, 15);
            ctx.strokeStyle = 'rgba(0,120,255,0.7)'; ctx.beginPath();
            ctx.arc(60, 30, 20, 0, Math.PI * 2); ctx.stroke();
            return hashString(c.toDataURL());
        } catch (e) { return ''; }
    }

    function webglInfo() {
        try {
            const c = document.createElement('canvas');
            const gl = c.getContext('webgl') || c.getContext('experimental-webgl');
            if (!gl) return { vendor: '', renderer: '' };
            const dbg = gl.getExtension('WEBGL_debug_renderer_info');
            return {
                vendor: dbg ? gl.getParameter(dbg.UNMASKED_VENDOR_WEBGL) : gl.getParameter(gl.VENDOR),
                renderer: dbg ? gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : gl.getParameter(gl.RENDERER),
            };
        } catch (e) { return { vendor: '', renderer: '' }; }
    }

    async function audioHash() {
        try {
            const Ctx = window.OfflineAudioContext || window.webkitOfflineAudioContext;
            if (!Ctx) return '';
            const ctx = new Ctx(1, 5000, 44100);
            const osc = ctx.createOscillator();
            osc.type = 'triangle'; osc.frequency.value = 1000;
            const comp = ctx.createDynamicsCompressor();
            osc.connect(comp); comp.connect(ctx.destination); osc.start(0);
            const buffer = await ctx.startRendering();
            const data = buffer.getChannelData(0);
            let sum = 0;
            for (let i = 0; i < data.length; i += 100) sum += Math.abs(data[i]);
            return hashString(sum.toString());
        } catch (e) { return ''; }
    }

    function detectFonts() {
        const base = ['monospace', 'sans-serif', 'serif'];
        const test = ['Arial', 'Calibri', 'Times New Roman', 'Georgia', 'Courier New', 'Verdana', 'Comic Sans MS', 'Tahoma', 'Impact'];
        const span = document.createElement('span');
        span.style.cssText = 'position:absolute;left:-9999px;font-size:72px';
        span.textContent = 'mmmmmmmmmmlli';
        document.body.appendChild(span);
        const baseSizes = {};
        base.forEach((b) => { span.style.fontFamily = b; baseSizes[b] = [span.offsetWidth, span.offsetHeight]; });
        const found = test.filter((f) =>
            base.some((b) => {
                span.style.fontFamily = "'" + f + "'," + b;
                return span.offsetWidth !== baseSizes[b][0] || span.offsetHeight !== baseSizes[b][1];
            })
        );
        document.body.removeChild(span);
        return found;
    }

    async function collectFingerprint() {
        const gl = webglInfo();
        const fp = {
            screen_width: screen.width,
            screen_height: screen.height,
            color_depth: screen.colorDepth,
            timezone: (Intl.DateTimeFormat().resolvedOptions().timeZone) || '',
            languages: navigator.languages ? Array.from(navigator.languages) : [navigator.language],
            hardware_concurrency: navigator.hardwareConcurrency || 0,
            device_memory: navigator.deviceMemory || 0,
            touch_support: ('ontouchstart' in window) || navigator.maxTouchPoints > 0,
            plugins: Array.from(navigator.plugins || []).map((p) => p.name),
            canvas_hash: canvasHash(),
            webgl_vendor: gl.vendor || '',
            webgl_renderer: gl.renderer || '',
            audio_hash: await audioHash(),
            fonts: detectFonts(),
        };
        fp.hash = hashString(JSON.stringify([
            fp.screen_width, fp.screen_height, fp.timezone, fp.canvas_hash,
            fp.webgl_renderer, fp.audio_hash, fp.hardware_concurrency, fp.color_depth,
            fp.languages.join(','), fp.fonts.join(','),
        ]));
        markCheck('fp', fp.canvas_hash !== '' && fp.hardware_concurrency > 0);
        return fp;
    }

    // ---- 3. Cookies & storage ------------------------------------------
    function collectCookies() {
        let cookieOk = false, persisted = false, ls = false, ss = false;
        try {
            document.cookie = 'bg_test=1; SameSite=Lax; path=/';
            cookieOk = navigator.cookieEnabled && document.cookie.indexOf('bg_test=1') !== -1;
            persisted = document.cookie.indexOf('bg_probe=') !== -1; // server-set on /verify
        } catch (e) { /* ignore */ }
        try { localStorage.setItem('bg_ls', '1'); ls = localStorage.getItem('bg_ls') === '1'; localStorage.removeItem('bg_ls'); } catch (e) { ls = false; }
        try { sessionStorage.setItem('bg_ss', '1'); ss = sessionStorage.getItem('bg_ss') === '1'; sessionStorage.removeItem('bg_ss'); } catch (e) { ss = false; }
        const out = { enabled: cookieOk, persisted: persisted, local_storage: ls, session_storage: ss };
        markCheck('cookies', cookieOk && ls);
        return out;
    }

    // ---- 4. Behavior listeners -----------------------------------------
    function noteInteraction() {
        if (!state.firstInteractionAt) state.firstInteractionAt = performance.now();
    }
    window.addEventListener('mousemove', (e) => {
        noteInteraction();
        state.mouse.push({ x: e.clientX, y: e.clientY, t: performance.now() });
        if (state.mouse.length > 600) state.mouse.shift();
    }, { passive: true });
    window.addEventListener('click', () => { noteInteraction(); state.clicks.push(performance.now()); }, { passive: true });
    window.addEventListener('keydown', (e) => {
        noteInteraction();
        state.keys.push(performance.now());
        if (e.key === 'Backspace') state.keyEvents.backspaces++;
    }, { passive: true });
    window.addEventListener('paste', () => { state.keyEvents.paste = true; }, { passive: true });
    window.addEventListener('scroll', () => {
        noteInteraction();
        state.scroll.push({ y: window.scrollY, t: performance.now() });
    }, { passive: true });

    // Link / interactive-element interaction: hover dwell, double-clicks, touch.
    let hoverStart = 0;
    let lastRenderClickGuard = performance.now();
    document.addEventListener('mouseover', (e) => {
        if (e.target.closest('a, button, [role="button"]')) hoverStart = performance.now();
    }, { passive: true });
    document.addEventListener('mouseout', (e) => {
        if (e.target.closest('a, button, [role="button"]') && hoverStart) {
            state.links.hovers.push(performance.now() - hoverStart);
            hoverStart = 0;
        }
    }, { passive: true });
    document.addEventListener('click', (e) => {
        if (e.target.closest('a, button, [role="button"]')) {
            // A click within ~50ms of the element appearing looks scripted.
            if (performance.now() - lastRenderClickGuard < 50) state.links.instantClicks++;
        }
    }, { passive: true });
    document.addEventListener('dblclick', () => { state.links.doubleClicks++; }, { passive: true });
    window.addEventListener('touchstart', () => { state.links.touch++; }, { passive: true });

    function summariseMouse() {
        const pts = state.mouse;
        if (pts.length < 3) return {};
        const speeds = [], angles = [];
        let pathLen = 0;
        for (let i = 1; i < pts.length; i++) {
            const dx = pts[i].x - pts[i - 1].x, dy = pts[i].y - pts[i - 1].y;
            const dt = Math.max(1, pts[i].t - pts[i - 1].t);
            const d = Math.hypot(dx, dy);
            pathLen += d; speeds.push(d / dt);
            if (i > 1) {
                const a1 = Math.atan2(pts[i - 1].y - pts[i - 2].y, pts[i - 1].x - pts[i - 2].x);
                const a2 = Math.atan2(dy, dx);
                angles.push(Math.abs(a2 - a1));
            }
        }
        const straight = Math.hypot(pts[pts.length - 1].x - pts[0].x, pts[pts.length - 1].y - pts[0].y);
        const clickIntervals = state.clicks.slice(1).map((t, i) => t - state.clicks[i]);
        return {
            move_count: pts.length,
            speed_stddev: stddev(speeds),
            straightness: pathLen > 0 ? straight / pathLen : 1,
            avg_curvature: mean(angles),
            click_count: state.clicks.length,
            click_interval_stddev: stddev(clickIntervals),
        };
    }
    function summariseKeyboard() {
        const intervals = state.keys.slice(1).map((t, i) => t - state.keys[i]);
        return {
            keydown_count: state.keys.length,
            avg_interval_ms: mean(intervals),
            interval_stddev_ms: stddev(intervals),
            paste_detected: state.keyEvents.paste,
            backspaces: state.keyEvents.backspaces,
        };
    }
    function summariseScroll() {
        const s = state.scroll;
        if (s.length < 2) return {};
        const speeds = [];
        let dist = 0, dirChanges = 0, lastDir = 0;
        for (let i = 1; i < s.length; i++) {
            const dy = s[i].y - s[i - 1].y;
            const dt = Math.max(1, s[i].t - s[i - 1].t);
            dist += Math.abs(dy); speeds.push(Math.abs(dy) / dt);
            const dir = Math.sign(dy);
            if (dir && lastDir && dir !== lastDir) dirChanges++;
            if (dir) lastDir = dir;
        }
        return {
            event_count: s.length,
            total_distance: dist,
            speed_stddev: stddev(speeds),
            direction_changes: dirChanges,
            dwell_ms: s[s.length - 1].t - s[0].t,
        };
    }
    function summariseLinks() {
        return {
            hover_count: state.links.hovers.length,
            avg_hover_ms: mean(state.links.hovers),
            instant_clicks: state.links.instantClicks,
            double_clicks: state.links.doubleClicks,
            touch_events: state.links.touch,
        };
    }

    // ---- 5. Challenge (slider puzzle OR remote CAPTCHA widget) ----------
    function loadScript(src) {
        return new Promise((resolve, reject) => {
            const s = document.createElement('script');
            s.src = src; s.async = true; s.defer = true;
            s.onload = resolve; s.onerror = () => reject(new Error('script load failed'));
            document.head.appendChild(s);
        });
    }
    function waitFor(cond, timeout) {
        timeout = timeout || 8000;
        return new Promise((resolve, reject) => {
            const t0 = Date.now();
            (function check() {
                if (cond()) return resolve();
                if (Date.now() - t0 > timeout) return reject(new Error('widget timeout'));
                setTimeout(check, 100);
            })();
        });
    }

    /**
     * Render one of the token-based providers (reCAPTCHA v2 / hCaptcha / Turnstile).
     * On success the provider invokes our callback with a response token, which
     * we forward to the server (verified there via siteverify).
     */
    async function renderRemote(type, siteKey, onSolved) {
        const box = document.getElementById('remoteCaptcha');
        document.getElementById('puzzleWrap').classList.add('d-none');
        const label = document.querySelector('#puzzleSection .form-label');
        if (label) label.textContent = 'Complete the challenge to continue';
        state.puzzleStart = performance.now();

        const onToken = (token) => {
            state.captchaToken = token || '';
            state.solveTime = Math.round(performance.now() - state.puzzleStart);
            markCheck('challenge', !!token);
            if (token) onSolved();
        };

        try {
            if (type === 'turnstile') {
                await loadScript('https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit');
                await waitFor(() => window.turnstile && window.turnstile.render);
                window.turnstile.render(box, { sitekey: siteKey, callback: onToken });
            } else if (type === 'hcaptcha') {
                await loadScript('https://js.hcaptcha.com/1/api.js?render=explicit');
                await waitFor(() => window.hcaptcha && window.hcaptcha.render);
                window.hcaptcha.render(box, { sitekey: siteKey, callback: onToken });
            } else if (type === 'recaptcha') {
                await loadScript('https://www.google.com/recaptcha/api.js?render=explicit');
                await waitFor(() => window.grecaptcha && window.grecaptcha.render);
                window.grecaptcha.render(box, { sitekey: siteKey, callback: onToken });
            } else {
                box.textContent = 'Unsupported challenge type.';
            }
        } catch (e) {
            box.className = 'alert alert-warning mt-2';
            box.textContent = 'Could not load the verification challenge. Please reload the page.';
        }
    }

    function setupPuzzle(challenge, onSolved) {
        const section = document.getElementById('puzzleSection');
        section.classList.remove('d-none');

        // Remote CAPTCHA providers render their own widget and yield a token.
        if (challenge && challenge.type && challenge.type !== 'slider') {
            renderRemote(challenge.type, challenge.site_key, onSolved);
            return;
        }

        const wrap = document.getElementById('puzzleWrap');
        const piece = document.getElementById('puzzlePiece');
        const target = document.getElementById('puzzleTarget');
        const width = challenge.track_width || wrap.clientWidth;
        const size = challenge.piece_size || 42;
        const tgt = challenge.target || Math.round(width * 0.6);
        const tol = challenge.tolerance || 10;
        target.style.left = tgt + 'px';

        state.puzzleStart = performance.now();
        let dragging = false, startX = 0, pieceX = 0;

        const onDown = (clientX) => { dragging = true; startX = clientX - pieceX; piece.classList.remove('ok'); noteInteraction(); };
        const onMove = (clientX) => {
            if (!dragging) return;
            pieceX = Math.max(0, Math.min(width - size, clientX - startX));
            piece.style.left = pieceX + 'px';
        };
        const onUp = () => {
            if (!dragging) return;
            dragging = false;
            state.puzzlePosition = Math.round(pieceX);
            state.solveTime = Math.round(performance.now() - state.puzzleStart);
            const aligned = Math.abs(pieceX - tgt) <= tol;
            if (aligned) {
                piece.classList.add('ok');
                markCheck('challenge', true);
                onSolved();
            } else {
                state.retries++;
                pieceX = 0; piece.style.left = '0px';
            }
        };

        piece.addEventListener('mousedown', (e) => onDown(e.clientX));
        window.addEventListener('mousemove', (e) => onMove(e.clientX));
        window.addEventListener('mouseup', onUp);
        piece.addEventListener('touchstart', (e) => onDown(e.touches[0].clientX), { passive: true });
        window.addEventListener('touchmove', (e) => onMove(e.touches[0].clientX), { passive: true });
        window.addEventListener('touchend', onUp);
    }

    // ---- 6. Submit ------------------------------------------------------
    async function submit(signals) {
        const res = document.getElementById('resultBox');
        try {
            const resp = await fetch(BG.endpoints.verify, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'X-Requested-With': 'XMLHttpRequest',
                    'X-CSRF-Token': BG.csrf,
                },
                body: JSON.stringify(signals),
            });
            const data = await resp.json();
            animateScore(data.score || 0, data.classification || 'bot');

            setTimeout(() => {
                if (data.redirect_url) {
                    window.location.href = data.redirect_url;
                } else {
                    res.className = 'alert alert-danger mt-3 mb-0';
                    res.textContent = data.block_message || 'Verification failed. Access blocked.';
                    res.classList.remove('d-none');
                }
            }, 1200);
        } catch (e) {
            res.className = 'alert alert-danger mt-3 mb-0';
            res.textContent = 'A network error occurred. Please reload and try again.';
            res.classList.remove('d-none');
        }
    }

    function animateScore(score, classification) {
        const ring = document.getElementById('scoreRing');
        const num = document.getElementById('scoreNum');
        const label = document.getElementById('scoreLabel');
        let cur = 0;
        const step = () => {
            cur += Math.max(1, Math.round((score - cur) / 6));
            if (cur >= score) cur = score;
            ring.style.setProperty('--value', cur);
            num.textContent = cur;
            if (cur < score) requestAnimationFrame(step);
        };
        step();
        const map = { human: ['Human', 'text-success'], suspicious: ['Suspicious', 'text-warning'], bot: ['Bot', 'text-danger'] };
        const [text, cls] = map[classification] || map.bot;
        label.textContent = text;
        label.className = 'small ' + cls;
    }

    // ---- Orchestration --------------------------------------------------
    async function run() {
        const js = await collectJs();
        const fingerprint = await collectFingerprint();
        const cookies = collectCookies();
        markCheck('behavior', true); // passive collectors are active

        setupPuzzle(BG.challenge || {}, async function onSolved() {
            const now = performance.now();
            const signals = {
                token: BG.token,
                javascript: js,
                fingerprint: fingerprint,
                cookies: cookies,
                mouse: summariseMouse(),
                keyboard: summariseKeyboard(),
                scroll: summariseScroll(),
                links: summariseLinks(),
                timing: {
                    time_on_page_ms: Math.round(now - loadStart),
                    time_to_interact_ms: Math.round((state.firstInteractionAt || now) - loadStart),
                    time_to_submit_ms: Math.round(now - loadStart),
                },
                honeypot: {
                    contact_email: (document.getElementById('hp_contact_email') || {}).value || '',
                    website: (document.getElementById('hp_website') || {}).value || '',
                    render_delay_ms: Math.round(now - loadStart),
                },
                captcha: {
                    provider: BG.provider,
                    position: state.puzzlePosition,
                    solve_time_ms: state.solveTime,
                    retries: state.retries,
                    token: state.captchaToken || '',
                },
            };
            document.getElementById('statusLine').textContent = 'Scoring your session…';
            await submit(signals);
        });

        document.getElementById('statusLine').textContent = 'Almost there — complete the challenge below.';
    }

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