package net.heartshield.data;

import android.content.Context;
import android.os.Build;
import android.os.Environment;

import net.heartshield.prevent.BuildConfig;
import net.heartshield.util.JsonUtil;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Random;

/**
 * Persists information about the app, such as its unique ID and install date.
 * App ID assignments are privacy relevant.
 *
 * Needs the WRITE_EXTERNAL_STORAGE permission.
 *
 * @author David Madl (git@abanbytes.eu)
 * @date 2017-03-20
 */
public class AppInfo implements IAppInfo {
    public static final String VERSION = BuildConfig.VERSION_NAME;
    public static final int VERSION_CODE = BuildConfig.VERSION_CODE;
    public static final String CODENAME = BuildConfig.CODENAME;

    private FileUtil mFileUtil;
    private File mExternalFilesDir;

    private JSONObject mTransientAppInfo;

    private AppInfo(Context context) {
        //mContext = context;
        mExternalFilesDir = context.getExternalFilesDir(null);
        mFileUtil = new FileUtil(mExternalFilesDir);


        loadDevSettings(context);

        try {
            mTransientAppInfo = transientAppInfo(context);
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
    }

    private static AppInfo sInstance = null;
    public static AppInfo getInstance(Context context) {
        if(sInstance == null) {
            sInstance = new AppInfo(context);
        }
        return sInstance;
    }

    public static class DevSettings {
        /** whether to use the test server instead of production server */
        public boolean useTestServer = false;

        /** whether to show mock data for demo */
        //@NoRemote
        public boolean useMockData = false;

        /** whether to skip all dialogs, including EnterAgeActivity, and just use test data */
        public boolean skipDialogs = false;

        /** Show personal tracking controls (sleep, happiness) in EditActivity */
        public boolean showPersonalTracking = false;

        /** Ask for height */
        public boolean showHeightDialog = false;

        public String serialize(JsonUtil.Target target) {
            return JsonUtil.getGson(target).toJson(this);
        }

        public static DevSettings deserialize(String json) {
            return JsonUtil.getGson().fromJson(json, DevSettings.class);
        }
    }

    public DevSettings devSettings;

    private static final String DEV_SETTINGS = "dev_settings.json";
    public void saveDevSettings(Context context) {
        try {
            mFileUtil.writeDataFileContents(DEV_SETTINGS, devSettings.serialize(JsonUtil.Target.REMOTE));
            activateDevSettings(context);
        } catch (IOException e) {
            // for developers only
            throw new RuntimeException(e);
        }
    }
    private void loadDevSettings(Context context) {
        try {
            devSettings = DevSettings.deserialize(mFileUtil.readDataFileString(DEV_SETTINGS));
            activateDevSettings(context);
        } catch (IOException e) {
            // for developers only -- file will never be created for normal users
            devSettings = new DevSettings();
        }
    }
    private void activateDevSettings(Context context) {
        // activate DevSettings
        IMeasurementDataManager mdm = MeasurementDataManager.getInstance(context);
        mdm.setUseTestServer(devSettings.useTestServer);
    }

    /**
     * Get descriptive information about the app. Write these files to persistent storage first, if necessary.
     * Needs the WRITE_EXTERNAL_STORAGE, will throw RuntimeException otherwise.
     */
    public JSONObject getAppInfo() {
        try {
            return JsonUtil.mergeJson(mTransientAppInfo, persistentAppInfo());
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
    }

    private static final String TRANSIENT_APP_INFO = "transient.b";

    private boolean firstInstall = true;

    public boolean isFirstInstall() { return firstInstall; }

    //public boolean isActivated() { return true; }

    public boolean isActivated() {
        JSONObject pai = null;
        try {
            pai = persistentAppInfo();
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
        return pai.has("activation_date");
    }

    /**
     * app info that is deleted between reinstalls or updates.
     */
    private JSONObject transientAppInfo(Context context) throws JSONException {
        JSONObject tai;

        try {
            FileInputStream is = context.openFileInput(TRANSIENT_APP_INFO);
            tai = new JSONObject(FileUtil.readFileString(is));
            firstInstall = false;
        } catch (IOException e) {
            tai = null;
        }

        // not sure if updates will remove the Internal Storage files, so check for VERSION changes
        if(tai != null && tai.getString("version").equals(VERSION) && tai.getString("codename").equals(CODENAME))
            return tai;

        firstInstall = true;

        // not existent, or old VERSION, write it now
        tai = new JSONObject();

        // note: new keys must also be added in {@link AppMeta} to end up on the server - see getAppMeta()
        tai.put("version", VERSION);
        tai.put("version_code", VERSION_CODE);
        tai.put("codename", CODENAME);
        tai.put("git_hash", BuildConfig.GIT_HASH);
        tai.put("build_type", BuildConfig.BUILD_TYPE);
        tai.put("install_date", (long) (System.currentTimeMillis() / 1e3));
        tai.put("install_android_versions", getAndroidVersions());

        try {
            FileOutputStream os = context.openFileOutput(TRANSIENT_APP_INFO, Context.MODE_PRIVATE);
            os.write(tai.toString().getBytes());
            os.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        return tai;
    }

    /**
     * app info that is persisted between reinstalls and updates.
     */
    private JSONObject persistentAppInfo() throws JSONException {
        JSONObject pai;

        if(!isExternalStorageReadable())
            throw new RuntimeException("cannot read persistent app info (external storage is unreadable)");

        try {
            File info = new File(mExternalFilesDir.getAbsolutePath() + FileUtil.PERSISTENT_APP_INFO_DIR, FileUtil.PERSISTENT_APP_INFO);
            pai = new JSONObject(FileUtil.readFileString(new FileInputStream(info)));
        } catch (IOException e) {
            pai = null;
        }

        if(pai != null)
            return pai;

        // not existent, write it now

        mFileUtil.makeDirectories(FileUtil.PERSISTENT_APP_INFO_DIR);

        pai = new JSONObject();

        pai.put("id", "");
        pai.put("first_install_date", (long) (System.currentTimeMillis() / 1e3));

        try {
            File info = new File(mExternalFilesDir.getAbsolutePath() + FileUtil.PERSISTENT_APP_INFO_DIR, FileUtil.PERSISTENT_APP_INFO);
            FileOutputStream os = new FileOutputStream(info);
            os.write(pai.toString().getBytes());
            os.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        return pai;
    }

    public void activate(String activationCode) {
        // not existent, write it now

        mFileUtil.makeDirectories(FileUtil.PERSISTENT_APP_INFO_DIR);

        JSONObject pai = new JSONObject();

        try {
            pai.put("id", activationCode.toUpperCase());
            pai.put("first_install_date", persistentAppInfo().getLong("first_install_date"));
            pai.put("activation_date", (long) (System.currentTimeMillis() / 1e3));
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }

        try {
            File info = new File(mExternalFilesDir.getAbsolutePath() + FileUtil.PERSISTENT_APP_INFO_DIR, FileUtil.PERSISTENT_APP_INFO);
            FileOutputStream os = new FileOutputStream(info);
            os.write(pai.toString().getBytes());
            os.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /* Checks if external storage is available to at least read */
    private boolean isExternalStorageReadable() {
        String state = Environment.getExternalStorageState();
        if(Environment.MEDIA_MOUNTED.equals(state) ||
                Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
            return true;
        }
        return false;
    }

    @Override
    public AppMeta getAppMeta() {
        JSONObject appInfo = getAppInfo();
        return AppMeta.deserialize(appInfo.toString());
    }

    private JSONObject getAndroidVersions() throws JSONException {
        JSONObject ver = new JSONObject();
        ver.put("os.version", System.getProperty("os.version"));
        ver.put("Build.VERSION.INCREMENTAL", Build.VERSION.INCREMENTAL);
        ver.put("Build.VERSION.SDK_INT", Build.VERSION.SDK_INT); // API level
        ver.put("Build.DEVICE", Build.DEVICE);
        ver.put("Build.MODEL", Build.MODEL);
        ver.put("Build.PRODUCT", Build.PRODUCT);
        return ver;
    }

    private String getRandomHexString(int n) {
        Random r = new Random();
        StringBuilder sb = new StringBuilder();
        while(sb.length() < n)
            sb.append(Integer.toHexString(r.nextInt()));
        return sb.toString().toUpperCase().substring(0, n);
    }
}
