package de.sebse.fuplanner.services.kvv;

import android.content.Context;

import org.jetbrains.annotations.Nullable;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.regex.MatchResult;
import java.util.regex.Matcher;

import androidx.arch.core.util.Function;
import de.sebse.fuplanner.services.kvv.types.Lecturer;
import de.sebse.fuplanner.services.kvv.types.Modules;
import de.sebse.fuplanner.services.kvv.types.Semester;
import de.sebse.fuplanner.tools.NewAsyncQueue;
import de.sebse.fuplanner.tools.Regex;
import de.sebse.fuplanner.tools.network.HTTPService;
import de.sebse.fuplanner.tools.network.NetworkCallback;
import de.sebse.fuplanner.tools.network.NetworkError;
import de.sebse.fuplanner.tools.network.NetworkErrorCallback;

import static de.sebse.fuplanner.services.kvv.PartModules.RETRY_COUNT;

public class ModulesList extends HTTPService {
    private final Login mLogin;
    private final KVVListener mListener;
    @Nullable private Modules mModules;
    private ModulesListLecturer mLecturer;
    private final NewAsyncQueue mQueue = new NewAsyncQueue();

    ModulesList(Login login, KVVListener listener, Context context) {
        super(context);
        this.mLogin = login;
        this.mListener = listener;
        restore();
    }

    @Nullable
    public String getUsername() {
        if (mModules != null) {
            return mModules.getUsername();
        }
        return null;
    }

    public void reloadIfOutdated() {
        try {
            if (mModules != null && mModules.isNewerVersionInStorage(getContext())) {
                restore();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void find(String moduleID, NetworkCallback<Modules.Module> moduleNetworkCallback, NetworkErrorCallback errorCallback) {
        find(moduleID, moduleNetworkCallback, errorCallback, RETRY_COUNT);
    }

    private void find(String moduleID, NetworkCallback<Modules.Module> moduleNetworkCallback, NetworkErrorCallback errorCallback, int retries) {
        if (mModules != null &&
                mLogin.getLoginTokenKVV() != null && mLogin.getLoginTokenKVV().isOtherUser(mModules.getUsername()) &&
                mLogin.getLoginTokenBB() != null && mLogin.getLoginTokenBB().isOtherUser(mModules.getUsername())
        )
            delete();
        if (retries < 0) {
            log.t("Too many retires", retries);
            errorCallback.onError(new NetworkError(101107, -1, "Too many retries!"));
            return;
        }
        if (this.mModules != null) {
            Modules.Module module = this.mModules.get(moduleID);
            if (module != null) {
                moduleNetworkCallback.onResponse(module);
                return;
            }
        }
        recv(success -> find(moduleID, moduleNetworkCallback, errorCallback, retries - 1), errorCallback, true, RETRY_COUNT);
    }

    void store() {
        if (this.mModules != null) {
            try {
                this.mModules.save(getContext());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private void restore() {
        try {
            this.mModules = Modules.load(getContext());
        } catch (FileNotFoundException ignored) {
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        if (this.mModules == null) {
            recv(success -> {}, log::e);
        }
    }

    public void delete() {
        if (this.mModules != null) {
            this.mModules.delete(getContext());
            this.mModules = null;
        }
    }

    public void recv(final NetworkCallback<Modules> callback, final NetworkErrorCallback errorCallback) {
        recv(callback, errorCallback, false);
    }

    public void recv(final NetworkCallback<Modules> callback, final NetworkErrorCallback errorCallback, boolean forceRefresh) {
        recv(callback, errorCallback, forceRefresh, RETRY_COUNT);
    }

    private void recv(final NetworkCallback<Modules> callback, final NetworkErrorCallback errorCallback, boolean forceRefresh, final int retries) {
        if (mModules != null &&
                mLogin.getLoginTokenKVV() != null && mLogin.getLoginTokenKVV().isOtherUser(mModules.getUsername()) &&
                mLogin.getLoginTokenBB() != null && mLogin.getLoginTokenBB().isOtherUser(mModules.getUsername())
        )
            delete();
        mQueue.add(() -> {
            if (this.mModules != null && !forceRefresh) {
                callback.onResponse(this.mModules);
                mQueue.next();
                return;
            }
            Function<Integer, NetworkErrorCallback> errorFunc = ((Integer errorCode) -> (error -> {
                if (retries > 0 && (error.getHttpStatus() == 401 || error.getHttpStatus() == 403)) {
                    mLogin.refreshLogin(success -> {
                        recv(callback, errorCallback, forceRefresh, retries - 1);
                        mQueue.next();
                    }, error1 -> {
                        errorCallback.onError(error1);
                        mQueue.next();
                    }, errorCode);
                    return;
                }
                errorCallback.onError(error);
                mQueue.next();
            }));
            this.upgradeKVV(successKVV -> {
                this.upgradeBB(successKVV, success -> {
                    if (this.mModules == null)
                        this.mModules = success;
                    else if (this.mModules.updateList(success)) {
                        mListener.onModuleListChange();
                        store();
                    }
                    callback.onResponse(this.mModules);
                    mQueue.next();
                }, errorFunc.apply(Login.LOGOUT_BB));
            }, errorFunc.apply(Login.LOGOUT_KVV));
        });
    }

    private void upgradeKVV(final NetworkCallback<Modules> callback, final NetworkErrorCallback errorCallback) {
        if (!mLogin.isInOnlineMode() || mLogin.getLoginTokenKVV() == null) {
            errorCallback.onError(new NetworkError(101110, 500, "Currently running in offline mode!"));
            return;
        }
        if (!mLogin.getLoginTokenKVV().isAvailable()) {
            callback.onResponse(new Modules(mLogin.getLoginTokenKVV().getUsername()));
            return;
        }
        get("https://kvv.imp.fu-berlin.de/direct/site.json", mLogin.getLoginTokenKVV().getCookies(), response -> {
            String body = response.getParsed();
            if (body == null) {
                errorCallback.onError(new NetworkError(101111, 403, "No module list retrieved!"));
                return;
            }
            Modules modules = new Modules(mLogin.getLoginTokenKVV().getUsername());
            JSONArray sites;
            try {
                JSONObject json = new JSONObject(body);
                sites = json.getJSONArray("site_collection");
            } catch (JSONException e) {
                e.printStackTrace();
                errorCallback.onError(new NetworkError(101112, 403, "Cannot parse module list!"));
                return;
            }
            for (int i = 0; i < sites.length(); i++) {
                try {
                    JSONObject site = sites.getJSONObject(i);
                    String semester_string = site.getJSONObject("props").optString("term_eid", null);
                    Semester semester;
                    if (semester_string == null)
                        semester = null;
                    else
                        semester = new Semester(semester_string);
                    HashSet<String> lvNumbers = new HashSet<>();
                    String kvv_lvnumbers = site.getJSONObject("props").optString("kvv_lvnumbers", null);
                    if (kvv_lvnumbers != null)
                        for (MatchResult matchResult : Regex.allMatches("[0-9]+", kvv_lvnumbers)) {
                            lvNumbers.add(matchResult.group());
                        }
                    String title = site.getString("entityTitle");
                    LinkedHashSet<Lecturer> lecturers = new LinkedHashSet<>();
                    String kvv_lecturers = site.getJSONObject("props").optString("kvv_lecturers", null);
                    if (kvv_lecturers != null) for (String lecturer : kvv_lecturers.split("#")) {
                        if (lecturer.length() > 2)
                            lecturers.add(new Lecturer(lecturer));
                    }
                    String type = site.getJSONObject("props").optString("kvv_coursetype", "Projekt");
                    String description = site.optString("description", "");
                    description = String.valueOf(PartModules.fromHtml(description));
                    String id = site.getString("id");
                    modules.addModule(semester, lvNumbers, title, lecturers, type, description, id, Modules.TYPE_KVV);
                } catch (JSONException e) {
                    log.e(new NetworkError(101113, 403, "Cannot parse module list!"));
                    log.e("ID:", i, "JSON:", sites);
                    e.printStackTrace();
                } catch (NoSuchFieldException e) {
                    log.e(new NetworkError(101114, 403, "Cannot parse module list!"));
                    log.e("ID:", i, "JSON:", sites);
                    e.printStackTrace();
                }
            }
            // Empty module *may be* because token is invalid -> check
            if (modules.size() == 0)
                mLogin.testLoginToken(token -> callback.onResponse(modules), errorCallback);
            else
                callback.onResponse(modules);
        }, error -> errorCallback.onError(new NetworkError(101115, error.networkResponse.statusCode, "Cannot get module list!")));
    }

    private void upgradeBB(final Modules modulesKVV, final NetworkCallback<Modules> callback, final NetworkErrorCallback errorCallback) {
        if (!mLogin.isInOnlineMode() || mLogin.getLoginTokenBB() == null) {
            errorCallback.onError(new NetworkError(101120, 500, "Currently running in offline mode!"));
            return;
        }
        if (!mLogin.getLoginTokenBB().isAvailable()) {
            callback.onResponse(modulesKVV);
            return;
        }
        get(String.format("https://lms.fu-berlin.de/learn/api/public/v1/users/%s/courses?fields=courseId,dataSourceId", mLogin.getLoginTokenBB().getId()), mLogin.getLoginTokenBB().getCookies(), response -> {
            String body = response.getParsed();
            if (body == null) {
                errorCallback.onError(new NetworkError(101121, 403, "No module list retrieved!"));
                return;
            }
            final JSONArray[] sites = new JSONArray[1];
            try {
                JSONObject json = new JSONObject(body);
                sites[0] = json.getJSONArray("results");
            } catch (JSONException e) {
                e.printStackTrace();
                errorCallback.onError(new NetworkError(101122, 403, "Cannot parse module list!"));
                return;
            }
            final int[] latch = {sites[0].length()};
            for (int i = 0; i < sites[0].length(); i++) {
                try {
                    JSONObject site = sites[0].getJSONObject(i);
                    String courseId = site.getString("courseId");
                    get(String.format("https://lms.fu-berlin.de/learn/api/v1/courses/%s?fields=name,courseId", courseId), mLogin.getLoginTokenBB().getCookies(), response1 -> {
                        String body1 = response1.getParsed();
                        if (body1 == null) {
                            errorCallback.onError(new NetworkError(101124, 403, "No module list retrieved!"));
                            return;
                        }
                        try {
                            JSONObject json = new JSONObject(body1);
                            String name = json.getString("name");
                            String description = json.optString("description", null);
                            String type, lvNumber, semYear, semType;
                            Semester semester = null;
                            HashSet<String> lvNumberSet = new HashSet<>();
                            boolean found = false;
                            try {
                                Matcher match = Regex.match("[A-Za-z0-9]*_([A-Za-z0-9]*)_([A-Za-z0-9]*)_([0-9]*)([A-Z]*)", json.getString("courseId"));
                                type = match.group(1);
                                lvNumber = match.group(2);
                                semYear = match.group(3);
                                semType = match.group(4);
                                semester = new Semester(semType.equals("W") ? Semester.SEM_WS : Semester.SEM_SS, Integer.valueOf(semYear));
                                lvNumberSet.add(lvNumber);
                                for (Modules.Module module: modulesKVV) {
                                    if (module.lvNumber.contains(lvNumber)) {
                                        found = true;
                                        break;
                                    }
                                }
                            } catch (NoSuchFieldException e) {
                                type = "Projekt";
                            }
                            if (!found) {
                                Semester finalSemester = semester;
                                String finalType = type;
                                lecturer().getBBLecturers(courseId, success -> {
                                    modulesKVV.addModule(finalSemester, lvNumberSet, name, success, finalType, description, courseId, Modules.TYPE_BB);
                                    if (--latch[0] == 0) callback.onResponse(modulesKVV);
                                }, error -> {
                                    log.e(error);
                                    if (--latch[0] == 0) callback.onResponse(modulesKVV);
                                });
                            } else if (--latch[0] == 0) callback.onResponse(modulesKVV);
                        } catch (JSONException e) {
                            e.printStackTrace();
                            errorCallback.onError(new NetworkError(101125, 403, "Cannot parse module list!"));
                        }
                    }, error -> errorCallback.onError(new NetworkError(101126, error.networkResponse.statusCode, "Cannot get module list!")));
                } catch (JSONException e) {
                    log.e(new NetworkError(101123, 403, "Cannot parse module list!"));
                    log.e("ID:", i, "JSON:", sites[0]);
                    e.printStackTrace();
                }
            }
        }, error -> errorCallback.onError(new NetworkError(101125, error.networkResponse.statusCode, "Cannot get module list!")));
    }



    private ModulesListLecturer lecturer() {
        if (mLecturer == null)
            mLecturer = new ModulesListLecturer(getContext(), mLogin);
        return mLecturer;
    }
}
