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 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 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.getLoginToken() != null && mLogin.getLoginToken().isOtherUser(mModules.getUsername()))
            delete();
        if (retries < 0) {
            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();
        }
    }

    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.getLoginToken() != null && mLogin.getLoginToken().isOtherUser(mModules.getUsername()))
            delete();
        mQueue.add(() -> {
            if (this.mModules != null && !forceRefresh) {
                callback.onResponse(this.mModules);
                mQueue.next();
                return;
            }
            this.upgrade(success -> {
                if (this.mModules == null)
                    this.mModules = success;
                else if(this.mModules.updateList(success)) {
                    mListener.onModuleListChange();
                    store();
                }
                callback.onResponse(this.mModules);
                mQueue.next();
            }, 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();
                    });
                    return;
                }
                errorCallback.onError(error);
                mQueue.next();
            });
        });
    }

    private void upgrade(final NetworkCallback<Modules> callback, final NetworkErrorCallback errorCallback) {
        if (!mLogin.isInOnlineMode() || mLogin.getLoginToken() == null) {
            errorCallback.onError(new NetworkError(101105, 500, "Currently running in offline mode!"));
            return;
        }
        get("https://kvv.imp.fu-berlin.de/direct/site.json", mLogin.getLoginToken().getCookies(), response -> {
            String body = response.getParsed();
            if (body == null) {
                errorCallback.onError(new NetworkError(101101, 403, "No module list retrieved!"));
                return;
            }
            Modules modules = new Modules(mLogin.getLoginToken().getUsername());
            JSONArray sites;
            try {
                JSONObject json = new JSONObject(body);
                sites = json.getJSONArray("site_collection");
            } catch (JSONException e) {
                e.printStackTrace();
                errorCallback.onError(new NetworkError(101102, 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);
                    if (semester_string == null)
                        continue;
                    Semester 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", null);
                    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);
                } catch (JSONException e) {
                    log.e(new NetworkError(101103, 403, "Cannot parse module list!"));
                    log.e("ID:", i, "JSON:", sites);
                    e.printStackTrace();
                } catch (NoSuchFieldException e) {
                    log.e(new NetworkError(101106, 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(101104, error.networkResponse.statusCode, "Cannot get module list!")));
    }
}
