package com.eveningoutpost.dexdrip.Models;

import android.provider.BaseColumns;
import android.util.Log;

import com.activeandroid.Model;
import com.activeandroid.annotation.Column;
import com.activeandroid.annotation.Table;
import com.activeandroid.query.Delete;
import com.activeandroid.query.Select;
import com.activeandroid.util.SQLiteUtils;
import com.eveningoutpost.dexdrip.AddCalibration;
import com.eveningoutpost.dexdrip.GlucoseMeter.GlucoseReadingRx;
import com.eveningoutpost.dexdrip.Home;
import com.eveningoutpost.dexdrip.Services.SyncService;
import com.eveningoutpost.dexdrip.UtilityModels.BgGraphBuilder;
import com.eveningoutpost.dexdrip.UtilityModels.Constants;
import com.eveningoutpost.dexdrip.UtilityModels.PersistentStore;
import com.eveningoutpost.dexdrip.UtilityModels.Pref;
import com.eveningoutpost.dexdrip.UtilityModels.UploaderQueue;
import com.eveningoutpost.dexdrip.calibrations.CalibrationAbstract;
import com.eveningoutpost.dexdrip.calibrations.NativeCalibrationPipe;
import com.eveningoutpost.dexdrip.calibrations.PluggableCalibration;
import com.eveningoutpost.dexdrip.messages.BloodTestMessage;
import com.eveningoutpost.dexdrip.messages.BloodTestMultiMessage;
import com.eveningoutpost.dexdrip.xdrip;
import com.google.common.math.DoubleMath;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.annotations.Expose;
import com.squareup.wire.Wire;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;

/**
 * Created by jamorham on 11/12/2016.
 */

@Table(name = "BloodTest", id = BaseColumns._ID)
public class BloodTest extends Model {

    public static final long STATE_VALID = 1 << 0;
    public static final long STATE_CALIBRATION = 1 << 1;
    public static final long STATE_NOTE = 1 << 2;
    public static final long STATE_UNDONE = 1 << 3;
    public static final long STATE_OVERWRITTEN = 1 << 4;

    private static long highest_timestamp = 0;
    private static boolean patched = false;
    private final static String TAG = "BloodTest";
    private final static String LAST_BT_AUTO_CALIB_UUID = "last-bt-auto-calib-uuid";
    private final static boolean d = false;

    @Expose
    @Column(name = "timestamp", unique = true, onUniqueConflicts = Column.ConflictAction.IGNORE)
    public long timestamp;

    @Expose
    @Column(name = "mgdl")
    public double mgdl;

    @Expose
    @Column(name = "created_timestamp")
    public long created_timestamp;

    @Expose
    @Column(name = "state")
    public long state; // bitfield

    @Expose
    @Column(name = "source")
    public String source;

    @Expose
    @Column(name = "uuid", unique = true, onUniqueConflicts = Column.ConflictAction.IGNORE)
    public String uuid;


    public GlucoseReadingRx glucoseReadingRx;

    // patches and saves
    public Long saveit() {
        fixUpTable();
        return save();
    }

    public void addState(long flag) {
        state |= flag;
        save();
    }

    public void removeState(long flag) {
        state &= ~flag;
        save();
    }

    public String toS() {
        final Gson gson = new GsonBuilder()
                .excludeFieldsWithoutExposeAnnotation()
                .create();
        return gson.toJson(this);
    }

    private BloodTestMessage toMessageNative() {
        return new BloodTestMessage.Builder()
                .timestamp(timestamp)
                .mgdl(mgdl)
                .created_timestamp(created_timestamp)
                .state(state)
                .source(source)
                .uuid(uuid)
                .build();
    }

    public byte[] toMessage() {
        final List<BloodTest> btl = new ArrayList<>();
        btl.add(this);
        return toMultiMessage(btl);
    }


    // static methods
    private static final long CLOSEST_READING_MS = 30000; // 30 seconds

    public static BloodTest create(long timestamp_ms, double mgdl, String source) {
        return create(timestamp_ms, mgdl, source, null);
    }

    public static BloodTest create(long timestamp_ms, double mgdl, String source, String suggested_uuid) {

        if ((timestamp_ms == 0) || (mgdl == 0)) {
            UserError.Log.e(TAG, "Either timestamp or mgdl is zero - cannot create reading");
            return null;
        }

        if (timestamp_ms < 1487759433000L) {
            UserError.Log.d(TAG, "Timestamp really too far in the past @ " + timestamp_ms);
            return null;
        }

        final long now = JoH.tsl();
        if (timestamp_ms > now) {
            if ((timestamp_ms - now) > 600000) {
                UserError.Log.wtf(TAG, "Timestamp is > 10 minutes in the future! Something is wrong: " + JoH.dateTimeText(timestamp_ms));
                return null;
            }
            timestamp_ms = now; // force to now if it showed up to 10 mins in the future
        }

        final BloodTest match = getForPreciseTimestamp(timestamp_ms, CLOSEST_READING_MS);
        if (match == null) {
            final BloodTest bt = new BloodTest();
            bt.timestamp = timestamp_ms;
            bt.mgdl = mgdl;
            bt.uuid = suggested_uuid == null ? UUID.randomUUID().toString() : suggested_uuid;
            bt.created_timestamp = JoH.tsl();
            bt.state = STATE_VALID;
            bt.source = source;
            bt.saveit();
            if (UploaderQueue.newEntry("insert", bt) != null) {
                SyncService.startSyncService(3000); // sync in 3 seconds
            }

            if (Pref.getBooleanDefaultFalse("bluetooth_meter_for_calibrations_auto")) {
                if ((JoH.msSince(bt.timestamp) < Constants.MINUTE_IN_MS * 5) && (JoH.msSince(bt.timestamp) > 0)) {
                    UserError.Log.d(TAG, "Blood test value recent enough to send to G5");
                    //Ob1G5StateMachine.addCalibration((int) bt.mgdl, timestamp_ms);
                    NativeCalibrationPipe.addCalibration((int) bt.mgdl, timestamp_ms);
                }
            }

            return bt;
        } else {
            UserError.Log.d(TAG, "Not creating new reading as timestamp is too close");
        }
        return null;
    }

    public static BloodTest createFromCal(double bg, double timeoffset, String source) {
        return createFromCal(bg, timeoffset, source, null);
    }

    public static BloodTest createFromCal(double bg, double timeoffset, String source, String suggested_uuid) {
        final String unit = Pref.getString("units", "mgdl");

        if (unit.compareTo("mgdl") != 0) {
            bg = bg * Constants.MMOLL_TO_MGDL;
        }

        if ((bg < 40) || (bg > 400)) {
            Log.wtf(TAG, "Invalid out of range bloodtest glucose mg/dl value of: " + bg);
            JoH.static_toast_long("Bloodtest out of range: " + bg + " mg/dl");
            return null;
        }

        return create((long) (new Date().getTime() - timeoffset), bg, source, suggested_uuid);
    }

    public static void pushBloodTestSyncToWatch(BloodTest bt, boolean is_new) {
        Log.d(TAG, "pushTreatmentSyncToWatch Add treatment to UploaderQueue.");
        if (Pref.getBooleanDefaultFalse("wear_sync")) {
            if (UploaderQueue.newEntryForWatch(is_new ? "insert" : "update", bt) != null) {
                SyncService.startSyncService(3000); // sync in 3 seconds
            }
        }
    }

    public static BloodTest last() {
        final List<BloodTest> btl = last(1);
        if ((btl != null) && (btl.size() > 0)) {
            return btl.get(0);
        } else {
            return null;
        }
    }

    public static List<BloodTest> last(int num) {
        try {
            return new Select()
                    .from(BloodTest.class)
                    .orderBy("timestamp desc")
                    .limit(num)
                    .execute();
        } catch (android.database.sqlite.SQLiteException e) {
            fixUpTable();
            return null;
        }
    }

    public static List<BloodTest> lastMatching(int num, String match) {
        try {
            return new Select()
                    .from(BloodTest.class)
                    .where("source like ?", match)
                    .orderBy("timestamp desc")
                    .limit(num)
                    .execute();
        } catch (android.database.sqlite.SQLiteException e) {
            fixUpTable();
            return null;
        }
    }

    public static BloodTest lastValid() {
        final List<BloodTest> btl = lastValid(1);
        if ((btl != null) && (btl.size() > 0)) {
            return btl.get(0);
        } else {
            return null;
        }
    }

    public static List<BloodTest> lastValid(int num) {
        try {
            return new Select()
                    .from(BloodTest.class)
                    .where("state & ? != 0", BloodTest.STATE_VALID)
                    .orderBy("timestamp desc")
                    .limit(num)
                    .execute();
        } catch (android.database.sqlite.SQLiteException e) {
            fixUpTable();
            return null;
        }
    }


    public static BloodTest byUUID(String uuid) {
        if (uuid == null) return null;
        try {
            return new Select()
                    .from(BloodTest.class)
                    .where("uuid = ?", uuid)
                    .executeSingle();
        } catch (android.database.sqlite.SQLiteException e) {
            fixUpTable();
            return null;
        }
    }

    public static BloodTest byid(long id) {
        try {
            return new Select()
                    .from(BloodTest.class)
                    .where("_ID = ?", id)
                    .executeSingle();
        } catch (android.database.sqlite.SQLiteException e) {
            fixUpTable();
            return null;
        }
    }

    public static byte[] toMultiMessage(List<BloodTest> btl) {
        if (btl == null) return null;
        final List<BloodTestMessage> BloodTestMessageList = new ArrayList<>();
        for (BloodTest bt : btl) {
            BloodTestMessageList.add(bt.toMessageNative());
        }
        return BloodTestMultiMessage.ADAPTER.encode(new BloodTestMultiMessage(BloodTestMessageList));
    }

    private static void processFromMessage(BloodTestMessage btm) {
        if ((btm != null) && (btm.uuid != null) && (btm.uuid.length() == 36)) {
            boolean is_new = false;
            BloodTest bt = byUUID(btm.uuid);
            if (bt == null) {
                bt = getForPreciseTimestamp(Wire.get(btm.timestamp, BloodTestMessage.DEFAULT_TIMESTAMP), CLOSEST_READING_MS);
                if (bt != null) {
                    UserError.Log.wtf(TAG, "Error matches a different uuid with the same timestamp: " + bt.uuid + " vs " + btm.uuid + " skipping!");
                    return;
                }
                bt = new BloodTest();
                is_new = true;
            } else {
                if (bt.state != Wire.get(btm.state, BloodTestMessage.DEFAULT_STATE)) {
                    is_new = true;
                }
            }
            bt.timestamp = Wire.get(btm.timestamp, BloodTestMessage.DEFAULT_TIMESTAMP);
            bt.mgdl = Wire.get(btm.mgdl, BloodTestMessage.DEFAULT_MGDL);
            bt.created_timestamp = Wire.get(btm.created_timestamp, BloodTestMessage.DEFAULT_CREATED_TIMESTAMP);
            bt.state = Wire.get(btm.state, BloodTestMessage.DEFAULT_STATE);
            bt.source = Wire.get(btm.source, BloodTestMessage.DEFAULT_SOURCE);
            bt.uuid = btm.uuid;
            bt.saveit(); // de-dupe by uuid
            if (is_new) { // cannot handle updates yet
                if (UploaderQueue.newEntry(is_new ? "insert" : "update", bt) != null) {
                    if (JoH.quietratelimit("start-sync-service", 5)) {
                        SyncService.startSyncService(3000); // sync in 3 seconds
                    }
                }
            }
        } else {
            UserError.Log.wtf(TAG, "processFromMessage uuid is null or invalid");
        }
    }

    public static void processFromMultiMessage(byte[] payload) {
        try {
            final BloodTestMultiMessage btmm = BloodTestMultiMessage.ADAPTER.decode(payload);
            if ((btmm != null) && (btmm.bloodtest_message != null)) {
                for (BloodTestMessage btm : btmm.bloodtest_message) {
                    processFromMessage(btm);
                }
                Home.staticRefreshBGCharts();
            }
        } catch (IOException | NullPointerException | IllegalStateException e) {
            UserError.Log.e(TAG, "exception processFromMessage: " + e);
        }
    }

    public static BloodTest fromJSON(String json) {
        if ((json == null) || (json.length() == 0)) {
            UserError.Log.d(TAG, "Empty json received in bloodtest fromJson");
            return null;
        }
        try {
            UserError.Log.d(TAG, "Processing incoming json: " + json);
            return new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create().fromJson(json, BloodTest.class);
        } catch (Exception e) {
            UserError.Log.d(TAG, "Got exception parsing bloodtest json: " + e.toString());
            Home.toaststaticnext("Error on Bloodtest sync, probably decryption key mismatch");
            return null;
        }
    }

    public static BloodTest getForPreciseTimestamp(long timestamp, long precision) {
        BloodTest bloodTest = new Select()
                .from(BloodTest.class)
                .where("timestamp <= ?", (timestamp + precision))
                .where("timestamp >= ?", (timestamp - precision))
                .orderBy("abs(timestamp - " + timestamp + ") asc")
                .executeSingle();
        if ((bloodTest != null) && (Math.abs(bloodTest.timestamp - timestamp) < precision)) {
            return bloodTest;
        }
        return null;
    }

    public static List<BloodTest> latestForGraph(int number, double startTime) {
        return latestForGraph(number, (long) startTime, Long.MAX_VALUE);
    }

    public static List<BloodTest> latestForGraph(int number, long startTime) {
        return latestForGraph(number, startTime, Long.MAX_VALUE);
    }

    public static List<BloodTest> latestForGraph(int number, long startTime, long endTime) {
        try {
            return new Select()
                    .from(BloodTest.class)
                    .where("state & ? != 0", BloodTest.STATE_VALID)
                    .where("timestamp >= " + Math.max(startTime, 0))
                    .where("timestamp <= " + endTime)
                    .orderBy("timestamp asc") // warn asc!
                    .limit(number)
                    .execute();
        } catch (android.database.sqlite.SQLiteException e) {
            fixUpTable();
            return new ArrayList<>();
        }
    }

    synchronized static void opportunisticCalibration() {
        if (Pref.getBooleanDefaultFalse("bluetooth_meter_for_calibrations_auto")) {
            final BloodTest bt = lastValid();
            if (bt == null) {
                Log.d(TAG, "opportunistic: No blood tests");
                return;
            }
            if (JoH.msSince(bt.timestamp) > (Constants.HOUR_IN_MS * 8)) {
                Log.d(TAG, "opportunistic: Blood test older than 8 hours ago");
                return;
            }

            if ((bt.uuid == null) || (bt.uuid.length() < 8)) {
                Log.d(TAG, "opportunisitic: invalid uuid");
                return;
            }

            if ((bt.uuid != null) && (bt.uuid.length() > 1) && PersistentStore.getString(LAST_BT_AUTO_CALIB_UUID).equals(bt.uuid)) {
                Log.d(TAG, "opportunistic: Already processed uuid: " + bt.uuid);
                return;
            }

            final Calibration calibration = Calibration.lastValid();
            if (calibration == null) {
                Log.d(TAG, "opportunistic: No calibrations");
                // TODO do we try to initial calibrate using this?
                return;
            }

            if (JoH.msSince(calibration.timestamp) < Constants.HOUR_IN_MS) {
                Log.d(TAG, "opportunistic: Last calibration less than 1 hour ago");
                return;
            }

            if (bt.timestamp <= calibration.timestamp) {
                Log.d(TAG, "opportunistic: Blood test isn't more recent than last calibration");
                return;
            }

            // get closest bgreading - must be within dexcom period and locked to sensor
            final BgReading bgReading = BgReading.getForPreciseTimestamp(bt.timestamp + (AddCalibration.estimatedInterstitialLagSeconds * 1000), BgGraphBuilder.DEXCOM_PERIOD);
            if (bgReading == null) {
                Log.d(TAG, "opportunistic: No matching bg reading");
                return;
            }

            if (bt.timestamp > highest_timestamp) {
                Accuracy.create(bt, bgReading, "xDrip Original");
                final CalibrationAbstract plugin = PluggableCalibration.getCalibrationPluginFromPreferences();
                final CalibrationAbstract.CalibrationData cd = (plugin != null) ? plugin.getCalibrationData(bgReading.timestamp) : null;
                if (plugin != null) {
                    BgReading pluginBgReading = plugin.getBgReadingFromBgReading(bgReading, cd);
                    Accuracy.create(bt, pluginBgReading, plugin.getAlgorithmName());
                }
                highest_timestamp = bt.timestamp;
            }

            if (!CalibrationRequest.isSlopeFlatEnough(bgReading)) {
                Log.d(TAG, "opportunistic: Slope is not flat enough at: " + JoH.dateTimeText(bgReading.timestamp));
                return;
            }

            // TODO store evaluation failure for this record in cache for future optimization

            // TODO Check we have prior reading as well perhaps
            JoH.clearCache();
            UserError.Log.ueh(TAG, "Opportunistic calibration for Blood Test at " + JoH.dateTimeText(bt.timestamp) + " of " + BgGraphBuilder.unitized_string_with_units_static(bt.mgdl) + " matching sensor slope at: " + JoH.dateTimeText(bgReading.timestamp) + " from source " + bt.source);
            final long time_since = JoH.msSince(bt.timestamp);


            Log.d(TAG, "opportunistic: attempting auto calibration");
            PersistentStore.setString(LAST_BT_AUTO_CALIB_UUID, bt.uuid);
            Home.startHomeWithExtra(xdrip.getAppContext(),
                    Home.BLUETOOTH_METER_CALIBRATION,
                    BgGraphBuilder.unitized_string_static(bt.mgdl),
                    Long.toString(time_since),
                    "auto");
        }
    }

    public static String evaluateAccuracy(long period) {

        // CACHE??

        final List<BloodTest> bloodTests = latestForGraph(1000, JoH.tsl() - period, JoH.tsl() - AddCalibration.estimatedInterstitialLagSeconds);
        final List<Double> difference = new ArrayList<>();
        final List<Double> plugin_difference = new ArrayList<>();
        if ((bloodTests == null) || (bloodTests.size() == 0)) return null;

        final boolean show_plugin = true;
        final CalibrationAbstract plugin = (show_plugin) ? PluggableCalibration.getCalibrationPluginFromPreferences() : null;


        for (BloodTest bt : bloodTests) {
            final BgReading bgReading = BgReading.getForPreciseTimestamp(bt.timestamp + (AddCalibration.estimatedInterstitialLagSeconds * 1000), BgGraphBuilder.DEXCOM_PERIOD);

            if (bgReading != null) {
                final Calibration calibration = bgReading.calibration;
                if (calibration == null) {
                    Log.d(TAG, "Calibration for bgReading is null! @ " + JoH.dateTimeText(bgReading.timestamp));
                    continue;
                }
                final double diff = Math.abs(bgReading.calculated_value - bt.mgdl);
                difference.add(diff);
                if (d) {
                    Log.d(TAG, "Evaluate Accuracy: difference: " + JoH.qs(diff));
                }
                final CalibrationAbstract.CalibrationData cd = (plugin != null) ? plugin.getCalibrationData(bgReading.timestamp) : null;
                if ((plugin != null) && (cd != null)) {
                    final double plugin_diff = Math.abs(bt.mgdl - plugin.getGlucoseFromBgReading(bgReading, cd));
                    plugin_difference.add(plugin_diff);
                    if (d)
                        Log.d(TAG, "Evaluate Plugin Accuracy: " + BgGraphBuilder.unitized_string_with_units_static(bt.mgdl) + " @ " + JoH.dateTimeText(bt.timestamp) + "  difference: " + JoH.qs(plugin_diff) + "/" + JoH.qs(plugin_diff * Constants.MGDL_TO_MMOLL, 2) + " calibration: " + JoH.qs(cd.slope, 2) + " " + JoH.qs(cd.intercept, 2));
                }
            }
        }

        if (difference.size() == 0) return null;
        double avg = DoubleMath.mean(difference);
        Log.d(TAG, "Average accuracy: " + accuracyAsString(avg) + "  (" + JoH.qs(avg, 5) + ")");

        if (plugin_difference.size() > 0) {
            double plugin_avg = DoubleMath.mean(plugin_difference);
            Log.d(TAG, "Plugin Average accuracy: " + accuracyAsString(plugin_avg) + "  (" + JoH.qs(plugin_avg, 5) + ")");
            return accuracyAsString(plugin_avg) + " / " + accuracyAsString(avg);
        }
        return accuracyAsString(avg);
    }

    public static String accuracyAsString(double avg) {
        final boolean domgdl = Pref.getString("units", "mgdl").equals("mgdl");
        // +- symbol
        return "\u00B1" + (!domgdl ? JoH.qs(avg * Constants.MGDL_TO_MMOLL, 2) + " mmol" : JoH.qs(avg, 1) + " mgdl");
    }

    public static List<BloodTest> cleanup(int retention_days) {
        return new Delete()
                .from(BloodTest.class)
                .where("timestamp < ?", JoH.tsl() - (retention_days * Constants.DAY_IN_MS))
                .execute();
    }

    // create the table ourselves without worrying about model versioning and downgrading
    private static void fixUpTable() {
        if (patched) return;
        final String[] patchup = {
                "CREATE TABLE BloodTest (_id INTEGER PRIMARY KEY AUTOINCREMENT);",
                "ALTER TABLE BloodTest ADD COLUMN timestamp INTEGER;",
                "ALTER TABLE BloodTest ADD COLUMN created_timestamp INTEGER;",
                "ALTER TABLE BloodTest ADD COLUMN state INTEGER;",
                "ALTER TABLE BloodTest ADD COLUMN mgdl REAL;",
                "ALTER TABLE BloodTest ADD COLUMN source TEXT;",
                "ALTER TABLE BloodTest ADD COLUMN uuid TEXT;",
                "CREATE UNIQUE INDEX index_Bloodtest_uuid on BloodTest(uuid);",
                "CREATE UNIQUE INDEX index_Bloodtest_timestamp on BloodTest(timestamp);",
                "CREATE INDEX index_Bloodtest_created_timestamp on BloodTest(created_timestamp);",
                "CREATE INDEX index_Bloodtest_state on BloodTest(state);"};

        for (String patch : patchup) {
            try {
                SQLiteUtils.execSql(patch);
                //  UserError.Log.e(TAG, "Processed patch should not have succeeded!!: " + patch);
            } catch (Exception e) {
                //  UserError.Log.d(TAG, "Patch: " + patch + " generated exception as it should: " + e.toString());
            }
        }
        patched = true;
    }
}