package de.sebse.fuplanner.services.KVV;

import android.content.Context;
import android.os.Environment;
import android.text.Html;
import android.util.Pair;

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

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.MatchResult;

import de.sebse.fuplanner.services.KVV.types.Announcement;
import de.sebse.fuplanner.services.KVV.types.Assignment;
import de.sebse.fuplanner.services.KVV.types.Event;
import de.sebse.fuplanner.services.KVV.types.Gradebook;
import de.sebse.fuplanner.services.KVV.types.Lecturer;
import de.sebse.fuplanner.services.KVV.types.LoginToken;
import de.sebse.fuplanner.services.KVV.types.Modules;
import de.sebse.fuplanner.services.KVV.types.AssignmentList;
import de.sebse.fuplanner.services.KVV.types.Resource;
import de.sebse.fuplanner.tools.AsyncQueue;
import de.sebse.fuplanner.services.KVV.types.EventList;
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 de.sebse.fuplanner.tools.network.Result;

import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;

/**
 * Created by sebastian on 29.10.17.
 */

class KVVModuleList extends HTTPService {
    private final LoginToken token;
    private Modules moduleList;
    private final AsyncQueue queueModuleDetails = new AsyncQueue();

    KVVModuleList(Context context, LoginToken token) {
        super(context);
        this.token = token;
        try {
            Modules modules = Modules.load(context);
            if (token == null || token.isSameUser(modules.getToken()))
                this.moduleList = modules;
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }




    public void getModuleList(final NetworkCallback<Modules> callback, final NetworkErrorCallback errorCallback, boolean forceRefresh) {
        queueModuleDetails.add("list", () -> {
            if (this.moduleList != null && !forceRefresh) {
                callback.onResponse(this.moduleList);
                queueModuleDetails.next("list");
                return;
            }
            this.getModuleListUpgrade(success -> {
                if (this.moduleList == null)
                    this.moduleList = success;
                else
                    this.moduleList.updateList(success);
                callback.onResponse(this.moduleList);
                queueModuleDetails.next("list");
            }, queueModuleDetails.check("list", errorCallback));
        });
    }

    private void getModuleListUpgrade(final NetworkCallback<Modules> callback, final NetworkErrorCallback errorCallback) {
        if (token == null) {
            errorCallback.onError(new NetworkError(101105, 500, "Currently running in offline mode!"));
            return;
        }
        get("https://kvv.imp.fu-berlin.de/direct/site.json", token.getCookies(), response -> {
            String body = response.getParsed();
            if (body == null) {
                errorCallback.onError(new NetworkError(101101, 403, "No module list retrieved!"));
                return;
            }
            Modules modules = new Modules(token);
            try {
                JSONObject json = new JSONObject(body);
                JSONArray sites = json.getJSONArray("site_collection");

                for (int i = 0; i < sites.length(); i++) {
                    JSONObject site = sites.getJSONObject(i);
                    String semester = site.getJSONObject("props").getString("term_eid");
                    HashSet<String> lvNumbers = new HashSet<>();
                    for (MatchResult matchResult : Regex.allMatches("[0-9]+", site.getJSONObject("props").getString("kvv_lvnumbers"))) {
                        lvNumbers.add(matchResult.group());
                    }
                    String title = site.getString("entityTitle");
                    HashSet<Lecturer> lecturers = new HashSet<>();
                    for (String lecturer : site.getJSONObject("props").getString("kvv_lecturers").split("#")) {
                        if (lecturer.length() > 2)
                            lecturers.add(new Lecturer(lecturer));
                    }
                    String type = site.getJSONObject("props").getString("kvv_coursetype");
                    String description = site.getString("description");
                    description = String.valueOf(Html.fromHtml(description));
                    String id = site.getString("id");
                    modules.addModule(semester, lvNumbers, title, lecturers, type, description, id);
                }
            } catch (JSONException e) {
                e.printStackTrace();
                errorCallback.onError(new NetworkError(101102, 403, "Cannot parse module list!"));
                return;
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
                errorCallback.onError(new NetworkError(101103, 403, "Cannot parse module list!"));
                return;
            }
            // Empty module *may be* because token is invalid -> check
            if (modules.size() == 0)
                testLogin(token, token -> callback.onResponse(modules), errorCallback);
            else
                callback.onResponse(modules);
        }, error -> errorCallback.onError(new NetworkError(101104, error.networkResponse.statusCode, "Cannot get module list!")));
    }

    public void deleteModulesOffline(Context context) {
        if (this.moduleList != null)
            this.moduleList.delete(context);
    }

    public void saveModulesOffline(Context context) throws IOException {
        if (this.moduleList != null)
            this.moduleList.save(context);
    }

    public void getModule(String id, final NetworkCallback<Modules.Module> callback, final NetworkErrorCallback errorCallback, boolean forceRefresh) {
        this.getModuleList(success -> callback.onResponse(success.get(id)), errorCallback, forceRefresh);
    }






    public void getModuleDetails(Modules.Module module, final NetworkCallback<Pair<Modules.Module, Boolean>> callback, final NetworkErrorCallback errorCallback, boolean forceRefresh) {
        AtomicInteger returns = new AtomicInteger(0);
        AtomicReference<NetworkError> lastError = new AtomicReference<>(null);
        final AtomicInteger items = new AtomicInteger(0);
        NetworkCallback<Modules.Module> successCb = success -> {
            returns.getAndIncrement();
            callback.onResponse(Pair.create(module, false));
            if (returns.get() == items.get()) {
                callback.onResponse(Pair.create(module, true));
                if (lastError.get() != null)
                    errorCallback.onError(lastError.get());
            }
        };
        NetworkErrorCallback errorCb = error -> {
            lastError.set(error);
            returns.getAndIncrement();
            if (returns.get() == items.get()) {
                callback.onResponse(Pair.create(module, true));
                if (lastError.get() != null)
                    errorCallback.onError(lastError.get());
            }
        };
        Runnable[] methods = {
                () -> this.getAssignments(module, successCb, errorCb, forceRefresh),
                () -> this.getEvents(module, successCb, errorCb, forceRefresh),
                () -> this.getAnnouncements(module, successCb, errorCb, forceRefresh),
                () -> this.getGradebook(module, successCb, errorCb, forceRefresh),
                () -> this.getResources(module, successCb, errorCb, forceRefresh)
        };
        items.set(methods.length);
        for (Runnable method: methods) {
            method.run();
        }
    }






    public void getAnnouncements(Modules.Module module, final NetworkCallback<Modules.Module> callback, final NetworkErrorCallback errorCallback, boolean forceRefresh) {
        queueModuleDetails.add(module.getID(), () -> {
            if (module.announcements != null && !forceRefresh) {
                callback.onResponse(module);
                queueModuleDetails.next(module.getID());
                return;
            }
            getAnnouncementsUpgrade(module.getID(), success -> {
                module.announcements = success;
                callback.onResponse(module);
                queueModuleDetails.next(module.getID());
            }, queueModuleDetails.check(module.getID(), errorCallback));
        });
    }

    private void getAnnouncementsUpgrade(String ID, final NetworkCallback<ArrayList<Announcement>> callback, final NetworkErrorCallback errorCallback) {
        if (token == null) {
            errorCallback.onError(new NetworkError(101204, 500, "Currently running in offline mode!"));
            return;
        }
        get(String.format("https://kvv.imp.fu-berlin.de/direct/announcement/site/%s.json?n=999999&d=999999999", ID), token.getCookies(), response -> {
            String body = response.getParsed();
            if (body == null) {
                errorCallback.onError(new NetworkError(101201, 403, "No announcements retrieved!"));
                return;
            }
            ArrayList<Announcement> announcements = new ArrayList<>();
            try {
                JSONObject json = new JSONObject(body);
                JSONArray sites = json.getJSONArray("announcement_collection");

                for (int i = 0; i < sites.length(); i++) {
                    JSONObject site = sites.getJSONObject(i);
                    String id = site.getString("announcementId");
                    String title = site.getString("title");
                    String text = site.getString("body");
                    text = String.valueOf(Html.fromHtml(text));
                    String createdBy = site.getString("createdByDisplayName");
                    long createdOn = site.getLong("createdOn");



                    //PDFs links rausziehen
                    JSONArray attachments = site.getJSONArray("attachments");
                    ArrayList<String> urls = new ArrayList<>();
                    for (int j =0; j<attachments.length(); j++){
                        urls.add(attachments.getJSONObject(j).optString("url",null));
                    }

                    announcements.add(new Announcement(id, title, text, createdBy, createdOn, urls));
                }

            } catch (JSONException e) {
                e.printStackTrace();
                errorCallback.onError(new NetworkError(101202, 403, "Cannot parse announcements!"));
                return;
            }
            // Empty announcements *may be* because token is invalid -> check
            if (announcements.size() == 0)
                testLogin(token, token -> callback.onResponse(announcements), errorCallback);
            else
                callback.onResponse(announcements);
        }, error -> errorCallback.onError(new NetworkError(101203, error.networkResponse.statusCode, "Cannot get announcements!")));
    }





    public void getAssignments(Modules.Module module, final NetworkCallback<Modules.Module> callback, final NetworkErrorCallback errorCallback, boolean forceRefresh) {
        queueModuleDetails.add(module.getID(), () -> {
            if (module.assignments != null && !forceRefresh) {
                callback.onResponse(module);
                queueModuleDetails.next(module.getID());
                return;
            }
            getAssignmentsUpgrade(module.getID(), success -> {
                module.assignments = success;
                callback.onResponse(module);
                queueModuleDetails.next(module.getID());
            }, queueModuleDetails.check(module.getID(), errorCallback));
        });
    }

    private void getAssignmentsUpgrade(String ID, final NetworkCallback<AssignmentList> callback, final NetworkErrorCallback errorCallback) {
        if (token == null) {
            errorCallback.onError(new NetworkError(101304, 500, "Currently running in offline mode!"));
            return;
        }
        get(String.format("https://kvv.imp.fu-berlin.de/direct/assignment/site/%s.json", ID), token.getCookies(), response ->{
            String body = response.getParsed();
            if (body == null) {
                errorCallback.onError(new NetworkError(101301, 403, "No assignments retrieved!"));
                return;
            }
            AssignmentList assignments = new AssignmentList();
            try {
                JSONObject json = new JSONObject(body);
                JSONArray sites = json.getJSONArray("assignment_collection");

                for (int i = 0; i < sites.length(); i++) {
                    JSONObject site = sites.getJSONObject(i);
                    String id = site.getString("id");
                    String title = site.getString("title");
                    String instructions = site.getString("instructions");
                    instructions = String.valueOf(Html.fromHtml(instructions));
                    long dueTime = site.getJSONObject("dueTime").getLong("time");
                    String gradebookItemName = site.optString("gradebookItemName", null);
                    String gradeScale = site.getString("gradeScale");
                    JSONArray attachments = site.getJSONArray("attachments");
                    ArrayList<String> urls = new ArrayList<>();
                    for (int j = 0; j<attachments.length(); j++){
                        urls.add(attachments.getJSONObject(j).getString("url"));
                    }
                    assignments.add(0, new Assignment(id, title, dueTime, gradebookItemName, gradeScale, urls, instructions));
                }
            } catch (JSONException e) {
                e.printStackTrace();
                errorCallback.onError(new NetworkError(101302, 403, "Cannot parse announcements!"));
                return;
            }
            // Empty assignments *may be* because token is invalid -> check
            if (assignments.size() == 0)
                testLogin(token, token -> callback.onResponse(assignments), errorCallback);
            else
                callback.onResponse(assignments);
            }, error -> errorCallback.onError(new NetworkError(101303, error.networkResponse.statusCode, "Cannot get assignments!")));


    }






    public void getEvents(Modules.Module module, final NetworkCallback<Modules.Module> callback, final NetworkErrorCallback errorCallback, boolean forceRefresh) {
        queueModuleDetails.add(module.getID(), () -> {
            if (module.events != null && !forceRefresh) {
                callback.onResponse(module);
                queueModuleDetails.next(module.getID());
                return;
            }
            getEventsUpgrade(module.getID(), success -> {
                module.events = success;
                callback.onResponse(module);
                queueModuleDetails.next(module.getID());
            }, queueModuleDetails.check(module.getID(), errorCallback));
        });
    }

    private void getEventsUpgrade(String ID, final NetworkCallback<EventList> callback, final NetworkErrorCallback errorCallback) {
        if (token == null) {
            errorCallback.onError(new NetworkError(101404, 500, "Currently running in offline mode!"));
            return;
        }
        get(String.format("https://kvv.imp.fu-berlin.de/direct/calendar/site/%s.json?detailed=true", ID), token.getCookies(), response -> {
            String body = response.getParsed();
            if (body == null) {
                errorCallback.onError(new NetworkError(101401, 403, "No calendar retrieved!"));
                return;
            }
            EventList events = new EventList();
            try {
                JSONObject json = new JSONObject(body);
                JSONArray sites = json.getJSONArray("calendar_collection");

                for (int i = 0; i < sites.length(); i++) {
                    JSONObject site = sites.getJSONObject(i);
                    String id = site.getString("eventId");
                    String type = site.getString("type");
                    String title = site.getString("title");
                    String siteId = site.getString("siteId");
                    long duration = site.getLong("duration");
                    long firstTime = site.getJSONObject("firstTime").getLong("time");
                    String location = site.getString("location");
                    events.add(new Event(id, type, title, duration, firstTime, siteId, location));
                }
            } catch (JSONException e) {
                e.printStackTrace();
                errorCallback.onError(new NetworkError(101402, 403, "Cannot parse calendar entries!"));
                return;
            }
            events.sort();
            // Empty events *may be* because token is invalid -> check
            if (events.size() == 0)
                testLogin(token, token -> callback.onResponse(events), errorCallback);
            else
                callback.onResponse(events);
        }, error -> errorCallback.onError(new NetworkError(101403, error.networkResponse.statusCode, "Cannot get calendar entries!")));
    }





    public void getGradebook(Modules.Module module, final NetworkCallback<Modules.Module> callback, final NetworkErrorCallback errorCallback, boolean forceRefresh) {
        queueModuleDetails.add(module.getID(), () -> {
            if (module.gradebook != null && !forceRefresh) {
                callback.onResponse(module);
                queueModuleDetails.next(module.getID());
                return;
            }
            getGradebookUpgrade(module.getID(), success -> {
                module.gradebook = success;
                callback.onResponse(module);
                queueModuleDetails.next(module.getID());
            }, queueModuleDetails.check(module.getID(), errorCallback));
        });
    }

    private void getGradebookUpgrade(String ID, final NetworkCallback<ArrayList<Gradebook>> callback, final NetworkErrorCallback errorCallback) {
        if (token == null) {
            errorCallback.onError(new NetworkError(101504, 500, "Currently running in offline mode!"));
            return;
        }
        get(String.format("https://kvv.imp.fu-berlin.de/direct/gradebook/site/%s.json", ID ), token.getCookies(), response ->{
            String body = response.getParsed();
            if (body == null) {
                errorCallback.onError(new NetworkError(101501, 403, "No assignments retrieved!"));
                return;
            }
            ArrayList<Gradebook> gradebook = new ArrayList<>();
            try {
                JSONObject json = new JSONObject(body);
                JSONArray sites = json.getJSONArray("assignments");

                for (int i = 0; i < sites.length(); i++) {
                    JSONObject site = sites.getJSONObject(i);
                    double grade = site.optDouble("grade", 0);
                    String itemName = site.optString("itemName", null);
                    double maxPoints = site.optDouble("points", -1);

                    gradebook.add(0, new Gradebook(itemName, grade, maxPoints));
                }

            }catch (JSONException e) {
                e.printStackTrace();
                errorCallback.onError(new NetworkError(101502, 403, "Cannot parse gradebook for announcements!"));
                return;
            }
            callback.onResponse(gradebook);
        }, error -> errorCallback.onError(new NetworkError(101503, error.networkResponse.statusCode, "Cannot get gradebook for assignments!")));

    }

    public void getResources(Modules.Module module, final NetworkCallback<Modules.Module> callback, final NetworkErrorCallback errorCallback, boolean forceRefresh) {
        queueModuleDetails.add(module.getID(), () -> {
            if (module.resources != null && !forceRefresh) {
                callback.onResponse(module);
                queueModuleDetails.next(module.getID());
                return;
            }
            getResourcesUpgrade(module.getID(), success -> {
                module.resources = success;
                callback.onResponse(module);
                queueModuleDetails.next(module.getID());
            }, queueModuleDetails.check(module.getID(), errorCallback));
        });
    }

    private void getResourcesUpgrade(String ID, final NetworkCallback<ArrayList<Resource>> callback, final NetworkErrorCallback errorCallback) {
        if (token == null) {
            errorCallback.onError(new NetworkError(101604, 500, "Currently running in offline mode!"));
            return;
        }
        get(String.format("https://kvv.imp.fu-berlin.de/direct/content/site/%s.json", ID ), token.getCookies(), response ->{
            String body = response.getParsed();
            if (body == null) {
                errorCallback.onError(new NetworkError(101601, 403, "No resources retrieved!"));
                return;
            }
            ArrayList<Resource> resources = new ArrayList<>();
            try {
                JSONObject json = new JSONObject(body);
                JSONArray sites = json.getJSONArray("content_collection");

                for (int i = 0; i < sites.length(); i++) {
                    JSONObject site = sites.getJSONObject(i);
                    String author = site.getString("author");
                    String title = site.getString("title");
                    long modifiedDate = site.getLong("modifiedDate");
                    String url = site.getString("url");
                    boolean visible = site.getBoolean("visible");
                    String type = site.getString("type");
                    String container = site.getString("container");
                    if (type.equals("collection")){
                        resources.add(new Resource.Folder(author, title, modifiedDate, url, visible, container));
                    }
                    else {
                        resources.add(new Resource.File(author, title, modifiedDate, url, visible, container, type));
                    }

                }

            } catch (JSONException e) {
                e.printStackTrace();
                errorCallback.onError(new NetworkError(101602, 403, "Cannot parse resources!"));
                return;
            }

            ArrayList<Resource> root = new ArrayList<>();
            for (Resource res: resources) {//Verzeichnisstrucktur anlegen
                if (!res.getContainer().equals("/content/group/")) {
                    if (res.getContainer().equals("/content/group/"+ID+"/")){//ist File im Hauptordner
                        root.add(res);
                    }
                    else{
                        for (Resource res2: resources) { // im Unterordner
                            if (res2.getUrl().endsWith(res.getContainer()) && res2 instanceof Resource.Folder){
                                ((Resource.Folder) res2).add(res);//File bzw. Ordner anfügen
                            }
                        }
                    }
                }
            }
            log.d(root);

            // Empty resources *may be* because token is invalid -> check
            if (root.size() == 0)
                testLogin(token, token -> callback.onResponse(root), errorCallback);
            else
                callback.onResponse(root);

        }, error -> errorCallback.onError(new NetworkError(101603, error.networkResponse.statusCode, "Cannot get resources!")));

    }

    public void getResourceFile( final NetworkCallback<String> callback, final NetworkErrorCallback errorCallback, String Filename, String url, String moduleName, boolean downloadNew) {
        //log.d("ResourceFile schreibrechte", WRITE_EXTERNAL_STORAGE);
        //queueModuleDetails.add(module.getID(), () -> {
        if (isExternalStorageReadable()){
            File f = new File(Environment.getExternalStoragePublicDirectory(
                    Environment.DIRECTORY_DOWNLOADS)+"/"+moduleName+"/"+Filename);
            if (f.exists() && !downloadNew) {//prüffen ob bereits herunter gelagen und nicht erneut heruntergeladen werden...
                callback.onResponse(f.getPath());
                log.d("ResourceFile laden aus Speicher");
                return;
            }
        }
        getResourceFileUpgrade(Filename, url , moduleName, success -> {
            callback.onResponse(success);
        }, errorCallback);
        //});
    }

    private void getResourceFileUpgrade(String Filename, String url , String moduleName, final NetworkCallback<String> callback, final NetworkErrorCallback errorCallback) {
        if (token == null) {
            errorCallback.onError(new NetworkError(101701, 500, "Currently running in offline mode!"));
            return;
        }
        get(url, token.getCookies(), response ->{
            final String[] pfad = {""};

            if (response.getBytes()==null){
                log.d("ResourceFile neu einlogen");
                testLogin(token, token -> {
                    if (isExternalStorageWritable()) {
                        get(url, token.getCookies(), response2 ->{//probiern die Datei erneut zu laden
                            log.d("ResourceFile: response2",response2.getParsed());
                            pfad[0] = saveFileInDownloads(Filename, response2, moduleName);
                            log.d("ResourceFile: pfad[0] 2",pfad[0]);
                            callback.onResponse(pfad[0]);
                            log.d("ResourceFile: ", token);
                        }, error -> errorCallback.onError(new NetworkError(101705, error.networkResponse.statusCode, "Cannot get File!")));
                    } else {
                        errorCallback.onError(new NetworkError(101703, 403, "Exgternal storage not writeable!"));
                    }
                }, errorCallback);
            }
            else if (isExternalStorageWritable()) {
                pfad[0] = saveFileInDownloads(Filename, response, moduleName);
                //log.d("ResourceFile: ", token);
                callback.onResponse(pfad[0]);
            } else {
                errorCallback.onError(new NetworkError(101704, 403, "Exgternal storage not writeable!"));
            }

        }, error -> errorCallback.onError(new NetworkError(101702, error.networkResponse.statusCode, "Cannot get File!")));


    }







    // TODO Better, more elegant solution than duplicate code KVVLogin
    private void testLogin(LoginToken loginToken, NetworkCallback<LoginToken> callback, NetworkErrorCallback errorCallback) {
        get(String.format("https://kvv.imp.fu-berlin.de/direct/profile/%s.json", loginToken.getUsername()), loginToken.getCookies(), response -> {
            String body = response.getParsed();
            try {
                JSONObject json = new JSONObject(body);
                String displayName = json.getString("displayName");
                String email = json.getString("email");
                loginToken.setAdditionals(displayName, email);
                callback.onResponse(loginToken);
            } catch (JSONException e) {
                errorCallback.onError(new NetworkError(100201, 403, "Cannot parse profile!"));
            }
        }, error -> errorCallback.onError(new NetworkError(100200, error.networkResponse.statusCode, "Testing login failed!")));
    }



    /* Checks if external storage is available for read and write */
    public boolean isExternalStorageWritable() {
        String state = Environment.getExternalStorageState();
        if (Environment.MEDIA_MOUNTED.equals(state)) {
            return true;
        }
        log.d("Dateisystem: kein schreiben möglich");
        return false;
    }

    /* Checks if external storage is available to at least read */
    public boolean isExternalStorageReadable() {
        String state = Environment.getExternalStorageState();
        if (Environment.MEDIA_MOUNTED.equals(state) ||
                Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
            return true;
        }
        log.d("Dateisystem: kein lesen möglich");
        return false;
    }
    public String saveFileInDownloads(String filename, Result datei, String moduleName) {
        //Speichert Datei im Ordner DOWNLOADS/moduleName
        File ordner = new File(Environment.getExternalStoragePublicDirectory(
                Environment.DIRECTORY_DOWNLOADS), moduleName);
        if (!ordner.mkdir()) {
            //Log.e(LOG_TAG, "Directory not created");
            log.d( "Directory not created");
        }
        String pfad="";
        try {
            //todo eventuel prüfen ob genügend speicher noch vorhanden ist
            FileOutputStream out = new FileOutputStream(ordner.getPath()+"/"+filename);
            out.write(datei.getBytes());
            out.close();
            pfad=ordner.getPath()+"/"+filename;
        } catch (Exception e) {
            log.d("Datei nicht gespeichert!");
            e.printStackTrace();
        }
        return pfad;
    }


}
