package com.avg.zaap.crashlibrary.offline.uploader;

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

import com.avg.zaap.crashlibrary.CrashLib;
import com.avg.zaap.crashlibrary.ICrashPublishingDelegate;
import com.avg.zaap.crashlibrary.ICrashServiceCallback;
import com.avg.zaap.crashlibrary.ReportData;
import com.avg.zaap.crashlibrary.core.logger.CrashLogger;
import com.avg.zaap.crashlibrary.offline.Strategy;
import com.avg.zaap.crashlibrary.offline.publisher.IOfflinePublisher;
import com.avg.zaap.crashlibrary.offline.publisher.metadata.ICrashMetadataReader;

import java.io.File;
import java.io.FilenameFilter;

/**
 * This class serves as the default offline uploader.
 * <p/>
 * Created by marek.osvald on 27.7.2016.
 */
public class OfflineUploader implements IOfflineUploader, ICrashServiceCallback {

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

    private Context mContext;
    private boolean mStarted = false;
    private FilenameFilter mFilenameFilter;
    private ICrashMetadataReader mCrashMetadataReader;
    private IOfflineUploaderFactory mFactory;
    private ICrashPublishingDelegate mDelegate;
    private IDelayHelper mDelayHelper;
    private final Object mLock = new Object();

    private String[] mCrashReports;
    private int mIndex;

    /**
     * Parametric constructor. Sets Android context, uses default dependency factory.
     *
     * @param context Android context instance
     */
    public OfflineUploader(Context context) {
        this(context, new OfflineUploaderFactory());
    }

    /**
     * Parametric constructor. Sets Android context and dependency factory to be used.
     *
     * @param context Android context instance
     * @param factory dependency factory to be used
     */
    public OfflineUploader(Context context, IOfflineUploaderFactory factory) {
        mContext = context;
        mFilenameFilter = factory.getCrashReportFilenameFilter();
        mCrashMetadataReader = factory.getCrashMetadataReader();
        mDelayHelper = factory.getDelayHelper();
        mFactory = factory;
    }

    @Override
    public void startUploader() {
        CrashLogger.d(TAG, "startUploader() called");

        synchronized (mLock) {
            if (!mStarted) {
                mStarted = true;

                // resets crash report array and the report index
                mDelegate = mFactory.getCrashPublishingDelegate(mContext);
                mCrashReports = listOfflineCrashReports();
                mIndex = 0;

                Thread backgroundThread = mFactory.getThread(new Runnable() {

                    @Override
                    public void run() {
                        processNextCrashReport();
                    }

                });

                backgroundThread.start();
            }
        }
    }

    @Override
    public void stopUploader() {
        CrashLogger.d(TAG, "stopUploader() called");

        synchronized (mLock) {
            mStarted = false;

            // resets the index
            mIndex = 0;
        }
    }

    private void processNextCrashReport() {
        CrashLogger.d(TAG, "Processing crash reports");

        if (mCrashReports != null && mCrashReports.length != 0) {
            if (mIndex > mCrashReports.length - 1) {
                CrashLogger.d(TAG, "End of reports have been reached. Stopping.");

                synchronized (mLock) {
                    mStarted = false;
                }

                return;
            }

            CrashLogger.d(TAG, "Processing crashReport number: %d", mIndex);

            String crashReport = mCrashReports[mIndex];
            File crashReportDirectory = mFactory.openFile(mContext.getFilesDir(), IOfflinePublisher.OFFLINE_REPORT_DIRECTORY_NAME);
            File crashReportFile = mFactory.openFile(crashReportDirectory, crashReport);

            processCrashReport(crashReportFile);
        } else {
            CrashLogger.d(TAG, "No reports to process.");
            stopUploader();
        }
    }

    private void processCrashReport(File crashReportFile) {
        final ReportData reportData = mCrashMetadataReader.readMetadata(crashReportFile);

        synchronized (mLock) {
            if (!mStarted) {
                CrashLogger.d(TAG, "Processing has been stopped.");
                return;
            }
        }

        if (reportData == null) {
            CrashLogger.e(TAG, "Report data for %s missing! Unable to report crash!", crashReportFile.getAbsolutePath());

            if (crashReportFile.delete()) {
                CrashLogger.d(TAG, "Crash report at %s successfully deleted", crashReportFile.getAbsolutePath());
            } else {
                CrashLogger.e(TAG, "Unable to remove crash report at %s", crashReportFile.getAbsolutePath());
            }

            // increase the index, which equals to skipping
            mIndex++;
            return;
        }

        // check whether the delay is valid
        if (mDelayHelper.isAttemptValid(reportData.getLastAttemptTimestamp())) {
            CrashLogger.d(TAG, "Delay has been met, trying to publish offline crash.");
            mDelegate.publishOfflineCrash(crashReportFile.getAbsolutePath(), reportData, this);
        } else {
            CrashLogger.d(TAG, "Delay not met, skipping report.");
            mIndex++;
            processNextCrashReport();
        }
    }

    @Nullable
    private String[] listOfflineCrashReports() {
        File offlineReportDirectory = mFactory.openFile(mContext.getFilesDir(), IOfflinePublisher.OFFLINE_REPORT_DIRECTORY_NAME);

        if (!offlineReportDirectory.exists()) {
            CrashLogger.e(TAG, "Crash directory does not exist.");
            return null;
        }

        if (offlineReportDirectory.isFile()) {
            CrashLogger.e(TAG, "Crash directory file path contains a file instead of a directory.");
            return null;
        }

        return offlineReportDirectory.list(mFilenameFilter);
    }

    @Override
    public void onPublisherResult(int successful, int unsuccessful) throws RemoteException {
        CrashLogger.d(TAG, "Successfully published data to %d publishers out of %d", successful, CrashLib.getConfig().getPublishers().size());

        final Strategy strategy = CrashLib.getConfig().getStrategy();

        switch (strategy) {
            case ALL:
                if (successful == CrashLib.getConfig().getPublishers().size()) {
                    onReportSuccessful(strategy);
                } else {
                    onReportUnsuccessful(strategy);
                }

                break;

            case ANY:
                if (successful > 0) {
                    onReportSuccessful(strategy);
                } else {
                    onReportUnsuccessful(strategy);
                }

                break;

            case BEST_EFFORT:
                onReportSuccessful(strategy);
                break;
        }
    }

    @Override
    public IBinder asBinder() {
        throw new UnsupportedOperationException("asBinder() not supported");
    }

    private void onReportSuccessful(Strategy strategy) {
        CrashLogger.d(TAG, "Report successfully reported according to the %s strategy.", strategy);
        mIndex++;
        processNextCrashReport();
    }

    private void onReportUnsuccessful(Strategy strategy) {
        CrashLogger.d(TAG, "Report handled unsuccessfully according to the %s strategy. Stopping uploader.", strategy);
        stopUploader();
    }

}
