package ch.threema.app.utils;

import android.app.job.JobInfo;
import android.app.job.JobScheduler;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.text.format.DateUtils;

import org.jetbrains.annotations.NotNull;
import org.koin.java.KoinJavaComponent;
import org.slf4j.Logger;

import java.util.Map;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.preference.PreferenceManager;
import androidx.work.Constraints;
import androidx.work.Data;
import androidx.work.NetworkType;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkManager;
import androidx.work.WorkRequest;

import ch.threema.app.AppConstants;
import ch.threema.app.R;
import ch.threema.app.ThreemaApplication;
import ch.threema.app.jobs.ReConnectJobService;
import ch.threema.app.managers.ServiceManager;
import ch.threema.app.preference.service.PreferenceService;
import ch.threema.app.push.PushRegistrationWorker;
import ch.threema.app.receivers.AlarmManagerBroadcastReceiver;
import ch.threema.app.services.ConversationCategoryService;
import ch.threema.app.services.LockAppService;
import ch.threema.app.services.NotificationPreferenceService;
import ch.threema.app.services.notification.BadgeUpdater;
import ch.threema.app.services.notification.NotificationService;
import ch.threema.app.services.notification.NotificationServiceImpl;
import ch.threema.app.services.PollingHelper;
import ch.threema.app.services.RingtoneService;
import ch.threema.app.widget.WidgetUpdater;
import ch.threema.common.TimeProvider;
import ch.threema.data.IdentityProvider;
import ch.threema.app.webclient.services.SessionWakeUpService;
import ch.threema.base.ThreemaException;

import static ch.threema.base.utils.LoggingKt.getThreemaLogger;

import ch.threema.data.datatypes.ConversationId;

import static ch.threema.app.di.DIJavaCompat.getMasterKeyManager;

public class PushUtil {
    private static final Logger logger = getThreemaLogger("PushUtil");

    public static final String EXTRA_CLEAR_TOKEN = "clear";
    public static final String EXTRA_WITH_CALLBACK = "cb";
    public static final String EXTRA_REGISTRATION_ERROR_BROADCAST = "rer";

    private static final String WEBCLIENT_SESSION = "wcs";
    private static final String WEBCLIENT_TIMESTAMP = "wct";
    private static final String WEBCLIENT_VERSION = "wcv";
    private static final String WEBCLIENT_AFFILIATION_ID = "wca";

    private static final int RECONNECT_JOB = 89;

    /**
     * Send push token to server
     *
     * @param context      Context
     * @param clear        Remove token from sever
     * @param withCallback Send broadcast after token refresh has been completed or failed
     */
    public static void enqueuePushTokenUpdate(Context context, boolean clear, boolean withCallback) {
        Data workerFlags = new Data.Builder()
            .putBoolean(EXTRA_CLEAR_TOKEN, clear)
            .putBoolean(EXTRA_WITH_CALLBACK, withCallback)
            .build();

        Constraints workerConstraints = new Constraints.Builder()
            .setRequiredNetworkType(NetworkType.CONNECTED)
            .build();

        // worker differs between hms and regular builds, see fcm and hms directory for for overwriting push worker versions
        WorkRequest pushTokenRegistrationRequest = new OneTimeWorkRequest.Builder(PushRegistrationWorker.class)
            .setInputData(workerFlags)
            .setConstraints(workerConstraints)
            .build();

        WorkManager.getInstance(context).enqueue(pushTokenRegistrationRequest);
    }

    /**
     * Send a push token to the server
     *
     * @param token String representing the token
     * @param type  int representing the token type (fcm, hms or none in case of a reset)
     */
    public static void sendTokenToServer(@NonNull String token, int type) throws ThreemaException {
        ServiceManager serviceManager = ServiceManager.get();

        if (serviceManager != null) {
            serviceManager.getTaskCreator().scheduleSendPushTokenTask(token, type);
            logger.info("Sending push token of type 0x{} successfully scheduled", Integer.toHexString(type));
            // Note that the last sent date of the push token is set in the task
        } else {
            throw new ThreemaException("Unable to send / clear push token. ServiceManager not available");
        }
    }

    /**
     * Signal a push token update through a local broadcast
     *
     * @param error      String potential error message
     * @param clearToken boolean whether the token was reset
     */
    public static void signalRegistrationFinished(@Nullable String error, boolean clearToken) {
        final Intent intent = new Intent(AppConstants.INTENT_PUSH_REGISTRATION_COMPLETE);
        if (error != null) {
            logger.error("Failed to get push token {}", error);
            intent.putExtra(PushUtil.EXTRA_REGISTRATION_ERROR_BROADCAST, true);
        } else {
            intent.putExtra(PushUtil.EXTRA_CLEAR_TOKEN, clearToken);
        }
        LocalBroadcastManager.getInstance(ThreemaApplication.getAppContext()).sendBroadcast(intent);
    }

    /**
     * Process the Data mapping received from a FCM message
     *
     * @param data Map<String, String> key value pairs with webclient session infos
     */
    public static void processRemoteMessage(Map<String, String> data) {
        logger.info("processRemoteMessage");

        // Webclient push
        if (data != null && data.containsKey(WEBCLIENT_SESSION) && data.containsKey(WEBCLIENT_TIMESTAMP)) {
            sendWebclientNotification(data);
        } else { // New messages push, trigger a reconnect and show new message notification(s)
            sendNotification();
        }
    }

    private static void sendNotification() {
        logger.info("sendNotification");
        Context appContext = ThreemaApplication.getAppContext();
        PollingHelper pollingHelper = new PollingHelper(appContext, "fcm");

        if (ConfigUtils.isBackgroundDataRestricted(appContext)) {
            logger.warn("Network blocked (background data disabled?)");
            // The same message may arrive twice (due to a network change). so we simply ignore messages that we were unable to fetch due to a blocked network
            // Simply schedule a poll when the device is back online
            JobScheduler js = (JobScheduler) appContext.getSystemService(Context.JOB_SCHEDULER_SERVICE);
            js.cancel(RECONNECT_JOB);

            JobInfo job = new JobInfo.Builder(RECONNECT_JOB,
                new ComponentName(appContext, ReConnectJobService.class))
                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
                .setRequiresCharging(false)
                .build();

            if (js.schedule(job) != JobScheduler.RESULT_SUCCESS) {
                logger.error("Job scheduling failed");
            }
            return;
        }

        //recheck after one minute
        AlarmManagerBroadcastReceiver.requireLoggedInConnection(
            appContext,
            (int) DateUtils.MINUTE_IN_MILLIS
        );

        NotificationPreferenceService notificationPreferenceService = KoinJavaComponent.get(NotificationPreferenceService.class);
        if (getMasterKeyManager().isLocked() && notificationPreferenceService.isMasterKeyNewMessageNotifications()) {
            displayAdHocNotification();
        }

        if (!pollingHelper.poll(true)) {
            logger.warn("Unable to establish connection");
        }
    }

    private static void displayAdHocNotification() {
        logger.info("displayAdHocNotification");
        final Context appContext = ThreemaApplication.getAppContext();
        final ServiceManager serviceManager = ServiceManager.get();

        // TODO(ANDR-4363): Always use real NotificationService here, once it is no longer attached to the session scope
        NotificationService notificationService;
        if (serviceManager != null) {
            notificationService = serviceManager.getNotificationService();
        } else {
            notificationService = new NotificationServiceImpl(
                appContext,
                KoinJavaComponent.get(LockAppService.class),
                new ConversationCategoryService() {

                    @Override
                    public void persistRemovePrivateMark(@NotNull ConversationId conversationId) {
                        // Nothing to do
                    }

                    @Override
                    public void persistAddPrivateMark(@NotNull ConversationId conversationId) {
                        // Nothing to do
                    }

                    @Override
                    public boolean isMarkedAsPrivate(@NotNull ConversationId conversationId) {
                        return false;
                    }

                    @Override
                    public boolean removePrivateMark(@NotNull ConversationId conversationId) {
                        return false;
                    }

                    @Override
                    public boolean setPrivateMark(@NotNull ConversationId conversationId) {
                        return false;
                    }

                    @Override
                    public boolean hasAnyPrivateMarks() {
                        return false;
                    }

                    @Override
                    public void invalidateCache() {
                        // Nothing to do
                    }
                },
                KoinJavaComponent.get(NotificationPreferenceService.class),
                KoinJavaComponent.get(RingtoneService.class),
                KoinJavaComponent.get(PreferenceService.class),
                KoinJavaComponent.get(IdentityProvider.class),
                KoinJavaComponent.get(BadgeUpdater.class),
                KoinJavaComponent.get(DoNotDisturbUtil.class),
                KoinJavaComponent.get(TimeProvider.class),
                KoinJavaComponent.get(WidgetUpdater.class)
            );
        }

        notificationService.showMasterKeyLockedNewMessageNotification();
    }

    private static void sendWebclientNotification(Map<String, String> data) {
        final String session = data.get(WEBCLIENT_SESSION);
        final String timestamp = data.get(WEBCLIENT_TIMESTAMP);
        final String version = data.get(WEBCLIENT_VERSION);
        final String affiliationId = data.get(WEBCLIENT_AFFILIATION_ID);
        if (session != null && !session.isEmpty() && timestamp != null && !timestamp.isEmpty()) {
            logger.debug("Received webclient wakeup for session {}", session);

            final Thread t = new Thread(() -> {
                logger.info("Trying to wake up webclient session {}", session);

                // Parse version number
                Integer versionNumber = null;
                if (version != null) { // Can be null during beta, if an old client doesn't yet send the version field
                    try {
                        versionNumber = Integer.parseInt(version, 10);
                    } catch (NumberFormatException e) {
                        logger.error("Could not parse webclient protocol version number: ", e);
                        return;
                    }
                }

                // Try to wake up session
                SessionWakeUpService sessionWakeUpService = KoinJavaComponent.get(SessionWakeUpService.class);
                sessionWakeUpService.resume(session, versionNumber == null ? 0 : versionNumber, affiliationId);
            });
            t.setName("webclient-wakeup");
            t.start();
        }
    }

    /**
     * Clear the "token last updated" setting in shared preferences
     *
     * @param context Context
     */
    public static void clearPushTokenSentDate(Context context) {
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
        if (sharedPreferences != null) {
            sharedPreferences
                .edit()
                .putLong(context.getString(R.string.preferences__token_sent_date), 0L)
                .apply();
        }
    }

    /**
     * Check if the token needs to be uploaded to the server i.e. no more than once a day.
     *
     * @param context Context
     * @return true if more than a day has passed since the token has been last sent to the server, false otherwise
     */
    public static boolean pushTokenNeedsRefresh(Context context) {
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);

        if (sharedPreferences != null) {
            long lastDate = sharedPreferences.getLong(context.getString(R.string.preferences__token_sent_date), 0L);
            // refresh push token at least once a day
            return (System.currentTimeMillis() - lastDate) > DateUtils.DAY_IN_MILLIS;
        }
        return true;
    }

    /**
     * Check if push services are enabled and polling is not used.
     *
     * @param context Context
     * @return true if polling is disabled or shared preferences are not available, false otherwise
     */
    public static boolean isPushEnabled(Context context) {
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);

        if (sharedPreferences != null) {
            return !sharedPreferences.getBoolean(context.getString(R.string.preferences__threema_push_switch), false);
        }
        return true;
    }
}
