"use strict";

// jso-protector-reactnative — mobile RASP runtime + build glue for
// React Native / Cordova / Ionic hybrid apps.
//
// Build-time obfuscation of the JS bundle is handled by the existing
// `jso-protector/react-native` Metro integration; this package adds the
// RUNTIME half: a mobile-class RASP guard that runs inside the shipped
// app, merges JS-observable tripwires with a host-wired native probe
// (root/jailbreak/Frida/emulator), scores the result, routes it to your
// SIEM through the same beacon envelope the browser modules use, and
// fires a configurable response.
//
// Public API
// ----------
//   const { createMobileGuard } = require("jso-protector-reactnative");
//
//   const guard = createMobileGuard({
//     nativeProbe: JsoNativeRasp.probe,   // () => Promise<nativeResult> | nativeResult
//     beaconUrl:   "https://beacon.example.com/v1/jso/mobile",
//     intervalMs:  30000,                  // re-evaluate cadence (0 = once)
//     isReleaseBuild: !__DEV__,
//     isDev: __DEV__,
//     onThreat: (report) => { /* your hook */ },
//     respondOnCompromised: "callback",    // see countermeasures.js actions
//   });
//   guard.start();                          // begins polling
//   const report = await guard.checkNow();  // one-shot, returns the report
//   guard.stop();
//
// The Metro serializer is re-exported for convenience so a host app can
// `require("jso-protector-reactnative").withMetro(...)` instead of
// reaching into jso-protector directly.

const rasp = require("./rasp.js");
const nativeProbeMod = require("./native-probe.js");

const DEFAULT_INTERVAL_MS = 30000;

function createMobileGuard(config) {
    config = config || {};
    const nativeProbe = typeof config.nativeProbe === "function" ? config.nativeProbe : nativeProbeMod.noopProbe;
    const onThreat = typeof config.onThreat === "function" ? config.onThreat : null;
    const onCompromised = typeof config.onCompromised === "function" ? config.onCompromised : null;
    const compromisedResponse = config.respondOnCompromised || "callback";
    if (["callback", "lock", "lock-and-callback"].indexOf(compromisedResponse) < 0) {
        throw new Error("mobile-rasp: respondOnCompromised must be callback, lock, or lock-and-callback");
    }
    const beaconUrl = config.beaconUrl || null;
    const intervalMs = (typeof config.intervalMs === "number" && config.intervalMs >= 0) ? config.intervalMs : DEFAULT_INTERVAL_MS;
    const fetchImpl = config.fetch || (typeof fetch === "function" ? fetch : null);
    const setIntervalImpl = config.setInterval || (typeof setInterval === "function" ? setInterval : null);
    const clearIntervalImpl = config.clearInterval || (typeof clearInterval === "function" ? clearInterval : null);
    const globalObj = config.globalObj || (typeof globalThis !== "undefined" ? globalThis : null);
    const nowImpl = config.now || (typeof Date !== "undefined" ? function () { return Date.now(); } : null);

    let timer = null;
    let lastVerdict = null;
    const history = [];

    async function evaluateOnce() {
        let nativeResult = null;
        try {
            nativeResult = await Promise.resolve(nativeProbe());
        } catch (e) {
            nativeResult = { probeError: (e && e.message) || "probe failed" };
        }
        const report = rasp.evaluate({
            globalObj: globalObj,
            nativeResult: nativeResult && nativeResult.probeError ? null : nativeResult,
            isDev: config.isDev,
            isReleaseBuild: config.isReleaseBuild,
            now: nowImpl,
            platform: config.platform,
            observedAt: config.observedAt || null,
        });
        if (nativeResult && nativeResult.probeError) {
            report.nativeProbeError = nativeResult.probeError;
        }

        // Fire the threat hook on any non-clean verdict, or on a
        // verdict change (clean->...) so the host can clear UI state.
        const changed = report.verdict !== lastVerdict;
        lastVerdict = report.verdict;
        history.push({ verdict: report.verdict, score: report.score });
        if (history.length > 50) history.shift();

        if (report.verdict !== "clean" || changed) {
            if (onThreat) { try { onThreat(report); } catch (e) { /* host hook must not break the loop */ } }
            if (beaconUrl) sendBeacon(report);
        }
        if (report.verdict === "compromised") {
            if ((compromisedResponse === "lock" || compromisedResponse === "lock-and-callback") && globalObj) {
                globalObj.__jsoMobileLocked = true;
                report.response = "lock";
            }
            if ((compromisedResponse === "callback" || compromisedResponse === "lock-and-callback") && onCompromised) {
                try { onCompromised(report); report.response = report.response ? report.response + "+callback" : "callback"; } catch (e) { report.responseError = (e && e.message) || "callback failed"; }
            }
        }
        return report;
    }

    function sendBeacon(report) {
        if (!beaconUrl || !fetchImpl) return;
        try {
            const result = fetchImpl(beaconUrl, {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify(report),
            });
            if (result && typeof result.catch === "function") result.catch(function () {});
        } catch (e) { /* swallow */ }
    }

    return {
        checkNow: evaluateOnce,
        start: function () {
            if (timer || !setIntervalImpl || intervalMs === 0) {
                // intervalMs 0 means "evaluate once on start, no polling".
                evaluateOnce();
                return;
            }
            evaluateOnce();
            timer = setIntervalImpl(evaluateOnce, intervalMs);
        },
        stop: function () {
            if (timer && clearIntervalImpl) { clearIntervalImpl(timer); timer = null; }
        },
        lastVerdict: function () { return lastVerdict; },
        history: function () { return history.slice(); },
    };
}

// Lazy Metro re-export so loading this module never hard-fails when
// jso-protector isn't installed (e.g. running the pure-engine tests).
function withMetro() {
    var mod = require("jso-protector/react-native");
    return mod.withJsoProtectorMetro.apply(mod, arguments);
}

module.exports = {
    SCHEMA_VERSION: rasp.SCHEMA_VERSION,
    createMobileGuard: createMobileGuard,
    evaluate: rasp.evaluate,
    KNOWN_TOOLS: rasp.KNOWN_TOOLS,
    SIGNAL_WEIGHTS: rasp.SIGNAL_WEIGHTS,
    nativeProbeSchema: nativeProbeMod.schema,
    noopProbe: nativeProbeMod.noopProbe,
    withMetro: withMetro,
};
