'use strict';
/**
 * MariBank SG 3.2.2 — Java + native attestation / encrypt trace
 * Package: sg.com.maribankmobile.digitalbank
 *
 * SG 差异: 无 utils.d / com.shopee.shpssdk.*，仅 shpssdkbank + uvwuvwuv
 */
const TAG = '[MB-NATIVE]';
const MAX_STR = 4000;
const MAX_BYTES_LOG = 8192;
const HOOKED_NATIVE_PTRS = {};

const JAVA_TARGETS = [
  'com.shopee.shpssdkbank.wvvvuwwu',
  'com.shopee.shpssdkbank.uwuvuvvww.vvuuuuvvv',
  'com.shopee.shpssdkbank.uwuvuvvww.uvwuuuuuw.vvvvuwwvu',
  'com.shopee.shpssdkbank.uwuvuvvww.wvvuuwvwu',
  'com.shopee.shpssdkbank.uvuwwuvwv.uvwwuuvvw',
  'com.shopee.bke.lib.jni.utils.uvwuvwuv',
  'com.shopee.bke.lib.jni.utils.uvwwwwuv',
  'com.shopee.shpssdkbank.SHPSSDK',
];

const SO_WATCH = [
  'libshpssdk_bank.so',
  'libshpssdk.so',
  'libsdkutils.so',
  'libbkutils.so',
];

function log(msg) {
  send(TAG + ' ' + msg);
}

function jniFn(envPtr, index, ret, args) {
  const funcs = envPtr.readPointer();
  const addr = funcs.add(index * Process.pointerSize).readPointer();
  if (!addr || addr.isNull()) return null;
  return new NativeFunction(addr, ret, args);
}

function jniReadByteArray(envPtr, jarrayPtr) {
  if (!jarrayPtr || jarrayPtr.isNull()) return null;
  try {
    const GetArrayLength = jniFn(envPtr, 171, 'int', ['pointer', 'pointer']);
    const GetByteArrayElements = jniFn(envPtr, 184, 'pointer', ['pointer', 'pointer', 'pointer']);
    const ReleaseByteArrayElements = jniFn(envPtr, 187, 'void', ['pointer', 'pointer', 'pointer', 'int']);
    if (!GetArrayLength || !GetByteArrayElements || !ReleaseByteArrayElements) {
      return jniReadByteArrayArt(envPtr, jarrayPtr);
    }
    const len = GetArrayLength(envPtr, jarrayPtr);
    if (len <= 0 || len > MAX_BYTES_LOG) return { len: len, hex: '', text: '' };
    const elems = GetByteArrayElements(envPtr, jarrayPtr, ptr(0));
    if (!elems || elems.isNull()) return { len: len, hex: '', text: '' };
    const raw = elems.readByteArray(Math.min(len, MAX_BYTES_LOG));
    ReleaseByteArrayElements(envPtr, jarrayPtr, elems, 0);
    return bytesToPreview(raw, len);
  } catch (e) {
    return { len: -1, hex: 'err:' + e, text: '' };
  }
}

function bytesToPreview(raw, len) {
  const arr = new Uint8Array(raw);
  let text = '';
  try {
    text = String.fromCharCode.apply(null, arr);
    if (text.indexOf('\u0000') >= 0 || !/^[\x20-\x7e\r\n\t\u4e00-\u9fff\u0100-\u024f]+$/.test(text.substring(0, Math.min(text.length, 200)))) {
      text = '';
    }
  } catch (e) {
    text = '';
  }
  if (text.length > MAX_STR) text = text.substring(0, MAX_STR) + '...';
  return { len: len, hex: hexPreview(arr, 64), text: text, arr: arr };
}

function jniReadByteArrayArt(envPtr, jarrayPtr) {
  const art = moduleByName('libart.so');
  if (!art) return { len: -1, hex: 'err:no-art', text: '' };
  let sym = null;
  art.enumerateSymbols().forEach(function (s) {
    if (sym) return;
    if (s.name.indexOf('GetByteArrayRegion') >= 0 && s.name.indexOf('JNI') >= 0) {
      sym = s.address;
    }
  });
  if (!sym) return { len: -1, hex: 'err:no-GetByteArrayRegion', text: '' };
  const GetLen = jniFn(envPtr, 171, 'int', ['pointer', 'pointer']);
  const len = GetLen ? GetLen(envPtr, jarrayPtr) : 0;
  if (len <= 0 || len > MAX_BYTES_LOG) return { len: len, hex: '', text: '' };
  const buf = Memory.alloc(len);
  const GetRegion = new NativeFunction(sym, 'void', ['pointer', 'pointer', 'int', 'int', 'pointer']);
  GetRegion(envPtr, jarrayPtr, 0, len, buf);
  return bytesToPreview(buf.readByteArray(len), len);
}

function jniReadJstring(envPtr, jstrPtr) {
  if (!jstrPtr || jstrPtr.isNull()) return '';
  try {
    const GetStringUTFChars = jniFn(envPtr, 169, 'pointer', ['pointer', 'pointer', 'pointer']);
    const ReleaseStringUTFChars = jniFn(envPtr, 170, 'void', ['pointer', 'pointer', 'pointer']);
    if (!GetStringUTFChars || !ReleaseStringUTFChars) return '';
    const chars = GetStringUTFChars(envPtr, jstrPtr, ptr(0));
    if (!chars || chars.isNull()) return '';
    const s = chars.readCString();
    ReleaseStringUTFChars(envPtr, jstrPtr, chars);
    return s || '';
  } catch (e) {
    return '';
  }
}

function dumpNativeArgs(methodName, sig, envPtr, args) {
  if (methodName === 'vuwuuwvw' && sig.indexOf('[B[B') >= 0) {
    const a0 = jniReadByteArray(envPtr, args[2]);
    const a1 = jniReadByteArray(envPtr, args[3]);
    if (a0) log('  nat in0 len=' + a0.len + ' hex=' + a0.hex);
    if (a1) log('  nat in1 len=' + a1.len + ' hex=' + a1.hex);
    return;
  }
  if (methodName === 'uvwuuww') {
    const plain = jniReadByteArray(envPtr, args[2]);
    const key = jniReadJstring(envPtr, args[3]);
    const flag = args[4] ? args[4].toInt32() : 0;
    if (plain) {
      log('  nat plain len=' + plain.len + ' hex=' + plain.hex);
      if (plain.text) log('  nat plain utf8=' + plain.text);
    }
    if (key) log('  nat key=' + key + ' flag=' + flag);
    return;
  }
  if (methodName === 'vuwuuuwv' && sig.indexOf('[B[B') >= 0) {
    const a0 = jniReadByteArray(envPtr, args[2]);
    const a1 = jniReadByteArray(envPtr, args[3]);
    if (a0) log('  nat defense in0 len=' + a0.len + ' hex=' + a0.hex);
    if (a1) log('  nat defense in1 len=' + a1.len + ' hex=' + a1.hex);
  }
}

function hexPreview(arr, limit) {
  if (!arr) return '';
  const n = Math.min(arr.length, limit || 64);
  let hex = '';
  for (let i = 0; i < n; i++) {
    const b = (arr[i] & 0xff).toString(16);
    hex += (b.length === 1 ? '0' : '') + b;
  }
  if (arr.length > n) hex += '...(' + arr.length + ')';
  return hex;
}

function dumpBytes(label, jobj) {
  if (jobj === null || jobj === undefined) {
    log(label + ' = null');
    return;
  }
  try {
    const arr = Java.cast(jobj, Java.use('[B'));
    let text = '';
    try {
      text = Java.use('java.lang.String').$new(arr, 'UTF-8').toString();
    } catch (e) {
      text = '';
    }
    const printable = text.length > 0 && text.indexOf('\u0000') < 0;
    if (printable && (text.indexOf('rdVerifyInfo') >= 0 || text.indexOf('REGISTRATION') >= 0
        || text.indexOf('deviceFingerprint') >= 0 || text.length < MAX_STR)) {
      const show = text.length > MAX_STR ? text.substring(0, MAX_STR) + '...' : text;
      log(label + ' byte[' + arr.length + '] utf8=' + show);
    } else {
      log(label + ' byte[' + arr.length + '] hex=' + hexPreview(arr, 48));
    }
  } catch (e) {
    log(label + ' dump err=' + e);
  }
}

function dumpJava(label, obj) {
  if (obj === null || obj === undefined) {
    log(label + ' = null');
    return;
  }
  try {
    const cls = obj.getClass().getName();
    if (cls === '[B') {
      dumpBytes(label, obj);
      return;
    }
    if (cls === 'java.lang.String') {
      const s = Java.cast(obj, Java.use('java.lang.String')).toString();
      const show = s.length > MAX_STR ? s.substring(0, MAX_STR) + '...' : s;
      log(label + ' String(' + s.length + ') ' + show);
      return;
    }
    if (cls === '[Ljava.lang.String;') {
      const arr = Java.cast(obj, Java.use('[Ljava.lang.String;'));
      log(label + ' String[' + arr.length + ']');
      for (let i = 0; i < arr.length; i++) dumpJava(label + '[' + i + ']', arr[i]);
      return;
    }
    if (cls === '[[B') {
      const outer = Java.cast(obj, Java.use('[[B'));
      log(label + ' byte[][] len=' + outer.length);
      for (let i = 0; i < outer.length; i++) dumpBytes(label + '[' + i + ']', outer[i]);
      return;
    }
    log(label + ' ' + cls + ' = ' + obj.toString());
  } catch (e) {
    log(label + ' err=' + e);
  }
}

function shouldLogRegisterText(s) {
  if (!s) return false;
  const low = s.toLowerCase();
  return low.indexOf('register') >= 0 || low.indexOf('rdverifyinfo') >= 0
      || low.indexOf('datakey') >= 0 || low.indexOf('fingerprint') >= 0
      || low.indexOf('3100012') >= 0 || s.indexOf('|') >= 0;
}

/* ---------- native: dlopen + RegisterNatives ---------- */

function moduleExport(moduleName, symbol) {
  if (typeof Module.getExportByName === 'function') {
    try {
      return Module.getExportByName(moduleName, symbol);
    } catch (e) {
      return null;
    }
  }
  if (typeof Module.findExportByName === 'function') {
    return Module.findExportByName(moduleName, symbol);
  }
  return null;
}

function moduleByName(name) {
  if (typeof Process.getModuleByName === 'function') {
    try {
      return Process.getModuleByName(name);
    } catch (e) {
      return null;
    }
  }
  if (typeof Process.findModuleByName === 'function') {
    return Process.findModuleByName(name);
  }
  return null;
}

function moduleByAddress(addr) {
  if (typeof Process.getModuleByAddress === 'function') {
    try {
      return Process.getModuleByAddress(addr);
    } catch (e) {
      return null;
    }
  }
  if (typeof Process.findModuleByAddress === 'function') {
    return Process.findModuleByAddress(addr);
  }
  return null;
}

function hookDlopen() {
  const names = ['android_dlopen_ext', '__loader_android_dlopen_ext', 'dlopen'];
  names.forEach(function (sym) {
    const addr = moduleExport(null, sym);
    if (!addr) return;
    Interceptor.attach(addr, {
      onEnter(args) {
        try {
          this.path = args[0].readCString();
        } catch (e) {
          this.path = '';
        }
      },
      onLeave() {
        if (!this.path) return;
        SO_WATCH.forEach(function (so) {
          if (this.path.indexOf(so) >= 0) log('dlopen ' + this.path);
        }, this);
      },
    });
    log('hooked ' + sym);
  });
}

function findRegisterNatives() {
  const art = moduleByName('libart.so');
  if (!art) return null;
  let found = null;
  art.enumerateSymbols().forEach(function (sym) {
    if (found) return;
    const n = sym.name;
    if (n.indexOf('RegisterNatives') >= 0
        && n.indexOf('CheckJNI') < 0
        && n.indexOf('art') >= 0) {
      found = sym.address;
    }
  });
  return found;
}

function hookNativePtr(className, methodName, sig, fnPtr) {
  const key = fnPtr.toString();
  if (HOOKED_NATIVE_PTRS[key]) return;
  HOOKED_NATIVE_PTRS[key] = true;
  const mod = moduleByAddress(fnPtr);
  const modName = mod ? mod.name : '?';
  const off = mod ? fnPtr.sub(mod.base) : fnPtr;
  log('RegisterNatives HOOK ' + className + '.' + methodName + sig
      + ' @ ' + modName + '+0x' + off.toString(16));

  try {
    Interceptor.attach(fnPtr, {
      onEnter(args) {
        this.mname = methodName;
        this.msig = sig;
        this.env = args[0];
        log('native>> ' + className + '.' + methodName + sig);
        dumpNativeArgs(methodName, sig, this.env, args);
      },
      onLeave(retval) {
        log('native<< ' + className + '.' + methodName + ' ret=' + retval);
      },
    });
  } catch (e) {
    log('Interceptor.attach fail ' + methodName + ': ' + e);
  }
}

function resolveJClassName(jclassPtr) {
  if (typeof Java === 'undefined' || !Java.available) {
    return '';
  }
  let className = '';
  const run = (typeof Java.performNow === 'function') ? Java.performNow : Java.perform;
  try {
    run(function () {
      className = Java.cast(jclassPtr, Java.use('java.lang.Class')).getName();
    });
  } catch (e) {
    className = '';
  }
  return className;
}

function isInterestingSo(modName) {
  if (!modName) return false;
  return modName.indexOf('shpssdk') >= 0
      || modName.indexOf('sdkutils') >= 0
      || modName.indexOf('bkutils') >= 0;
}

function hookRegisterNatives() {
  const addr = findRegisterNatives();
  if (!addr) {
    log('RegisterNatives symbol not found');
    return;
  }
  Interceptor.attach(addr, {
    onEnter(args) {
      const count = args[3].toInt32();
      const methods = args[2];
      const clazz = args[1];
      const className = resolveJClassName(clazz) || '<unknown>';
      const classHit = className.indexOf('shpssdk') >= 0
          || className.indexOf('jni.utils') >= 0
          || className.indexOf('bke.lib.jni') >= 0;

      const ptrSize = Process.pointerSize;
      let loggedClass = false;
      for (let i = 0; i < count; i++) {
        const base = methods.add(i * ptrSize * 3);
        const name = base.readPointer().readCString();
        const sig = base.add(ptrSize).readPointer().readCString();
        const fnPtr = base.add(ptrSize * 2).readPointer();
        const mod = moduleByAddress(fnPtr);
        const modName = mod ? mod.name : '';
        if (!classHit && !isInterestingSo(modName)) continue;
        if (!loggedClass) {
          log('RegisterNatives class=' + className + ' count=' + count);
          loggedClass = true;
        }
        log('  JNI ' + name + sig + ' -> ' + fnPtr + ' (' + modName + ')');
        hookNativePtr(className, name, sig, fnPtr);
      }
    },
  });
  log('hooked RegisterNatives @ ' + addr);
}

/* ---------- Java: hook static native + key methods ---------- */

function hookJavaMethod(className, clazz, methodName, isNative, retName, isStatic) {
  try {
    const overloads = clazz[methodName].overloads;
    overloads.forEach(function (ovl) {
      ovl.implementation = function () {
        const args = [].slice.call(arguments);
        log('Java>> ' + className + '.' + methodName
            + (isStatic ? ' static' : '')
            + (isNative ? ' native' : '') + ' args=' + args.length);
        args.forEach(function (a, idx) { dumpJava('  in' + idx, a); });

        const ret = ovl.apply(this, args);

        if (retName === 'void') {
          log('Java<< ' + methodName + ' void');
        } else if (retName === '[B') {
          dumpBytes('  out', ret);
        } else if (retName === 'java.lang.String') {
          dumpJava('  out', ret);
        } else if (retName === '[[B') {
          dumpJava('  out', ret);
        } else if (retName === 'boolean' || retName === 'int' || retName === 'long') {
          log('  out=' + ret);
        } else {
          dumpJava('  out', ret);
        }
        return ret;
      };
    });
    log('hooked ' + className + '.' + methodName + ' overloads=' + overloads.length
        + (isNative ? ' native' : '') + (isStatic ? ' static' : ''));
    return 1;
  } catch (e) {
    return 0;
  }
}

function hookClassMethods(className, staticOnly, instanceOnly) {
  let clazz;
  try {
    clazz = Java.use(className);
  } catch (e) {
    log('skip Java class ' + className + ': ' + e);
    return 0;
  }

  const Modifier = Java.use('java.lang.reflect.Modifier');
  const declared = clazz.class.getDeclaredMethods();
  let hooked = 0;

  for (let i = 0; i < declared.length; i++) {
    const m = declared[i];
    const isStatic = Modifier.isStatic(m.getModifiers());
    if (staticOnly && !isStatic) continue;
    if (instanceOnly && isStatic) continue;

    const methodName = m.getName();
    const isNative = Modifier.isNative(m.getModifiers());
    const retName = m.getReturnType().getName();
    hooked += hookJavaMethod(className, clazz, methodName, isNative, retName, isStatic);
  }
  return hooked;
}

function hookShpsSdkFacade() {
  try {
    const SHPSSDK = Java.use('com.shopee.shpssdkbank.SHPSSDK');
    ['getRiskToken', 'getRiskSync', 'requestDefense', 'assessRisk'].forEach(function (name) {
      if (!SHPSSDK[name]) return;
      SHPSSDK[name].overloads.forEach(function (ovl) {
        ovl.implementation = function () {
          const args = [].slice.call(arguments);
          log('Java>> SHPSSDK.' + name);
          args.forEach(function (a, idx) { dumpJava('  in' + idx, a); });
          const ret = ovl.apply(this, args);
          dumpJava('  out', ret);
          return ret;
        };
      });
      log('hooked SHPSSDK.' + name);
    });
  } catch (e) {
    log('SHPSSDK facade skip: ' + e);
  }
}

function hookOkHttp() {
  try {
    const RealCall = Java.use('okhttp3.RealCall');
    RealCall.execute.implementation = function () {
      const req = this.request();
      const url = req.url().toString();
      if (url.indexOf('register') >= 0 || url.indexOf('dfp') >= 0 || url.indexOf('uapi') >= 0) {
        log('HTTP>> ' + req.method() + ' ' + url);
      }
      const resp = this.execute.call(this);
      if (url.indexOf('register') >= 0) {
        try {
          const peek = resp.peekBody(Java.use('java.lang.Long').parseLong('1048576'));
          log('HTTP<< register ' + peek.string());
        } catch (e) {
          log('HTTP<< register peek err=' + e);
        }
      }
      return resp;
    };
    log('hooked OkHttp RealCall.execute');
  } catch (e) {
    log('OkHttp skip: ' + e);
  }
}

function hookGsonRegister() {
  try {
    const Gson = Java.use('com.google.gson.Gson');
    Gson.toJson.overload('java.lang.Object').implementation = function (obj) {
      const ret = this.toJson(obj);
      if (shouldLogRegisterText(ret)) {
        const show = ret.length > MAX_STR ? ret.substring(0, MAX_STR) + '...' : ret;
        log('Gson.toJson REGISTRATION len=' + ret.length + ' ' + show);
      }
      return ret;
    };
    log('hooked Gson.toJson');
  } catch (e) {
    log('Gson skip: ' + e);
  }
}

function hookRiskTokenEntry() {
  try {
    const V = Java.use('com.shopee.shpssdkbank.uwuvuvvww.vvuuuuvvv');
    hookJavaMethod(
        'com.shopee.shpssdkbank.uwuvuvvww.vvuuuuvvv',
        V, 'wwvuwuwvu', false, 'java.lang.String', true);
  } catch (e) {
    log('vvuuuuvvv skip: ' + e);
  }
}

function hookEncryptHelper() {
  hookClassMethods('com.shopee.bke.lib.jni.utils.uvwwwwuv', false, true);
}

function isAdbSettingKey(key) {
  if (!key) return false;
  const lower = key.toLowerCase();
  return lower.indexOf('adb') >= 0
      || lower === 'development_settings_enabled'
      || lower.indexOf('wireless_debug') >= 0;
}

function hookAdbBypassJava() {
  try {
    const fakeInt = function (key) {
      if (isAdbSettingKey(key)) {
        log('fake Settings int ' + key + ' -> 0');
        return 0;
      }
      return null;
    };
    const fakeStr = function (key) {
      if (isAdbSettingKey(key)) {
        log('fake Settings str ' + key + ' -> 0');
        return '0';
      }
      return null;
    };

    ['Global', 'Secure', 'System'].forEach(function (bucket) {
      const Cls = Java.use('android.provider.Settings$' + bucket);
      Cls.getInt.overloads.forEach(function (ovl) {
        ovl.implementation = function () {
          const key = arguments[1];
          const f = fakeInt(String(key));
          if (f !== null) return f;
          return ovl.apply(this, arguments);
        };
      });
      if (Cls.getString) {
        Cls.getString.overloads.forEach(function (ovl) {
          ovl.implementation = function () {
            const key = arguments[1];
            const f = fakeStr(String(key));
            if (f !== null) return f;
            return ovl.apply(this, arguments);
          };
        });
      }
    });

    const SysProp = Java.use('android.os.SystemProperties');
    SysProp.get.overload('java.lang.String').implementation = function (key) {
      if (key === 'init.svc.adbd' || key === 'init.svc.adb_wifi') {
        log('fake SystemProperties ' + key + ' -> stopped');
        return 'stopped';
      }
      return this.get(key);
    };
    log('hooked ADB Settings/SystemProperties bypass');
  } catch (e) {
    log('ADB Java bypass skip: ' + e);
  }
}

function installJavaHooks() {
  hookAdbBypassJava();
  let total = 0;
  JAVA_TARGETS.forEach(function (cn) {
    total += hookClassMethods(cn, true, false);
  });
  hookRiskTokenEntry();
  hookEncryptHelper();
  hookShpsSdkFacade();
  hookOkHttp();
  hookGsonRegister();
  log('Java hooks installed methods=' + total + ' pid=' + Process.id);
  log('READY SG — Sign up -> +65 -> Next (watch native>> / Gson / HTTP)');
}

function waitForJava(n) {
  n = n || 0;
  if (typeof Java === 'undefined' || !Java.available) {
    if (n % 10 === 0) log('waiting Java.available attempt=' + n);
    setTimeout(function () { waitForJava(n + 1); }, 500);
    return;
  }
  Java.perform(function () {
    installJavaHooks();
  });
}

setImmediate(function () {
  log('SG native trace loaded pid=' + Process.id);
  hookDlopen();
  hookRegisterNatives();
  waitForJava(0);
});
