package com.cmcm.crashhandler;

import android.annotation.SuppressLint;
import android.app.Application;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.text.TextUtils;
import android.util.Log;

import com.facebook.react.ReactNativeHost;
import com.facebook.react.bridge.NativeModuleCallExceptionHandler;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.common.JavascriptException;

import org.json.JSONObject;

import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Locale;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/***
 ** @author nieyihe
 ** @email nieyihe@cmcm.com
 ** @date 2019/4/19
 **/
public class OSNativeModuleCallExceptionHandler implements NativeModuleCallExceptionHandler {

    //是否自动重启
    private boolean mIsAutoStart;
    private ReactNativeHost mReactNativeHost;
    private String mCrashLogPath;
    private String mCrashFileName;
    private ExecutorService mSingleThreadExecutor = Executors.newSingleThreadExecutor();
    private boolean isSetPath = false;
    //当触发异常时可能ReactContext还没有初始化完成，并不能获取到，则需要一个Context来保底使用。
    private Application mApplicationContext;

    private IPostCatchExceptionCallback mPostCatchExceptionCallback;
    private Handler mMainHandler = new Handler(Looper.getMainLooper()) {
        @Override
        public void dispatchMessage(Message msg) {
            try {
                super.dispatchMessage(msg);
            } catch (Exception e) {
                Log.w("cmcm", "OSNativeModuleCallExceptionHandler handler dispatchMessage Error  >> " + Log.getStackTraceString(e));
            }
        }
    };


    public OSNativeModuleCallExceptionHandler(ReactNativeHost reactNativeHost, Application context, boolean isAutoStart, String crashLogPath, String crashFileName) {
        mIsAutoStart = isAutoStart;
        mReactNativeHost = reactNativeHost;
        mApplicationContext = context;
        mCrashLogPath = crashLogPath;
        mCrashFileName = crashFileName;
    }

    public void setPostCatchExceptionCallback(IPostCatchExceptionCallback postCatchExceptionCallback) {
        mPostCatchExceptionCallback = postCatchExceptionCallback;
    }

    @SuppressLint("LongLogTag")
    @Override
    public void handleException(final Exception e) {
        Log.w("cmcm", "ReactNativeEnvironment Crash >> " + Log.getStackTraceString(e));
        if (!isSetPath) {
            mCrashLogPath = mApplicationContext.getExternalFilesDir(null).getAbsolutePath() + mCrashLogPath;
            isSetPath = true;
        }
        saveLogInner(mApplicationContext, Log.getStackTraceString(e), DeviceUtils.getCharacterName(), DeviceUtils.getScenesName());
        if (mPostCatchExceptionCallback != null) {
            mPostCatchExceptionCallback.onCatchJSException(e);
        }
        restartEnvironmentIfNeed(e);
    }

    /***
     * 初始化Native环境的异常捕获
     * */
    public void initNativeEnvironmentCrashHandler() {
        if (!isSetPath) {
            mCrashLogPath = mApplicationContext.getExternalFilesDir(null).getAbsolutePath() + mCrashLogPath;
            isSetPath = true;
        }
        final Thread.UncaughtExceptionHandler uncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
        Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
            @Override
            public void uncaughtException(final Thread t, final Throwable e) {
                Log.w("cmcm", "current Thread " + (Thread.currentThread() == Looper.getMainLooper().getThread()));
                saveLogInner(mApplicationContext, Log.getStackTraceString(e), "unknow character", "unknow scenes");
                Log.w("cmcm", "NativeEnvironment Crash >> " + Log.getStackTraceString(e));
                if (uncaughtExceptionHandler != null) {
                    uncaughtExceptionHandler.uncaughtException(t, e);
                }
            }
        });
    }

    /**
     * 当需要时则重启环境
     */
    private void restartEnvironmentIfNeed(final Throwable e) {
        if (mIsAutoStart) {
            final ReactContext reactContext = mReactNativeHost.getReactInstanceManager().getCurrentReactContext();
            if (reactContext != null && e instanceof JavascriptException) {
                //纯JS调用错误
                Log.w("cmcm", "doRestartJSEnvironment");
                mMainHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        //需要再主线程中重启
                        AutoStartHelper.doRestartJSEnvironment(reactContext);
                    }
                });
            } else {
                //当ReactContext还未初始化完成，则使用mApplicationContext重启整个App
                Log.w("cmcm", "doRestartNativeEnvironment");
                AutoStartHelper.doRestartNativeEnvironment(mApplicationContext);
            }
        }
    }

    private String mAndroidId = null;

    /**
     * 保存日志
     */
    private void saveLogInner(Context context, String stack, String character, String scenes) {
        Log.w("cmcm", "start save log >> ");
        StringBuffer sb = new StringBuffer("\n");
        appendln(sb, "----------------------------System Information----------------------------");

        PackageInfo applicationInfo = null;
        try {
            applicationInfo = mApplicationContext.getPackageManager().getPackageInfo(mApplicationContext.getPackageName(), 0);
        } catch (Exception e) {
        }

        if (applicationInfo != null) {
            appendln(sb, "appPkgName:" + applicationInfo.toString());
            appendln(sb, "versionCode:" + applicationInfo.versionCode);
            appendln(sb, "versionName:" + applicationInfo.versionName);
            appendln(sb, "debug:" + (0 != (applicationInfo.applicationInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE)));
        } else {
            appendln(sb, "versionCode:-1");
            appendln(sb, "versionName:null");
            appendln(sb, "debug:un_known");
        }
        if (mAndroidId == null) {
            mAndroidId = DeviceUtils.getAndroidId(context);
        }
        appendln(sb, "board:" + Build.BOARD);
        appendln(sb, "ro.bootloader:" + Build.BOOTLOADER);
        appendln(sb, "ro.product.brand:" + Build.BRAND);
        appendln(sb, "ro.product.cpu.abi:" + Build.CPU_ABI);
        appendln(sb, "ro.product.cpu.abi2:" + Build.CPU_ABI2);
        appendln(sb, "ro.product.device:" + Build.DEVICE);
        appendln(sb, "ro.build.display.id:" + Build.DISPLAY);
        appendln(sb, "ro.build.fingerprint:" + Build.FINGERPRINT);
        appendln(sb, "ro.hardware:" + Build.HARDWARE);
        appendln(sb, "ro.build.host:" + Build.HOST);
        appendln(sb, "ro.build.id:" + Build.ID);
        appendln(sb, "ro.product.manufacturer:" + Build.MANUFACTURER);
        appendln(sb, "ro.product.model:" + Build.MODEL);
        appendln(sb, "ro.product.name:" + Build.PRODUCT);
        appendln(sb, "ro.build.tags:" + Build.TAGS);
        appendln(sb, "ro.build.type:" + Build.TYPE);
        appendln(sb, "ro.build.user:" + Build.USER);
        appendln(sb, "ro.build.version.codename:" + Build.VERSION.CODENAME);
        appendln(sb, "ro.build.version.incremental:" + Build.VERSION.INCREMENTAL);
        appendln(sb, "ro.build.version.release:" + Build.VERSION.RELEASE);
        appendln(sb, "ro.build.version.sdk:" + Build.VERSION.SDK_INT);
        appendln(sb, "language:" + Locale.getDefault().getLanguage());
        appendln(sb, "imei:" + mAndroidId);
        appendln(sb, "aid:" + mAndroidId);
        appendln(sb, "Sn:" + DeviceUtils.getSystemProperties("ro.serialno.robot"));
        appendln(sb, "scenes:" + scenes);
        appendln(sb, "character:" + character);
        appendln(sb, "meminfo:" + DeviceUtils.getMemoryInfoString());
        appendln(sb, "nativefd:" + DeviceUtils.getNativeFdCnt());
        appendln(sb, "Launcher:" + DeviceUtils.getCurrentLauncherName(context));
        appendln(sb, "Root:" + DeviceUtils.isRoot());
        appendln(sb, "storage:" + DeviceUtils.getDeviceStorageInfo());
        appendln(sb, "procname:" + DeviceUtils.getCurrentProcessName(context));
        appendln(sb, "crashtime:" + Long.toString((System.currentTimeMillis() / 1000L)));
        appendln(sb, "----------------------------Exception----------------------------");
        appendln(sb, stack);
        appendln(sb, "----------------------------Exception StackTrace----------------------------");
        FileUtils.appendFile(mCrashLogPath, mCrashFileName, sb.toString());
        postCrashMsg(sb.toString());
        Log.w("cmcm", "end save log >> ");
    }

    /**
     * 将数据发送给服务器
     */
    private void postCrashMsg(final String stack) {
        if (BuildConfig.DEBUG) {
            //debug模式下 不预警
            Log.w("cmcm", "debug don't postCrashMsg");
            return;
        }
        if (TextUtils.isEmpty(stack)) {
            return;
        }
        Log.w("cmcm", "postCrashMsg start");
        String urlString = "http://10.60.116.113:3000/crash";
        HttpURLConnection urlConnection = null;
        try {
            URL url = new URL(urlString);
            urlConnection = (HttpURLConnection) url.openConnection();
            // 设置是否向connection输出，因为这个是post请求，参数要放在
            // http正文内，因此需要设为true
            urlConnection.setDoOutput(true);
            // Read from the connection. Default is true.
            urlConnection.setDoInput(true);
            urlConnection.setRequestMethod("POST");
            // Post 请求不能使用缓存
            urlConnection.setUseCaches(false);
            //设置本次连接是否自动重定向
            urlConnection.setInstanceFollowRedirects(true);
            urlConnection.setRequestProperty("Content-Type","application/json; charset=UTF-8");
            urlConnection.setRequestProperty("Accept", "application/json");

            OutputStream out = new BufferedOutputStream(urlConnection.getOutputStream());

            BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out, "UTF-8"));
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("crash", stack);
            writer.write(jsonObject.toString());
            writer.flush();
            writer.close();
            int response = urlConnection.getResponseCode();
            if(response == HttpURLConnection.HTTP_OK) {
                Log.w("cmcm", "postCrashMsg success");
            }
            out.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        Log.w("cmcm", "postCrashMsg end");
    }

    private void appendln(StringBuffer sb, String word) {
        sb.append(word);
        sb.append("\n");
    }

    private void runTask(SafeRunnable task) {
        mSingleThreadExecutor.execute(task);
    }

    /**
     * 内部捕获异常
     */
    private abstract class SafeRunnable implements Runnable {

        @Override
        public void run() {
            try {
                work();
            } catch (Exception e) {
                Log.w("cmcm", "OSNativeModuleCallExceptionHandler ThreadExecutor execute Error  >> " + Log.getStackTraceString(e));
            }
        }

        abstract void work();
    }


    /**
     * 捕获异常回调
     * */
    public interface IPostCatchExceptionCallback {
        void onCatchJSException(Exception e);
    }
}
