import { ref, reactive, watchEffect } from 'vue';
function base64EncodeUtf8(input) {
    // Browser-first, Node fallback.
    if (typeof globalThis.btoa === 'function') {
        const bytes = new TextEncoder().encode(input);
        let bin = '';
        for (let i = 0; i < bytes.length; i++)
            bin += String.fromCharCode(bytes[i]);
        return globalThis.btoa(bin);
    }
    // eslint-disable-next-line @typescript-eslint/no-var-requires
    return Buffer.from(input, 'utf8').toString('base64');
}
function encodeDevPayload(sql) {
    return base64EncodeUtf8(JSON.stringify({ v: 0, sql }));
}
function evalWhen(when, params) {
    if (when === undefined)
        return true;
    if (typeof when === 'boolean')
        return when;
    try {
        return when(params);
    }
    catch {
        return false;
    }
}
function pickRows(json) {
    const v = json?.rows ?? json?.data ?? json;
    return Array.isArray(v) ? v : [];
}
export function s2queryRuntime(args) {
    // Create a reactive state object for metadata
    const state = reactive({
        executed: false,
        loading: false,
        error: null,
        last_update: null
    });
    // Create the ref for the array data
    const out = ref([]);
    // Expose state properties as getters on the ref itself
    Object.defineProperty(out, 'executed', {
        get: () => state.executed,
        enumerable: true
    });
    Object.defineProperty(out, 'loading', {
        get: () => state.loading,
        enumerable: true
    });
    Object.defineProperty(out, 'error', {
        get: () => state.error,
        enumerable: true
    });
    Object.defineProperty(out, 'last_update', {
        get: () => state.last_update,
        enumerable: true
    });
    Object.defineProperty(out, 'length', {
        get: () => out.value.length,
        enumerable: true
    });
    // Also expose on out.value for template usage
    Object.defineProperty(out.value, 'executed', {
        get: () => state.executed,
        enumerable: false
    });
    Object.defineProperty(out.value, 'loading', {
        get: () => state.loading,
        enumerable: false
    });
    Object.defineProperty(out.value, 'error', {
        get: () => state.error,
        enumerable: false
    });
    Object.defineProperty(out.value, 'last_update', {
        get: () => state.last_update,
        enumerable: false
    });
    let started = false;
    let timer = null;
    function start() {
        if (started)
            return;
        started = true;
        watchEffect(() => {
            const paramsReactive = (args.params ?? {});
            for (const k in paramsReactive)
                void paramsReactive[k];
            if (!evalWhen(args.when, args.params))
                return;
            if (timer)
                clearTimeout(timer);
            timer = setTimeout(() => void runNow(), args.debounceMs);
        });
    }
    function resetNow() {
        // clear rows but keep same array identity
        out.value.splice(0, out.value.length);
        // reset state flags
        state.executed = false;
        state.loading = false;
        state.error = null;
        state.last_update = null;
    }
    function executeNow() {
        state.executed = true;
        start();
        if (timer)
            clearTimeout(timer);
        void runNow();
    }
    // 1) script usage
    out.execute = () => {
        executeNow();
        return out;
    };
    out.reset = () => {
        resetNow();
        return out;
    };
    // 2) template usage (ref-unwrapped): rows.execute(), rows.reset()
    Object.defineProperty(out.value, 'execute', {
        value: () => executeNow(),
        enumerable: false
    });
    Object.defineProperty(out.value, 'reset', {
        value: () => resetNow(),
        enumerable: false
    });
    async function runNow() {
        state.loading = true;
        state.error = null;
        try {
            const qs = (args.qs ?? []);
            const tokens = (args.tokens ?? []);
            let payload;
            if (args.input === 'token') {
                // tolerate older/alternate codegen that might still emit `qs` for tokens
                payload = tokens[0] ?? qs[0];
            }
            else {
                payload = encodeDevPayload(qs[0] ?? '');
            }
            if (!payload) {
                throw new Error(`s2queryRuntime: missing payload (input=${args.input}; has tokens=${tokens.length}; has qs=${qs.length})`);
            }
            const res = await fetch(args.endpoint, {
                method: 'POST',
                headers: {
                    'content-type': 'application/json',
                    ...(args.headers ?? {})
                },
                body: JSON.stringify({
                    payload,
                    params: args.params ?? {}
                })
            });
            const ok = res.status === 200 || res.status === 201;
            const json = await res.json().catch(() => null);
            if (!ok)
                throw new Error(`s2queryRuntime failed (${res.status}): ${JSON.stringify(json)}`);
            const rows = pickRows(json);
            out.value.splice(0, out.value.length, ...rows);
            // Set last_update only on successful row write
            state.last_update = Date.now();
            args.callback?.(out.value);
        }
        catch (e) {
            state.error = e;
            throw e;
        }
        finally {
            state.loading = false;
        }
    }
    if (!args.guarded)
        start();
    return out;
}
