package com.avg.zaap.crashlibrary.core;

import android.content.Context;
import android.os.Process;
import android.os.RemoteException;

import com.avg.zaap.crashlibrary.BuildConfig;
import com.avg.zaap.crashlibrary.CrashLib;
import com.avg.zaap.crashlibrary.ICrashServiceCommunicator;
import com.avg.zaap.crashlibrary.ReportData;
import com.avg.zaap.crashlibrary.core.hash.IHashGenerator;
import com.avg.zaap.crashlibrary.core.helper.process.IProcessHelper;
import com.avg.zaap.crashlibrary.core.logger.CrashLogger;
import com.avg.zaap.crashlibrary.provider.CrashProviderException;
import com.avg.zaap.crashlibrary.provider.ICrashSyncProvider;
import com.avg.zaap.crashlibrary.provider.stacktrace.IStackTraceWriter;
import com.avg.zaap.crashlibrary.provider.stacktrace.StackTraceWriterException;
import com.avg.zaap.crashlibrary.publish.cap.ICapHelper;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

/**
 * This class serves as the application's default Exception handler.
 * <p>
 * Created by marek.osvald on 31.3.2016.
 */
public class CrashUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler, ICrashReportingServiceCallback {

    private static final String TAG = CrashUncaughtExceptionHandler.class.getSimpleName();
    private final Context mContext;
    private List<String> mFilesToPack = new ArrayList<>();

    private ICrashServiceCommunicator mCrashReportingService;
    private IHashGenerator mHashGenerator;
    private ICrashUncaughtExceptionHandlerHelper mHelper;
    private ICapHelper mCapHelper;
    private IProcessHelper mProcessHelper;
    private ICrashUncaughtExceptionHandlerFactory mFactory;

    /**
     * Parametric constructor. Sets the Android context. Uses default dependency factory.
     *
     * @param context Android context
     */
    public CrashUncaughtExceptionHandler(Context context) {
        this(context, new CrashUncaughtExceptionHandlerFactory());
    }

    /**
     * Parametric constructor. Sets the Android context and dependency factory to be used.
     *
     * @param context Android context
     * @param factory dependency factory to be used
     */
    public CrashUncaughtExceptionHandler(Context context, ICrashUncaughtExceptionHandlerFactory factory) {
        CrashLogger.d(TAG, "Constructing exception handler.");

        mContext = context;
        mHashGenerator = factory.getHashGenerator();
        mHelper = factory.getHelper();
        mCapHelper = factory.getCapHelper();
        mProcessHelper = factory.getProcessHelper();
        mFactory = factory;

        if (!mProcessHelper.isReportingServiceProcess(context)) {
            bindReportingService();
        }
    }

    @Override
    public void uncaughtException(Thread thread, final Throwable throwable) {
        CrashLogger.d(TAG, "UncaughtException: %s", throwable.getMessage());

        String pathToLogsDirectory = mContext.getFilesDir().getAbsolutePath();

        //get logs with stack traces
        IStackTraceWriter stackTraceWriter = mFactory.getStackTraceWriter();

        if (!CrashLib.getConfig().isDisabledStackTraceAllThreads()) {
            try {
                final String stackTraces = stackTraceWriter.writeAllStackTraces(pathToLogsDirectory);
                mFilesToPack.add(stackTraces);
            } catch (StackTraceWriterException e) {
                CrashLogger.e(TAG, "Writing stack trace of all threads failed: %s.", e.getMessage());
            }
        }

        if (!CrashLib.getConfig().isDisabledStackTraceCrashingThread()) {
            try {
                final String crashingStacktrace = stackTraceWriter.writeCrashingThreadStackTrace(pathToLogsDirectory, throwable);
                mFilesToPack.add(crashingStacktrace);
            } catch (StackTraceWriterException e) {
                CrashLogger.e(TAG, "Writing stack trace of crashing thread failed: %s.", e.getMessage());
            }
        }

        //call all synchronous providers
        List<ICrashSyncProvider> syncProviders = CrashLib.getConfig().getSyncProviders();
        if (syncProviders != null) {
            for (ICrashSyncProvider syncProvider : syncProviders) {
                try {
                    String filepath = syncProvider.provideData(pathToLogsDirectory);
                    mFilesToPack.add(filepath);
                    CrashLogger.e(TAG, "Sync provider provided data to %s.", filepath);
                } catch (CrashProviderException e) {
                    CrashLogger.e(TAG, "Sync provider failed to provide data: %s", e.getMessage());
                }
            }
        }

        //report crash
        reportAndroidCrash(throwable);

        //kill process
        android.os.Process.killProcess(Process.myPid());
    }

    private void bindReportingService() {
        IServiceConnectionHelper serviceConnectionHelper = mFactory.getServiceConnectionHelper(this);
        serviceConnectionHelper.startServiceConnectionThread(mContext);
    }

    private void reportAndroidCrash(Throwable throwable) {
        final String crashCode = mHashGenerator.generateHash(throwable.getStackTrace());
        final StackTraceElement topElement = throwable.getStackTrace()[0];

        ReportData reportData = mFactory.getReportData();

        // common
        reportData.setCrashCode(crashCode);
        reportData.setErrorName(throwable.getClass().getSimpleName());
        reportData.setErrorMessage(throwable.getMessage());
        reportData.setErrorDetails(mHelper.getErrorDetails(throwable));
        reportData.setFilesToPack(mFilesToPack);

        // CAP
        reportData.setCapExceptionCode(mCapHelper.getExceptionCodeFromCrashCode(crashCode));
        reportData.setCapFaultingOffset(topElement.getLineNumber());
        reportData.setCapModuleName(String.format("%s_%s_%s", topElement.getFileName(), topElement.getClassName(), topElement.getMethodName()));
        reportData.setCapModuleVersion(CrashLib.getConfig().getApplicationVersion());
        reportData.setCapProcessName(mProcessHelper.getCurrentProcessName(mContext));
        reportData.setCapProcessVersion(BuildConfig.VERSION_NAME);
        reportData.setCapGUID(UUID.randomUUID().toString());

        // offline
        reportData.setPublishAttempts(0);
        reportData.setLastAttemptTimestamp(0);

        if (crashCode != null) {
            if (mCrashReportingService != null) {
                try {
                    CrashLogger.e(TAG, "Report Android crash: %s, mFilesToPack: %s", crashCode, mFilesToPack.size());
                    mCrashReportingService.reportAndroidCrash(reportData);
                } catch (RemoteException ex) {
                    CrashLogger.e(TAG, "Report Android crash has failed. %s", ex.getMessage());
                }
            }
        } else {
            CrashLogger.w(TAG, "Generated hash is null!");
        }
    }

    @Override
    public void onServiceConnected(ICrashServiceCommunicator crashReportingService) {
        if (mCrashReportingService != null) {
            CrashLogger.e(TAG, "mCrashReportingService is not null");
            return;
        }

        mCrashReportingService = crashReportingService;
    }

}
