package com.avg.zaap.crashlibrary;

import android.content.Context;
import android.os.RemoteException;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;

import com.avg.zaap.crashlibrary.core.logger.CrashLogger;
import com.avg.zaap.crashlibrary.offline.IConnectivityHelper;
import com.avg.zaap.crashlibrary.offline.Strategy;
import com.avg.zaap.crashlibrary.offline.publisher.IOfflinePublisher;
import com.avg.zaap.crashlibrary.offline.publisher.IOfflinePublisherCallback;
import com.avg.zaap.crashlibrary.publish.ICrashPublisher;
import com.avg.zaap.crashlibrary.publish.ICrashPublisherCallback;
import com.avg.zaap.crashlibrary.publish.packer.IBinPacker;

import java.io.File;
import java.util.List;

/**
 * This class serves as the default crash publishing delegate. Works as a single point of entry
 * and starts publishing process on all enabled publishers.
 * <p/>
 * Created by marek.osvald on 1.7.2016.
 */
public class CrashPublishingDelegate implements ICrashPublishingDelegate, ICrashPublisherCallback, IBinPackerCallback, IOfflinePublisherCallback {

    private static final String TAG = CrashPublishingDelegate.class.getSimpleName();

    private Context mContext;
    private String mReportFilePath;
    private int mSuccessfulPublisherCounter;
    private int mUnsuccessfulPublisherCounter;
    private final Object mLock = new Object();
    private ICrashServiceCallback mCallback;
    private IConnectivityHelper mConnectivityHelper;
    private IOfflinePublisher mOfflinePublisher;
    private ICrashPublishingDelegateFactory mFactory;
    private int mPublisherCount;

    /**
     * Parametric constructor. Sets Android context.
     *
     * @param context Android context to be set
     */
    public CrashPublishingDelegate(Context context) {
        this(context, new CrashPublishingDelegateFactory());
    }

    /**
     * Parametric constructor. Sets Android context and dependency factory to be used.
     *
     * @param context Android context to be set
     * @param factory dependency factory to be used
     */
    public CrashPublishingDelegate(Context context, ICrashPublishingDelegateFactory factory) {
        mContext = context;
        mPublisherCount = CrashLib.getConfig().getPublishers().size();
        mConnectivityHelper = factory.getConnectivityHelper(context);
        mOfflinePublisher = factory.getOfflinePublisher(this);
        mFactory = factory;
    }

    @Override
    public void publishCrash(final ReportData reportData, ICrashServiceCallback callback) {
        mCallback = callback;
        Thread thread = mFactory.getThread(new Runnable() {

            @Override
            public void run() {
                packReports(reportData);
            }

        });

        thread.start();
    }

    @Override
    public void publishOfflineCrash(final String filePath, final ReportData reportData, ICrashServiceCallback callback) {
        reportData.incrementsPublishAttempts();

        mCallback = callback;
        mReportFilePath = filePath;

        for (final ICrashPublisher publisher : CrashLib.getConfig().getPublishers()) {
            Thread thread = mFactory.getThread(new Runnable() {

                @Override
                public void run() {
                    publisher.publish(mContext, reportData, filePath, CrashPublishingDelegate.this);
                }

            });

            thread.start();
        }
    }

    @Override
    public void onBinPackerSuccess(@NonNull final ReportData reportData, final String filePath) {
        CrashLogger.d(TAG, "onBinPackerSuccess() called, filePath: %s", filePath);
        cleanPackedData(reportData.getFilesToPack());

        mReportFilePath = filePath;

        final ConnectionStatus connectionStatus = mConnectivityHelper.getConnectionStatus();

        if (mConnectivityHelper.isConnectivitySuitable(connectionStatus)) {
            reportData.incrementsPublishAttempts();

            // if online publish bin-packed file to all publishers
            for (final ICrashPublisher publisher : CrashLib.getConfig().getPublishers()) {
                Thread thread = mFactory.getThread(new Runnable() {

                    @Override
                    public void run() {
                        publisher.publish(mContext, reportData, filePath, CrashPublishingDelegate.this);
                    }

                });

                thread.start();
            }
        } else {
            Thread thread = mFactory.getThread(new Runnable() {

                @Override
                public void run() {
                    mOfflinePublisher.publish(mContext, reportData, filePath);
                }

            });

            thread.start();
        }
    }

    @Override
    public void onBinPackerFailure(@NonNull ReportData reportData, Throwable cause) {
        CrashLogger.e(TAG, "Bin-packing has failed, original exception %s: %s", cause.getClass().getSimpleName(), cause.getMessage());
        cleanPackedData(reportData.getFilesToPack());

        try {
            mCallback.onPublisherResult(0, mPublisherCount);
        } catch (RemoteException ex) {
            CrashLogger.e(TAG, "Unable to contact CrashReporter via AIDL, original exception %s: %s", ex.getClass().getSimpleName(), ex.getMessage());
        }
    }

    @Override
    public void onCrashPublisherSuccess(@NonNull String id, @NonNull ReportData reportData) {
        CrashLogger.d(TAG, "Crash publisher %s finished successfully.", id);
        onCrashPublisherResponse(true, reportData);
    }

    @Override
    public void onCrashPublisherFailure(@NonNull String id, @NonNull ReportData reportData, @Nullable Throwable cause) {
        CrashLogger.d(TAG, "Crash publisher %s finished with a failure: %s.", id, cause != null ? cause.getClass().getSimpleName() + ": " + cause.getMessage() : "null");
        onCrashPublisherResponse(false, reportData);
    }

    @Override
    public void onSuccess() {
        CrashLogger.d(TAG, "Offline publisher successfully finished.");
    }

    @Override
    public void onFailure(@NonNull String message) {
        CrashLogger.e(TAG, "Offline publisher has failed: %s", message);
    }

    private void onCrashPublisherResponse(boolean successful, ReportData reportData) {
        synchronized (mLock) {
            if (successful) {
                mSuccessfulPublisherCounter++;
            } else {
                mUnsuccessfulPublisherCounter++;
            }

            if (mSuccessfulPublisherCounter + mUnsuccessfulPublisherCounter == mPublisherCount) {
                final Strategy strategy = CrashLib.getConfig().getStrategy();

                switch (strategy) {
                    case ALL:
                        if (mPublisherCount == mSuccessfulPublisherCounter) {
                            cleanPublishedData();
                        } else {
                            handleAsOffline(reportData);
                        }

                        break;

                    case ANY:
                        if (mSuccessfulPublisherCounter > 0) {
                            cleanPublishedData();
                        } else {
                            handleAsOffline(reportData);
                        }

                        break;

                    case BEST_EFFORT:
                        cleanPublishedData();
                        break;
                }

                try {
                    mCallback.onPublisherResult(mSuccessfulPublisherCounter, mUnsuccessfulPublisherCounter);
                } catch (RemoteException ex) {
                    CrashLogger.e(TAG, "Unable to contact CrashReporter via AIDL, original exception %s: %s", ex.getClass().getSimpleName(), ex.getMessage());
                }
            }
        }
    }

    private void packReports(ReportData reportData) {
        // converts list to array in order to use it as varargs argument
        String[] files = new String[reportData.getFilesToPack().size()];
        reportData.getFilesToPack().toArray(files);

        try {
            IBinPacker binPacker = mFactory.getBinPacker(mContext.getFilesDir().getAbsolutePath());
            String filePath = binPacker.pack(reportData.getCrashCode(), files);
            onBinPackerSuccess(reportData, filePath);
        } catch (IBinPacker.BinPackerException ex) {
            CrashLogger.e(TAG, "Unable to bin-pack the files, original exception %s: %s", ex.getClass().getSimpleName(), ex.getMessage());
            onBinPackerFailure(reportData, ex);
        }
    }

    private void cleanPublishedData() {
        File file = mFactory.openFile(mReportFilePath);

        if (file.delete()) {
            CrashLogger.d(TAG, "Bin-packed file %s successfully removed.", mReportFilePath);
        } else {
            CrashLogger.e(TAG, "Bin-packed file %s cannot be removed.", mReportFilePath);
        }
    }

    private void cleanPackedData(List<String> filePaths) {
        for (String filePath : filePaths) {
            File file = mFactory.openFile(filePath);

            if (file.delete()) {
                CrashLogger.d(TAG, "Packed file %s successfully deleted.", file.getAbsolutePath());
            } else {
                CrashLogger.e(TAG, "Unable to delete packed file %s.", file.getAbsolutePath());
            }
        }
    }

    private void handleAsOffline(final ReportData reportData) {
        Thread thread = mFactory.getThread(new Runnable() {
            @Override
            public void run() {
                mOfflinePublisher.publish(mContext, reportData, mReportFilePath);
            }
        });

        thread.start();
    }

}
