package com.fox2code.mmm.repo;

import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.utils.io.net.Http;
import com.fox2code.mmm.utils.realm.ModuleListCache;
import com.fox2code.mmm.utils.realm.ReposList;

import org.json.JSONArray;
import org.json.JSONObject;

import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;

import io.realm.Realm;
import io.realm.RealmConfiguration;
import io.realm.RealmResults;
import timber.log.Timber;

public class RepoUpdater {
    public final RepoData repoData;
    public byte[] indexRaw;
    private List<RepoModule> toUpdate;
    private Collection<RepoModule> toApply;

    public RepoUpdater(RepoData repoData) {
        this.repoData = repoData;
    }

    public int fetchIndex() {
        if (!RepoManager.getINSTANCE().hasConnectivity()) {
            this.indexRaw = null;
            this.toUpdate = Collections.emptyList();
            this.toApply = Collections.emptySet();
            return 0;
        }
        if (!this.repoData.isEnabled()) {
            this.indexRaw = null;
            this.toUpdate = Collections.emptyList();
            this.toApply = Collections.emptySet();
            return 0;
        }
        // if we shouldn't update, get the values from the ModuleListCache realm
        if (!this.repoData.shouldUpdate() && Objects.equals(this.repoData.id, "androidacy_repo")) { // for now, only enable cache reading for androidacy repo, until we handle storing module prop file values in cache
            Timber.d("Fetching index from cache for %s", this.repoData.id);
            File cacheRoot = MainApplication.getINSTANCE().getDataDirWithPath("realms/repos/" + this.repoData.id);
            RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().name("ModuleListCache.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).schemaVersion(1).deleteRealmIfMigrationNeeded().allowWritesOnUiThread(true).allowQueriesOnUiThread(true).directory(cacheRoot).build();
            Realm realm = Realm.getInstance(realmConfiguration);
            RealmResults<ModuleListCache> results = realm.where(ModuleListCache.class).equalTo("repoId", this.repoData.id).findAll();
            // repos-list realm
            RealmConfiguration realmConfiguration2 = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build();
            Realm realm2 = Realm.getInstance(realmConfiguration2);
            this.toUpdate = Collections.emptyList();
            this.toApply = new HashSet<>();
            for (ModuleListCache moduleListCache : results) {
                this.toApply.add(new RepoModule(this.repoData, moduleListCache.getCodename(), moduleListCache.getName(), moduleListCache.getDescription(), moduleListCache.getAuthor(), moduleListCache.getDonate(), moduleListCache.getConfig(), moduleListCache.getSupport(), moduleListCache.getVersion(), moduleListCache.getVersionCode()));
            }
            Timber.d("Fetched %d modules from cache for %s, from %s records", this.toApply.size(), this.repoData.id, results.size());
            // apply the toApply list to the toUpdate list
            try {
                JSONObject jsonObject = new JSONObject();
                jsonObject.put("modules", new JSONArray(results.asJSON()));
                this.toUpdate = this.repoData.populate(jsonObject);
            } catch (Exception e) {
                Timber.e(e);
            }
            // close realm
            realm.close();
            realm2.close();
            // Since we reuse instances this should work
            this.toApply = new HashSet<>(this.repoData.moduleHashMap.values());
            this.toApply.removeAll(this.toUpdate);
            // Return repo to update
            return this.toUpdate.size();
        }
        try {
            if (!this.repoData.prepare()) {
                this.indexRaw = null;
                this.toUpdate = Collections.emptyList();
                this.toApply = this.repoData.moduleHashMap.values();
                return 0;
            }
            this.indexRaw = Http.doHttpGet(this.repoData.getUrl(), false);
            this.toUpdate = this.repoData.populate(new JSONObject(new String(this.indexRaw, StandardCharsets.UTF_8)));
            // Since we reuse instances this should work
            this.toApply = new HashSet<>(this.repoData.moduleHashMap.values());
            this.toApply.removeAll(this.toUpdate);
            // Return repo to update
            return this.toUpdate.size();
        } catch (
                Exception e) {
            Timber.e(e);
            this.indexRaw = null;
            this.toUpdate = Collections.emptyList();
            this.toApply = Collections.emptySet();
            return 0;
        }
    }

    public List<RepoModule> toUpdate() {
        return this.toUpdate;
    }

    public Collection<RepoModule> toApply() {
        return this.toApply;
    }

    public boolean finish() {
        var success = new AtomicBoolean(false);
        // If repo is not enabled we don't need to do anything, just return true
        if (!this.repoData.isEnabled()) {
            return true;
        }
        if (this.indexRaw != null) {
            try {
                // iterate over modules, using this.supportedProperties as a template to attempt to get each property from the module. everything that is not null is added to the module
                // use realm to insert to
                // props avail:
                File cacheRoot = MainApplication.getINSTANCE().getDataDirWithPath("realms/repos/" + this.repoData.id);
                RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().name("ModuleListCache.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).schemaVersion(1).deleteRealmIfMigrationNeeded().allowWritesOnUiThread(true).allowQueriesOnUiThread(true).directory(cacheRoot).build();
                // array with module info default values
                // supported properties for a module
                //id=<string>
                //name=<string>
                //version=<string>
                //versionCode=<int>
                //author=<string>
                //description=<string>
                //minApi=<int>
                //maxApi=<int>
                //minMagisk=<int>
                //needRamdisk=<boolean>
                //support=<url>
                //donate=<url>
                //config=<package>
                //changeBoot=<boolean>
                //mmtReborn=<boolean>
                // extra properties only useful for the database
                //repoId=<string>
                //installed=<boolean>
                //installedVersionCode=<int> (only if installed)
                //
                // all except first six can be null
                // this.indexRaw is the raw index file (json)
                JSONObject modules = new JSONObject(new String(this.indexRaw, StandardCharsets.UTF_8));
                JSONArray modulesArray;
                // androidacy repo uses "data" key, others should use "modules" key. Both are JSONArrays
                if (this.repoData.getName().equals("Androidacy Modules Repo")) {
                    // get modules from "data" key. This is a JSONArray so we need to convert it to a JSONObject
                    modulesArray = modules.getJSONArray("data");
                } else {
                    // get modules from "modules" key. This is a JSONArray so we need to convert it to a JSONObject
                    modulesArray = modules.getJSONArray("modules");
                }
                Realm realm = Realm.getInstance(realmConfiguration);
                // drop old data
                if (realm.isInTransaction()) {
                    realm.commitTransaction();
                }
                realm.beginTransaction();
                realm.where(ModuleListCache.class).equalTo("repoId", this.repoData.id).findAll().deleteAllFromRealm();
                realm.commitTransaction();
                // iterate over modules. pls don't hate me for this, its ugly but it works
                for (int n = 0; n < modulesArray.length(); n++) {
                    // get module
                    JSONObject module = modulesArray.getJSONObject(n);
                    try {
                        // get module id
                        // if codename is present, prefer that over id
                        String id;
                        if (module.has("codename") && !module.getString("codename").equals("")) {
                            id = module.getString("codename");
                        } else {
                            id = module.getString("id");
                        }
                        // get module name
                        String name = module.getString("name");
                        // get module version
                        String version = module.getString("version");
                        // get module version code
                        int versionCode = module.getInt("versionCode");
                        // get module author
                        String author = module.getString("author");
                        // get module description
                        String description = module.getString("description");
                        // get module min api
                        String minApi;
                        if (module.has("minApi") && !module.getString("minApi").equals("")) {
                            minApi = module.getString("minApi");
                        } else {
                            minApi = "0";
                        }
                        // coerce min api to int
                        int minApiInt = Integer.parseInt(minApi);
                        // get module max api and set to 0 if it's "" or null
                        String maxApi;
                        if (module.has("maxApi") && !module.getString("maxApi").equals("")) {
                            maxApi = module.getString("maxApi");
                        } else {
                            maxApi = "0";
                        }
                        // coerce max api to int
                        int maxApiInt = Integer.parseInt(maxApi);
                        // get module min magisk
                        String minMagisk;
                        if (module.has("minMagisk") && !module.getString("minMagisk").equals("")) {
                            minMagisk = module.getString("minMagisk");
                        } else {
                            minMagisk = "0";
                        }
                        // coerce min magisk to int
                        int minMagiskInt = Integer.parseInt(minMagisk);
                        // get module need ramdisk
                        boolean needRamdisk;
                        if (module.has("needRamdisk")) {
                            needRamdisk = module.getBoolean("needRamdisk");
                        } else {
                            needRamdisk = false;
                        }
                        // get module support
                        String support;
                        if (module.has("support")) {
                            support = module.getString("support");
                        } else {
                            support = "";
                        }
                        // get module donate
                        String donate;
                        if (module.has("donate")) {
                            donate = module.getString("donate");
                        } else {
                            donate = "";
                        }
                        // get module config
                        String config;
                        if (module.has("config")) {
                            config = module.getString("config");
                        } else {
                            config = "";
                        }
                        // get module change boot
                        boolean changeBoot;
                        if (module.has("changeBoot")) {
                            changeBoot = module.getBoolean("changeBoot");
                        } else {
                            changeBoot = false;
                        }
                        // get module mmt reborn
                        boolean mmtReborn;
                        if (module.has("mmtReborn")) {
                            mmtReborn = module.getBoolean("mmtReborn");
                        } else {
                            mmtReborn = false;
                        }
                        // try to get updated_at or lastUpdate value for lastUpdate
                        int lastUpdate;
                        if (module.has("updated_at")) {
                            lastUpdate = module.getInt("updated_at");
                        } else if (module.has("lastUpdate")) {
                            lastUpdate = module.getInt("lastUpdate");
                        } else {
                            lastUpdate = 0;
                        }
                        // now downloads or stars
                        int downloads;
                        if (module.has("downloads")) {
                            downloads = module.getInt("downloads");
                        } else if (module.has("stars")) {
                            downloads = module.getInt("stars");
                        } else {
                            downloads = 0;
                        }
                        // get module repo id
                        String repoId = this.repoData.id;
                        // get module installed
                        boolean installed = false;
                        // get module installed version code
                        int installedVersionCode = 0;
                        // get safe property. for now, only supported by androidacy repo and they use "vt_status" key
                        boolean safe = false;
                        if (this.repoData.getName().equals("Androidacy Modules Repo")) {
                            if (module.has("vt_status")) {
                                if (module.getString("vt_status").equals("Clean")) {
                                    safe = true;
                                }
                            }
                        }
                        // insert module to realm
                        // first create a collection of all the properties
                        // then insert to realm
                        // then commit
                        // then close
                        if (realm.isInTransaction()) {
                            realm.cancelTransaction();
                        }
                        // create a realm object and insert or update it
                        // add everything to the realm object
                        if (realm.isInTransaction()) {
                            realm.commitTransaction();
                        }
                        realm.beginTransaction();
                        ModuleListCache moduleListCache = realm.createObject(ModuleListCache.class, id);
                        moduleListCache.setName(name);
                        moduleListCache.setVersion(version);
                        moduleListCache.setVersionCode(versionCode);
                        moduleListCache.setAuthor(author);
                        moduleListCache.setDescription(description);
                        moduleListCache.setMinApi(minApiInt);
                        moduleListCache.setMaxApi(maxApiInt);
                        moduleListCache.setMinMagisk(minMagiskInt);
                        moduleListCache.setNeedRamdisk(needRamdisk);
                        moduleListCache.setSupport(support);
                        moduleListCache.setDonate(donate);
                        moduleListCache.setConfig(config);
                        moduleListCache.setChangeBoot(changeBoot);
                        moduleListCache.setMmtReborn(mmtReborn);
                        moduleListCache.setRepoId(repoId);
                        moduleListCache.setInstalled(installed);
                        moduleListCache.setInstalledVersionCode(installedVersionCode);
                        moduleListCache.setSafe(safe);
                        moduleListCache.setLastUpdate(lastUpdate);
                        moduleListCache.setStats(downloads);
                        realm.copyToRealmOrUpdate(moduleListCache);
                        realm.commitTransaction();
                    } catch (
                            Exception ignored) {
                    }
                }
                realm.close();
            } catch (
                    Exception ignored) {
            }
            this.indexRaw = null;
            RealmConfiguration realmConfiguration2 = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build();
            Realm realm2 = Realm.getInstance(realmConfiguration2);
            if (realm2.isInTransaction()) {
                realm2.cancelTransaction();
            }
            // set lastUpdate
            realm2.executeTransaction(r -> {
                ReposList repoListCache = r.where(ReposList.class).equalTo("id", this.repoData.id).findFirst();
                if (repoListCache != null) {
                    success.set(true);
                    // get unix timestamp of current time
                    int currentTime = (int) (System.currentTimeMillis() / 1000);
                    Timber.d("Updating lastUpdate for repo %s to %s which is %s seconds ago", this.repoData.id, currentTime, (currentTime - repoListCache.getLastUpdate()));
                    repoListCache.setLastUpdate(currentTime);
                } else {
                    Timber.w("Failed to update lastUpdate for repo %s", this.repoData.id);
                }
            });
            realm2.close();
        } else {
            success.set(true); // assume we're reading from cache. this may be unsafe but it's better than nothing
        }
        return success.get();
    }

}
