package info.nightscout.android.medtronic.service;

import android.annotation.TargetApi;
import android.app.AlarmManager;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbManager;
import android.os.BatteryManager;
import android.os.Build;
import android.os.IBinder;
import android.os.PowerManager;
import android.support.annotation.NonNull;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;
import android.util.Log;

import java.util.Date;

import info.nightscout.android.history.PumpHistoryHandler;
import info.nightscout.android.medtronic.Stats;
import info.nightscout.android.model.medtronicNg.PumpHistorySystem;
import info.nightscout.android.model.store.StatCnl;
import info.nightscout.android.pushover.PushoverUploadService;
import info.nightscout.android.R;
import info.nightscout.android.USB.UsbHidDriver;
import info.nightscout.android.UploaderApplication;
import info.nightscout.android.medtronic.MainActivity;
import info.nightscout.android.medtronic.StatusNotification;
import info.nightscout.android.model.medtronicNg.PumpStatusEvent;
import info.nightscout.android.model.store.DataStore;
import info.nightscout.android.medtronic.UserLogMessage;
import info.nightscout.android.upload.nightscout.NightscoutUploadService;
import info.nightscout.android.urchin.UrchinService;
import info.nightscout.android.utils.FormatKit;
import info.nightscout.android.utils.RealmKit;
import info.nightscout.android.xdrip_plus.XDripPlusUploadService;
import io.realm.Realm;
import io.realm.RealmResults;
import io.realm.Sort;

import static android.support.v4.app.NotificationCompat.PRIORITY_MAX;
import static android.support.v4.app.NotificationCompat.VISIBILITY_PUBLIC;
import static info.nightscout.android.medtronic.service.MedtronicCnlService.POLL_GRACE_PERIOD_MS;
import static info.nightscout.android.medtronic.service.MedtronicCnlService.POLL_PERIOD_MS;
import static info.nightscout.android.medtronic.service.MedtronicCnlService.POLL_PRE_GRACE_PERIOD_MS;
import static info.nightscout.android.medtronic.service.MedtronicCnlService.POLL_RECOVERY_PERIOD_MS;
import static info.nightscout.android.medtronic.service.MedtronicCnlService.POLL_WARMUP_PERIOD_MS;
import static info.nightscout.android.medtronic.service.MedtronicCnlService.USB_WARMUP_TIME_MS;
import static info.nightscout.android.utils.ToolKit.getWakeLock;
import static info.nightscout.android.utils.ToolKit.releaseWakeLock;

/**
 * Created by Pogman on 13.9.17.
 */

public class MasterService extends Service {
    private static final String TAG = MasterService.class.getSimpleName();

    public static final int USB_DISCONNECT_NOFICATION_ID = 1;
    public static final int SERVICE_NOTIFICATION_ID = 2;
    private static final int ALARM_ID = 102;

    public static final long HEARTBEAT_PERIOD_MS = 5 * 60000L;
    public static final long CNL_JITTER_MS = 5 * 60000L;

    private Context mContext;

    private MasterServiceReceiver masterServiceReceiver;

    private StatusNotification statusNotification;

    private static PendingIntent pendingIntent;
    private static AlarmManager alarmManager;

    private boolean serviceStart;
    private boolean serviceActive;

    private final static int uploaderBatteryLow = 15;
    private final static int uploaderBatteryVeryLow = 5;
    private static int uploaderBatteryLevel = 100;

    @Override
    public IBinder onBind(Intent intent) {
        throw new UnsupportedOperationException("Not yet implemented");
    }

    @Override
    public void onCreate() {
        super.onCreate();
        Log.i(TAG, "onCreate called");

        mContext = this.getBaseContext();

        final Realm storeRealm = Realm.getInstance(UploaderApplication.getStoreConfiguration());
        final DataStore dataStore = storeRealm.where(DataStore.class).findFirst();

        // safety check: don't proceed until main ui has been run and datastore initialised
        if (dataStore != null) {

            storeRealm.executeTransaction(new Realm.Transaction() {
                @Override
                public void execute(@NonNull Realm realm) {
                    dataStore.clearAllCommsErrors();
                    // check Xdrip available
                    dataStore.setXdripPlusUploadAvailable(false);
                    // check Nightscout site available
                    dataStore.setNightscoutReportTime(0);
                    dataStore.setNightscoutAvailable(false);
                    // revalidate Pushover account
                    dataStore.setPushoverAPItokenCheck("");
                    dataStore.setPushoverUSERtokenCheck("");
                }
            });

            masterServiceReceiver = new MasterServiceReceiver();
            IntentFilter intentFilter = new IntentFilter();
            intentFilter.addAction(Constants.ACTION_CNL_COMMS_ACTIVE);
            intentFilter.addAction(Constants.ACTION_CNL_COMMS_FINISHED);
            intentFilter.addAction(Constants.ACTION_CNL_COMMS_READY);
            intentFilter.addAction(Constants.ACTION_STOP_SERVICE);
            intentFilter.addAction(Constants.ACTION_READ_NOW);
            intentFilter.addAction(Constants.ACTION_READ_PROFILE);
            intentFilter.addAction(Constants.ACTION_READ_OVERDUE);
            intentFilter.addAction(Constants.ACTION_SETTINGS_CHANGED);
            intentFilter.addAction(Constants.ACTION_URCHIN_UPDATE);
            intentFilter.addAction(Constants.ACTION_STATUS_UPDATE);
            intentFilter.addAction(Constants.ACTION_HEARTBEAT);
            intentFilter.addAction(Constants.ACTION_USERLOG_MESSAGE);

            intentFilter.addAction(Intent.ACTION_BATTERY_LOW);
            intentFilter.addAction(Intent.ACTION_BATTERY_CHANGED);
            intentFilter.addAction(Intent.ACTION_BATTERY_OKAY);
            intentFilter.addAction(Intent.ACTION_POWER_CONNECTED);
            intentFilter.addAction(Intent.ACTION_POWER_DISCONNECTED);

            intentFilter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
            intentFilter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
            intentFilter.addAction(Constants.ACTION_USB_ACTIVITY);
            intentFilter.addAction(Constants.ACTION_USB_PERMISSION);
            intentFilter.addAction(Constants.ACTION_NO_USB_PERMISSION);

            intentFilter.addAction(Intent.ACTION_LOCALE_CHANGED);
            intentFilter.addAction(Intent.ACTION_TIME_CHANGED);
            intentFilter.addAction(Intent.ACTION_DATE_CHANGED);

            registerReceiver(masterServiceReceiver, intentFilter);

            statusNotification = new StatusNotification();

            serviceStart = true;

        } else {

            serviceStart = false;
        }

        storeRealm.close();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.d(TAG, "onDestroy called");

        cancelAlarm();

        stopService(new Intent(mContext, UrchinService.class));

        if (statusNotification != null) statusNotification.endNotification();
        if (masterServiceReceiver != null) unregisterReceiver(masterServiceReceiver);
    }

    @Override
    public void onTaskRemoved(Intent intent) {
        Log.w(TAG, "onTaskRemoved called");
    }

    @Override
    public void onLowMemory() {
        Log.w(TAG, "onLowMemory called");
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.d(TAG, "Received start id " + startId + ": " + intent);
        //userLogMessage.add(TAG + " Received start id " + startId + ": " + (intent == null ? "null" : intent));

        if (!serviceStart) {
            stopSelf();
            return START_NOT_STICKY;

        } else if (intent == null || startId == 1) {
            Log.i(TAG, "service start");
            //userLogMessage.add(TAG + " service start");

            Intent notificationIntent = new Intent(this, MainActivity.class);
            PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);

            String channel;
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                createNotificationChannels();
                channel = "status";
            } else channel = "";

            Notification notification = new NotificationCompat.Builder(this, channel)
                    .setContentTitle("600 Series Uploader")
                    .setSmallIcon(R.drawable.ic_notification)
                    .setVisibility(VISIBILITY_PUBLIC)
                    .setContentIntent(pendingIntent)
                    .build();

            startForeground(SERVICE_NOTIFICATION_ID, notification);

            statusNotification.initNotification(mContext);

            serviceActive = true;
            setHeartbeatAlarm();

            // Android OS can kill and restart processes at any time
            // ask the CNL service to check state as it may already be active
            startService(new Intent(mContext, MedtronicCnlService.class)
                    .setAction(MasterService.Constants.ACTION_CNL_CHECKSTATE));
        }

        return START_STICKY;
    }

    @TargetApi(26)
    private synchronized void createNotificationChannels() {

        NotificationManager notificationManager = (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE);
        if (notificationManager == null) {
            Log.e(TAG, "Could not create notification channels. NotificationManager is null");
            stopSelf();
        }

        NotificationChannel statusChannel = new NotificationChannel("status", "Status", NotificationManager.IMPORTANCE_LOW);
        statusChannel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
        notificationManager.createNotificationChannel(statusChannel);

        NotificationChannel errorChannel = new NotificationChannel("error", "Errors", NotificationManager.IMPORTANCE_HIGH);
        errorChannel.enableLights(true);
        errorChannel.setLightColor(Color.RED);
        errorChannel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
        notificationManager.createNotificationChannel(errorChannel);
    }

    private class MasterServiceReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (!action.equals(Constants.ACTION_USERLOG_MESSAGE))
                Log.d(TAG, "receiver: " + action);

            switch (action) {

                case Constants.ACTION_CNL_COMMS_ACTIVE:
                    setHeartbeatAlarm();
                    statusNotification.updateNotification(StatusNotification.NOTIFICATION.BUSY);
                    break;

                case Constants.ACTION_CNL_COMMS_FINISHED:
                    if (serviceActive) {
                        PowerManager.WakeLock wl = getWakeLock(context, TAG, 10000);

                        runUploadServices();

                        long nextpoll = intent.getLongExtra("nextpoll", 0);

                        if (nextpoll > 0) {
                            setPollingAlarm(nextpoll);
                            statusNotification.updateNotification(StatusNotification.NOTIFICATION.NORMAL, nextpoll);
                        } else {
                            statusNotification.updateNotification(StatusNotification.NOTIFICATION.ERROR);
                        }

                        releaseWakeLock(wl);

                    } else {
                        Log.d(TAG, "onReceive : stopping master service");
                        stopSelf();
                    }
                    break;

                case Constants.ACTION_CNL_COMMS_READY:
                    RealmKit.compact(mContext);
                    statusNotification.updateNotification(StatusNotification.NOTIFICATION.NORMAL);
                    setHeartbeatAlarm();
                    runUploadServices();
                    if (checkUsbDevice()) {
                        if (hasUsbPermission()) startCgmServiceDelayed(USB_WARMUP_TIME_MS);
                        else usbNoPermission();
                    }
                    break;

                case Constants.ACTION_HEARTBEAT:
                    setHeartbeatAlarm();
                    runUploadServices();
                    break;

                case Constants.ACTION_STOP_SERVICE:
                    cancelAlarm();
                    serviceActive = false;
                    stopSelf();
                    break;

                case Constants.ACTION_READ_NOW:
                    UserLogMessage.send(mContext, R.string.ul_main__requesting_poll_now);
                    setPollingAlarm(System.currentTimeMillis() + 1000L);
                    break;

                case Constants.ACTION_READ_PROFILE:
                    UserLogMessage.send(mContext, R.string.ul_main__requesting_pump_profile);
                    Realm storeRealm = Realm.getInstance(UploaderApplication.getStoreConfiguration());
                    storeRealm.executeTransaction(new Realm.Transaction() {
                        @Override
                        public void execute(@NonNull Realm realm) {
                            realm.where(DataStore.class).findFirst().setRequestProfile(true);
                        }
                    });
                    storeRealm.close();
                    break;

                case Constants.ACTION_READ_OVERDUE:
                    if (isUsbOperational()
                            && System.currentTimeMillis() - lastPollSuccess() > POLL_PERIOD_MS) {
                        setPollingAlarm(System.currentTimeMillis() + USB_WARMUP_TIME_MS);
                    }
                    break;

                case Constants.ACTION_SETTINGS_CHANGED:
                    runUploadServices();
                    break;

                case Constants.ACTION_URCHIN_UPDATE:
                    startService(new Intent(mContext, UrchinService.class).setAction("update"));
                    break;

                case Intent.ACTION_LOCALE_CHANGED:
                case Intent.ACTION_TIME_CHANGED:
                case Intent.ACTION_DATE_CHANGED:
                case Constants.ACTION_STATUS_UPDATE:
                    statusNotification.updateNotification();
                    break;

                case Constants.ACTION_USERLOG_MESSAGE:
                    UserLogMessage.getInstance().addAsync(
                            (UserLogMessage.TYPE) intent.getSerializableExtra("type"),
                            (UserLogMessage.FLAG) intent.getSerializableExtra("flag"),
                            intent.getStringExtra("message"));
                    break;

                // received from OS
                case Intent.ACTION_BATTERY_LOW:
                case Intent.ACTION_BATTERY_CHANGED:
                case Intent.ACTION_BATTERY_OKAY:
                    updateUploaderBattery(intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1));
                    break;
                case Intent.ACTION_POWER_CONNECTED:
                    new PumpHistoryHandler(mContext).systemEvent()
                            .dismiss(PumpHistorySystem.STATUS.UPLOADER_BATTERY_VERYLOW)
                            .dismiss(PumpHistorySystem.STATUS.UPLOADER_BATTERY_LOW)
                            .closeHandler();
                    break;
                case Intent.ACTION_POWER_DISCONNECTED:
                    new PumpHistoryHandler(mContext).systemEvent()
                            .dismiss(PumpHistorySystem.STATUS.UPLOADER_BATTERY_FULL)
                            .closeHandler();
                    break;

                // received from UsbActivity
                case Constants.ACTION_USB_ACTIVITY:
                    usbActivity();
                    break;

                // received from OS
                case Constants.ACTION_USB_PERMISSION:
                    usbPermission();
                    break;

                // received from OS
                case UsbManager.ACTION_USB_DEVICE_ATTACHED:
                    usbAttached();
                    break;

                // received from OS
                case UsbManager.ACTION_USB_DEVICE_DETACHED:
                    usbDetached();
                    break;

                // received from CnlService
                case Constants.ACTION_NO_USB_PERMISSION:
                    usbNoPermission();
                    break;
            }

        }
    }

    private void setPollingAlarm(long time) {
        Intent intent = new Intent(this, MedtronicCnlService.class).setAction(Constants.ACTION_CNL_READPUMP);
        PendingIntent pi = PendingIntent.getService(this, ALARM_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT);
        setAlarm(time, pi);
    }

    private void setHeartbeatAlarm() {
        Intent intent = new Intent(Constants.ACTION_HEARTBEAT);
        PendingIntent pi = PendingIntent.getBroadcast(this, ALARM_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT);
        setAlarm(System.currentTimeMillis() + HEARTBEAT_PERIOD_MS, pi);
    }

    private void setAlarm(long time, PendingIntent pi) {
        Log.d(TAG, "request to set Alarm at " + new Date(time));

        if (alarmManager == null)
            alarmManager = (AlarmManager) this.getSystemService(Context.ALARM_SERVICE);

        if (pendingIntent != null)
            cancelAlarm();

        pendingIntent = pi;

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            alarmManager.setAlarmClock(new AlarmManager.AlarmClockInfo(time, null), pendingIntent);
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            // Android 5.0.0 + 5.0.1 (e.g. Galaxy S4) has a bug.
            // Alarms are not exact. Fixed in 5.0.2 and CM12
            alarmManager.setExact(AlarmManager.RTC_WAKEUP, time, pendingIntent);
        } else {
            alarmManager.set(AlarmManager.RTC_WAKEUP, time, pendingIntent);
        }
    }

    private void cancelAlarm() {
        if (alarmManager == null || pendingIntent == null)
            return;

        alarmManager.cancel(pendingIntent);
    }

    private void startCgmService() {
        startCgmServiceDelayed(1000L);
    }

    private void startCgmServiceDelayed(long delay) {
        long now = System.currentTimeMillis();
        long start = nextPollTime();

        if (start - now < delay) start = now + delay;
        setPollingAlarm(start);
        statusNotification.updateNotification(StatusNotification.NOTIFICATION.NORMAL, start);

        if (start - now > 10000L)
            UserLogMessage.send(mContext, UserLogMessage.TYPE.INFO, String.format("{id;%s} {time.poll;%s}", R.string.ul_poll__next_poll_due_at, start));
    }

    private long lastPollSuccess() {
        long last = 0;
        Realm realm = Realm.getDefaultInstance();

        RealmResults<PumpStatusEvent> results = realm.where(PumpStatusEvent.class)
                .greaterThan("eventDate", new Date(System.currentTimeMillis() - (24 * 60 * 60000L)))
                .sort("eventDate", Sort.DESCENDING)
                .findAll();

        if (results.size() > 0)
            last = results.first().getEventDate().getTime();

        realm.close();
        return last;
    }

    private long nextPollTime() {

        long now = System.currentTimeMillis();

        long lastRecievedEventTime;
        long lastActualEventTime;
        long nextExpectedEventTime;
        long nextRequestedPollTime;

        final Realm realm = Realm.getDefaultInstance();
        final Realm storeRealm = Realm.getInstance(UploaderApplication.getStoreConfiguration());
        final DataStore dataStore = storeRealm.where(DataStore.class).findFirst();

        RealmResults<PumpStatusEvent> results = realm.where(PumpStatusEvent.class)
                .greaterThan("eventDate", new Date(now - (24 * 60 * 60000L)))
                .sort("eventDate", Sort.DESCENDING)
                .findAll();

        RealmResults<PumpStatusEvent> cgmresults = results.where()
                .equalTo("cgmActive", true)
                .sort("cgmDate", Sort.DESCENDING)
                .findAll();

        long pollOffset = dataStore.isSysEnablePollOverride() ? dataStore.getSysPollGracePeriod() : POLL_GRACE_PERIOD_MS;

        if (cgmresults.size() > 0) {

            lastRecievedEventTime = cgmresults.first().getCgmDate().getTime();

            // normalise last received cgm time to current time window
            lastActualEventTime = lastRecievedEventTime + (((now - lastRecievedEventTime) / POLL_PERIOD_MS) * POLL_PERIOD_MS);

            // check if pump has lost sensor
            if (now - lastRecievedEventTime <= 60 * 60000L
                    && cgmresults.first().getCgmRTC() != results.first().getCgmRTC())
                pollOffset = dataStore.isSysEnablePollOverride() ? dataStore.getSysPollRecoveryPeriod() : POLL_RECOVERY_PERIOD_MS;

                // check if sensor is in warmup phase
            else if (now - lastRecievedEventTime <= 120 * 60000L
                    && cgmresults.first().isCgmWarmUp())
                pollOffset = dataStore.isSysEnablePollOverride() ? dataStore.getSysPollWarmupPeriod() : POLL_WARMUP_PERIOD_MS;

            nextExpectedEventTime = lastActualEventTime + POLL_PERIOD_MS;

            if (nextExpectedEventTime - lastRecievedEventTime > POLL_PERIOD_MS
                    && now < nextExpectedEventTime - POLL_PRE_GRACE_PERIOD_MS)
                nextRequestedPollTime = now;
            else
                nextRequestedPollTime = lastActualEventTime + POLL_PERIOD_MS + pollOffset;

        } else {

            // no cgm event available to sync with
            nextRequestedPollTime = now;
        }

        storeRealm.close();
        realm.close();

        return nextRequestedPollTime;
    }

    private void runUploadServices() {
        Log.d(TAG, "Running upload services");
        startService(new Intent(mContext, UrchinService.class).setAction("update"));
        startService(new Intent(mContext, PushoverUploadService.class));
        startService(new Intent(mContext, XDripPlusUploadService.class));
        startService(new Intent(mContext, NightscoutUploadService.class));
    }

    public static int getUploaderBatteryLevel() {
        return uploaderBatteryLevel;
    }

    private int batteryUpdateLastLevel = 100;
    private long batteryUpdateTimestamp;

    private void updateUploaderBattery(int level) {
        if (level < 0) return;
        uploaderBatteryLevel = level;

        long now = System.currentTimeMillis();

        // skip checks if previous level 100 (initial startup value)

        if (batteryUpdateTimestamp < now && batteryUpdateLastLevel != 100) {

                if ((100 - level) / (100 - uploaderBatteryVeryLow)
                        > (100 - batteryUpdateLastLevel) / (100 - uploaderBatteryVeryLow)) {

                    new PumpHistoryHandler(this).systemEvent(PumpHistorySystem.STATUS.UPLOADER_BATTERY_VERYLOW)
                            .dismiss(PumpHistorySystem.STATUS.UPLOADER_BATTERY_VERYLOW)
                            .dismiss(PumpHistorySystem.STATUS.UPLOADER_BATTERY_LOW)
                            .data(level)
                            .process()
                            .closeHandler();
                    batteryUpdateTimestamp = now + 5 * 60000L;

                } else if ((100 - level) / (100 - uploaderBatteryLow)
                        > (100 - batteryUpdateLastLevel) / (100 - uploaderBatteryLow)) {

                    new PumpHistoryHandler(this).systemEvent(PumpHistorySystem.STATUS.UPLOADER_BATTERY_LOW)
                            .dismiss(PumpHistorySystem.STATUS.UPLOADER_BATTERY_VERYLOW)
                            .dismiss(PumpHistorySystem.STATUS.UPLOADER_BATTERY_LOW)
                            .data(level)
                            .process()
                            .closeHandler();
                    batteryUpdateTimestamp = now + 5 * 60000L;

                } else if (level == 100 && level > batteryUpdateLastLevel) {

                    new PumpHistoryHandler(this).systemEvent(PumpHistorySystem.STATUS.UPLOADER_BATTERY_FULL)
                            .dismiss(PumpHistorySystem.STATUS.UPLOADER_BATTERY_FULL)
                            .data(level)
                            .process()
                            .closeHandler();
                    batteryUpdateTimestamp = now + 60 * 60000L;

                }
            }

        batteryUpdateLastLevel = level;
    }

    private void usbActivity() {
        clearDisconnectionNotification();
        if (hasUsbPermission()) {
            cnlReady();
            UserLogMessage.send(mContext, UserLogMessage.TYPE.INFO, R.string.ul_usb__got_permission_for_usb);
            statusNotification.updateNotification(StatusNotification.NOTIFICATION.NORMAL);
            startCgmServiceDelayed(MedtronicCnlService.USB_WARMUP_TIME_MS);
        } else {
            usbNoPermission();
        }
    }

    private void usbPermission() {
        clearDisconnectionNotification();
        if (hasUsbPermission()) {
            cnlReady();
            UserLogMessage.send(mContext, UserLogMessage.TYPE.INFO, R.string.ul_usb__got_permission_for_usb);
            statusNotification.updateNotification(StatusNotification.NOTIFICATION.NORMAL);
            startCgmServiceDelayed(MedtronicCnlService.USB_WARMUP_TIME_MS);
        } else {
            UserLogMessage.send(mContext, UserLogMessage.TYPE.INFO, R.string.ul_usb__user_has_not_granted_permission_for_usb);
        }
    }

    private void cnlReady() {
        Realm storeRealm = Realm.getInstance(UploaderApplication.getStoreConfiguration());
        storeRealm.executeTransaction(new Realm.Transaction() {
            @Override
            public void execute(@NonNull Realm realm) {
                DataStore dataStore = realm.where(DataStore.class).findFirst();
                dataStore.clearAllCommsErrors();

                long plug = System.currentTimeMillis();
                dataStore.setCnlPlugTimestamp(plug);
                long unplug = dataStore.getCnlUnplugTimestamp();
                if (unplug == 0) unplug = plug;
                if (plug - unplug > dataStore.getPushoverBackfillLimiter() * 60000L)
                    dataStore.setCnlLimiterTimestamp(plug);
            }
        });
        storeRealm.close();

        StatCnl statCnl = ((StatCnl) Stats.open().readRecord(StatCnl.class));
        statCnl.connected();
        UserLogMessage.sendE(mContext, UserLogMessage.TYPE.NOTE, statCnl.toString());
        Stats.close();
    }

    private void usbAttached() {
        clearDisconnectionNotification();
        UserLogMessage.send(mContext, UserLogMessage.TYPE.INFO, R.string.ul_usb__contour_next_link_plugged_in);
        if (!hasUsbPermission()) {
            Log.d(TAG, "No permission for USB. Waiting.");
            UserLogMessage.send(mContext, UserLogMessage.TYPE.INFO, R.string.ul_usb__waiting_for_usb_permission);
        }
    }

    private void usbDetached() {
        showDisconnectionNotification(FormatKit.getInstance().getString(R.string.ul_usb__usb_error),
                FormatKit.getInstance().getString(R.string.ul_usb__contour_next_link_unplugged));
        UserLogMessage.send(mContext, UserLogMessage.TYPE.WARN, String.format("{id;%s}. {id;%s}",
                R.string.ul_usb__usb_error,
                R.string.ul_usb__contour_next_link_unplugged));

        statusNotification.updateNotification(StatusNotification.NOTIFICATION.ERROR);

        ((StatCnl) Stats.open().readRecord(StatCnl.class)).disconnected();
        Stats.close();

        Realm storeRealm = Realm.getInstance(UploaderApplication.getStoreConfiguration());
        storeRealm.executeTransaction(new Realm.Transaction() {
            @Override
            public void execute(@NonNull Realm realm) {
                DataStore dataStore = realm.where(DataStore.class).findFirst();
                long now = System.currentTimeMillis();
                // allow for jitter, only change timestamp when >period since last plug
                if (dataStore.getCnlUnplugTimestamp() == 0
                        || now - dataStore.getCnlPlugTimestamp() > CNL_JITTER_MS)
                    dataStore.setCnlUnplugTimestamp(now);
            }
        });
        storeRealm.close();
    }

    private void usbNoPermission() {
        Log.w(TAG, "No permission to read the USB device.");
        UserLogMessage.send(mContext, UserLogMessage.TYPE.WARN, R.string.ul_usb__no_permission_to_read_the_usb_device);

        statusNotification.updateNotification(StatusNotification.NOTIFICATION.ERROR);

        Realm storeRealm = Realm.getInstance(UploaderApplication.getStoreConfiguration());
        DataStore dataStore = storeRealm.where(DataStore.class).findFirst();
        if (dataStore.isSysEnableUsbPermissionDialog()) {
            requestUsbPermission();
        } else {
            UserLogMessage.send(mContext, UserLogMessage.TYPE.HELP, R.string.ul_usb__help_permission);
        }
        storeRealm.close();
    }

    private boolean hasUsbPermission() {
        UsbManager usbManager = (UsbManager) this.getSystemService(Context.USB_SERVICE);
        if (usbManager == null) return false;
        UsbDevice cnlDevice = UsbHidDriver.getUsbDevice(usbManager, MedtronicCnlService.USB_VID, MedtronicCnlService.USB_PID);
        return !(cnlDevice != null && !usbManager.hasPermission(cnlDevice));
    }

    private void requestUsbPermission() {
        Log.d(TAG, "requestUsbPermission");
        UsbManager usbManager = (UsbManager) this.getSystemService(Context.USB_SERVICE);
        UsbDevice cnlDevice = UsbHidDriver.getUsbDevice(usbManager, MedtronicCnlService.USB_VID, MedtronicCnlService.USB_PID);
        PendingIntent permissionIntent = PendingIntent.getBroadcast(this, 0, new Intent(Constants.ACTION_USB_PERMISSION), 0);
        usbManager.requestPermission(cnlDevice, permissionIntent);
    }

    private boolean checkUsbDevice() {
        if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_USB_HOST)) {
            Log.e(TAG, "Device does not support USB OTG");
            statusNotification.updateNotification(StatusNotification.NOTIFICATION.ERROR);
            UserLogMessage.send(mContext, UserLogMessage.TYPE.WARN, R.string.ul_usb__no_support);
            return false;
        }

        UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);

        if (usbManager == null) {
            Log.e(TAG, "USB connection error. mUsbManager == null");
            statusNotification.updateNotification(StatusNotification.NOTIFICATION.ERROR);
            UserLogMessage.send(mContext, UserLogMessage.TYPE.WARN, R.string.ul_usb__no_connection);
            return false;
        }

        if (UsbHidDriver.getUsbDevice(usbManager, MedtronicCnlService.USB_VID, MedtronicCnlService.USB_PID) == null) {
            Log.w(TAG, "USB connection error. Is the CNL plugged in?");
            statusNotification.updateNotification(StatusNotification.NOTIFICATION.ERROR);
            UserLogMessage.send(mContext, UserLogMessage.TYPE.WARN, R.string.ul_usb__no_connection);
            return false;
        }

        return true;
    }

    private boolean isUsbOperational() {
        if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_USB_HOST)) {
            return false;
        }

        UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);

        if (usbManager == null) {
            return false;
        }

        if (UsbHidDriver.getUsbDevice(usbManager, MedtronicCnlService.USB_VID, MedtronicCnlService.USB_PID) == null) {
            return false;
        }

        return true;
    }

    private void showDisconnectionNotification(String title, String message) {

        String channel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? "error" : "";

        NotificationCompat.Builder mBuilder =
                new NotificationCompat.Builder(this, channel)
                        .setPriority(PRIORITY_MAX)
                        .setSmallIcon(android.R.drawable.stat_notify_error)
                        .setContentTitle(title)
                        .setContentText(message)
                        .setTicker(message)
                        .setVibrate(new long[]{1000, 1000, 1000, 1000, 1000});

        // Creates an explicit intent for an Activity in your app
        Intent resultIntent = new Intent(this, MainActivity.class);

        TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);

        // Adds the back stack for the Intent (but not the Intent itself)
        stackBuilder.addParentStack(MainActivity.class);

        // Adds the Intent that starts the Activity to the top of the stack
        stackBuilder.addNextIntent(resultIntent);
        PendingIntent resultPendingIntent =
                stackBuilder.getPendingIntent(
                        0,
                        PendingIntent.FLAG_UPDATE_CURRENT
                );

        mBuilder.setContentIntent(resultPendingIntent);

        NotificationManager mNotificationManager =
                (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        mNotificationManager.notify(USB_DISCONNECT_NOFICATION_ID, mBuilder.build());
    }

    private void clearDisconnectionNotification() {
        NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        notificationManager.cancel(USB_DISCONNECT_NOFICATION_ID);
    }

    public final class Constants {
        public static final String ACTION_USERLOG_MESSAGE = "info.nightscout.android.medtronic.USERLOG_MESSAGE";
        public static final String ACTION_CNL_COMMS_ACTIVE = "info.nightscout.android.medtronic.CNL_COMMS_ACTIVE";
        public static final String ACTION_CNL_COMMS_FINISHED = "info.nightscout.android.medtronic.CNL_COMMS_FINISHED";
        public static final String ACTION_CNL_COMMS_READY = "info.nightscout.android.medtronic.CNL_COMMS_READY";
        public static final String ACTION_CNL_READPUMP = "info.nightscout.android.medtronic.CNL_READPUMP";
        public static final String ACTION_CNL_SHUTDOWN = "info.nightscout.android.medtronic.CNL_SHUTDOWN";
        public static final String ACTION_CNL_CHECKSTATE = "info.nightscout.android.medtronic.CNL_STATE";
        public static final String ACTION_STOP_SERVICE = "info.nightscout.android.medtronic.STOP_SERVICE";
        public static final String ACTION_READ_NOW = "info.nightscout.android.medtronic.READ_NOW";
        public static final String ACTION_READ_PROFILE = "info.nightscout.android.medtronic.READ_PROFILE";
        public static final String ACTION_READ_OVERDUE = "info.nightscout.android.medtronic.READ_OVERDUE";
        public static final String ACTION_SETTINGS_CHANGED = "info.nightscout.android.medtronic.SETTINGS_CHANGED";
        public static final String ACTION_URCHIN_UPDATE = "info.nightscout.android.medtronic.URCHIN_UPDATE";
        public static final String ACTION_STATUS_UPDATE = "info.nightscout.android.medtronic.STATUS_UPDATE";
        public static final String ACTION_HEARTBEAT = "info.nightscout.android.medtronic.HEARTBEAT";
        public static final String ACTION_NO_USB_PERMISSION = "info.nightscout.android.medtronic.NO_USB_PERMISSION";
        public static final String ACTION_USB_PERMISSION = "info.nightscout.android.medtronic.USB_PERMISSION";
        public static final String ACTION_USB_ACTIVITY = "info.nightscout.android.medtronic.USB_ACTIVITY";
    }
}