package de.sebse.fuplanner.services.kvv;

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

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

import java.io.File;
import java.io.FileOutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;

import de.sebse.fuplanner.services.kvv.types.Modules;
import de.sebse.fuplanner.services.kvv.types.Resource;
import de.sebse.fuplanner.tools.Regex;
import de.sebse.fuplanner.tools.UtilsDate;
import de.sebse.fuplanner.tools.network.NetworkCallback;
import de.sebse.fuplanner.tools.network.NetworkError;
import de.sebse.fuplanner.tools.network.NetworkErrorCallback;

public class ModulesResources extends PartModules<ArrayList<Resource>> {

    ModulesResources(Login login, ModulesList list, Context context) {
        super(login, list, context);
    }

    @Override
    protected ArrayList<Resource> getPart(Modules.Module module) {
        return module.resources;
    }

    @Override
    protected boolean setPart(Modules.Module module, ArrayList<Resource> part) {
        boolean changed = module.resources == null || module.resources.hashCode() != part.hashCode();
        module.resources = part;
        return changed;
    }

    @Override
    protected void upgradeKVV(final String ID, final NetworkCallback<ArrayList<Resource>> callback, final NetworkErrorCallback errorCallback) {
        if (!mLogin.isInOnlineMode() || mLogin.getLoginTokenKVV() == null || !mLogin.getLoginTokenKVV().isAvailable()) {
            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), mLogin.getLoginTokenKVV().getCookies(), response -> {
            String body = response.getParsed();
            if (body == null) {
                errorCallback.onError(new NetworkError(101601, 403, "No resources retrieved!"));
                return;
            }
            ArrayList<Resource> resources = new ArrayList<>();
            JSONArray sites;
            try {
                JSONObject json = new JSONObject(body);
                sites = json.getJSONArray("content_collection");
            } catch (JSONException e) {
                e.printStackTrace();
                errorCallback.onError(new NetworkError(101602, 403, "Cannot parse resources!"));
                return;
            }

            for (int i = 0; i < sites.length(); i++) {
                try {
                    JSONObject site = sites.getJSONObject(i);
                    String author = site.getString("author");
                    String title = site.getString("title");
                    //long modifiedDate = site.getLong("modifiedDate");"20181018155405439"
                    long modifiedDate = UtilsDate.stringToMillis(site.getString("modifiedDate"), "yyyyMMddHHmmssSSS");
                    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) {
                    log.e(new NetworkError(101605, 403, "Cannot parse resources!"));
                    e.printStackTrace();
                    log.e("ID:", i, "JSON:", sites);
                    return;
                }
            }

            ArrayList<Resource> root = new ArrayList<>();
            // Generate folder structure
            for (Resource res: resources) {
                if (!res.getContainer().equals("/content/group/")) {
                    if (res.getContainer().equals("/content/group/"+ID+"/")){
                        // if file in root folder
                        root.add(res);
                    } else {
                        // in sub folder
                        for (Resource res2: resources) {
                            try {
                                String utf8Name;
                                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
                                    utf8Name = StandardCharsets.UTF_8.name();
                                else utf8Name = "UTF-8";
                                if (URLDecoder.decode(res2.getUrl(), utf8Name).endsWith(res.getContainer()) && res2 instanceof Resource.Folder) {
                                    // Append File/Folder to list
                                    ((Resource.Folder) res2).add(res);
                                }
                            } catch (UnsupportedEncodingException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }
            }

            // Empty resources *may be* because token is invalid -> check
            if (resources.size() == 0)
                mLogin.testLoginToken(token -> callback.onResponse(root), errorCallback);
            else
                callback.onResponse(root);
        }, error -> errorCallback.onError(new NetworkError(101603, error.networkResponse.statusCode, "Cannot get resources!")));
    }

    @Override
    protected void upgradeBB(String ID, NetworkCallback<ArrayList<Resource>> callback, NetworkErrorCallback errorCallback) {
        if (!mLogin.isInOnlineMode() || mLogin.getLoginTokenBB() == null || !mLogin.getLoginTokenBB().isAvailable()) {
            errorCallback.onError(new NetworkError(101614, 500, "Currently running in offline mode!"));
            return;
        }
        getRescourceFolder(String.format("https://lms.fu-berlin.de/learn/api/public/v1/courses/%s/contents/", ID), ID, false, callback, errorCallback);
        //callback.onResponse(new ArrayList<>());
    }

    private void getRescourceFolder(String url, String ID, boolean subRequest, NetworkCallback<ArrayList<Resource>> callback, NetworkErrorCallback errorCallback) {
        if (mLogin.getLoginTokenBB() == null) {
            errorCallback.onError(new NetworkError(101610, 500, "Cannot get resources!"));
            return;
        }
        get(url, mLogin.getLoginTokenBB().getCookies(), response -> {
            String body = response.getParsed();
            if (body == null) {
                errorCallback.onError(new NetworkError(101611, 403, "No resources retrieved!"));
                return;
            }
            ArrayList<Resource> resources = new ArrayList<>();
            JSONArray sites;
            try {
                JSONObject json = new JSONObject(body);
                sites = json.getJSONArray("results");
            } catch (JSONException e) {
                e.printStackTrace();
                errorCallback.onError(new NetworkError(101612, 400, "Cannot parse resources!"));
                return;
            }

            final int[] latch = {sites.length()};
            for (int i = 0; i < sites.length(); i++) {
                try {
                    JSONObject resource = sites.getJSONObject(i);
                    String resid = resource.getString("id");
                    String title = resource.getString("title");
                    String created = resource.getString("created");
                    String content = resource.getJSONObject("contentHandler").getString("id");
                    long createdDate = UtilsDate.stringToMillis(created, "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
                    if (content.equals("resource/x-bb-folder")) {
                        String url2 = String.format("https://lms.fu-berlin.de/learn/api/public/v1/courses/%s/contents/%s", ID, resid);
                        Resource.Folder folder = new Resource.Folder("", title, createdDate, url2, true, "");
                        resources.add(folder);
                        getRescourceFolder(String.format("https://lms.fu-berlin.de/learn/api/public/v1/courses/%s/contents/%s/children", ID, resid), ID, true, response1 -> {
                            for (Resource resource1 : response1) {
                                folder.add(resource1);
                            }
                            if (--latch[0] == 0) callback.onResponse(resources);
                        }, error -> {
                            if (error.getHttpStatus() == 403) {
                                if (--latch[0] == 0) callback.onResponse(resources);
                            } else {
                                errorCallback.onError(error);
                            }
                        });
                    } else {
                        String bodyText = resource.optString("body", "");
                        bodyText = String.valueOf(PartModules.fromHtml(bodyText));
                        String url2 = String.format("https://lms.fu-berlin.de/learn/api/public/v1/courses/%s/contents/%s", ID, resid);
                        Resource.Document document = new Resource.Document("", title, createdDate, url2, true, "", bodyText);
                        resources.add(document);
                        get(String.format("https://lms.fu-berlin.de/learn/api/public/v1/courses/%s/contents/%s/attachments", ID, resid), mLogin.getLoginTokenBB().getCookies(), response1 -> {

                            String body1 = response1.getParsed();
                            if (body1 == null) {
                                errorCallback.onError(new NetworkError(101617, 403, "No resource attachments retrieved!"));
                                return;
                            }
                            JSONArray attachments;
                            try {
                                JSONObject json = new JSONObject(body1);
                                attachments = json.getJSONArray("results");
                            } catch (JSONException e) {
                                e.printStackTrace();
                                errorCallback.onError(new NetworkError(101618, 400, "Cannot parse resource attachments!"));
                                return;
                            }
                            for (int j = 0; j < attachments.length(); j++) {
                                try {
                                    JSONObject attachment = attachments.getJSONObject(j);
                                    String attachid = attachment.getString("id");
                                    String attachname = attachment.getString("fileName");
                                    document.addUrl(String.format("https://lms.fu-berlin.de/learn/api/public/v1/courses/%s/contents/%s/attachments/%s/download", ID, resid, attachid), attachname);
                                } catch (JSONException e) {
                                    log.e(new NetworkError(101619, 400, "Cannot parse resource attachments!"));
                                    e.printStackTrace();
                                    log.e("ID:", j, "JSON:", attachments);
                                }
                            }
                            if (--latch[0] == 0) callback.onResponse(resources);
                        }, error -> errorCallback.onError(new NetworkError(101616, error.networkResponse.statusCode, "Cannot get resource attachments!")));
                    }
                } catch (JSONException e) {
                    log.e(new NetworkError(101615, 400, "Cannot parse resources!"));
                    e.printStackTrace();
                    log.e("ID:", i, "JSON:", sites);
                }
            }
        }, error -> {
            if (!subRequest || error.networkResponse.statusCode != 403) {
                errorCallback.onError(new NetworkError(101613, error.networkResponse.statusCode, "Cannot get resources!"));
            } else {
                callback.onResponse(new ArrayList<>());
            }
        });
    }
















    public void file(final String filename, final String url, final String modulename, final NetworkCallback<String> callback, final NetworkErrorCallback errorCallback, boolean forceRefresh) {
        file(filename, url, modulename, callback, errorCallback, forceRefresh, RETRY_COUNT);
    }

    private void file(final String filename, final String url, final String modulename, final NetworkCallback<String> callback, final NetworkErrorCallback errorCallback, boolean forceRefresh, int retries) {
        if (isExternalStorageReadable()){
            File f = new File(Environment.getExternalStoragePublicDirectory(
                    Environment.DIRECTORY_DOWNLOADS)+"/"+modulename+"/"+filename);
            // check if file already downloaded -> do not download again
            if (f.exists() && !forceRefresh) {
                callback.onResponse(f.getPath());
                return;
            }
        }
        fileUpgrade(filename, url, modulename, callback, error -> {
            if (retries >= 0 && (error.getHttpStatus() == 401 || error.getHttpStatus() == 403)) {
                mLogin.refreshLogin(success -> {
                    file(filename, url, modulename, callback, errorCallback, forceRefresh, retries-1);
                }, errorCallback, url.contains("lms.fu-berlin.de") ? Login.LOGOUT_BB : Login.LOGOUT_KVV);
                return;
            }
            errorCallback.onError(error);
        });
    }

    private void fileUpgrade(String filename, String url, String modulename, final NetworkCallback<String> callback, final NetworkErrorCallback errorCallback) {
        if (url.contains("lms.fu-berlin.de")) {
            fileUpgradeBB(filename, url, modulename, callback, errorCallback);
        } else {
            fileUpgradeKVV(filename, url, modulename, callback, errorCallback);
        }
    }

    private void fileUpgradeKVV(String filename, String url, String modulename, final NetworkCallback<String> callback, final NetworkErrorCallback errorCallback) {
        if (!mLogin.isInOnlineMode() || mLogin.getLoginTokenKVV() == null || !mLogin.getLoginTokenKVV().isAvailable()) {
            errorCallback.onError(new NetworkError(101604, 500, "Currently running in offline mode!"));
            return;
        }
        get(url, mLogin.getLoginTokenKVV().getCookies(), response -> {
            if (Regex.has("\\.[Uu][Rr][Ll]$", url)){
                // Return redirected URL
                String path = response.getHeaders().get("Location");
                if (path == null){
                    path = "";
                }
                callback.onResponse(path);
            } else if (response.getBytes() == null) {
                errorCallback.onError(new NetworkError(101705, 403, "Cannot get file!"));
            } else if (isExternalStorageWritable()) {
                String path = saveFileInDownloads(filename, response.getBytes(), modulename);
                callback.onResponse(path);
            } else {
                errorCallback.onError(new NetworkError(101704, 400, "External storage not writable!"));
            }
        }, error -> errorCallback.onError(new NetworkError(101702, error.networkResponse.statusCode, "Cannot get file!")));
    }

    private void fileUpgradeBB(String filename, String url, String modulename, final NetworkCallback<String> callback, final NetworkErrorCallback errorCallback) {
        if (!mLogin.isInOnlineMode() || mLogin.getLoginTokenBB() == null || !mLogin.getLoginTokenBB().isAvailable()) {
            errorCallback.onError(new NetworkError(101615, 500, "Currently running in offline mode!"));
            return;
        }
        get(url, mLogin.getLoginTokenBB().getCookies(), response -> {
            if (response.getHeaders().containsKey("Location")) {
                // Return redirected URL
                String redirectUrl = response.getHeaders().get("Location");
                if (redirectUrl == null){
                    redirectUrl = "";
                }
                get(redirectUrl, mLogin.getLoginTokenBB().getCookies(), response1 -> {
                    if (response1.getHeaders().containsKey("Location")) {
                        // Return redirected URL
                        String redirectUrl2 = response1.getHeaders().get("Location");
                        if (redirectUrl2 == null) {
                            redirectUrl2 = "";
                        } else {
                            redirectUrl2 = "https://lms.fu-berlin.de" + redirectUrl2;
                        }
                        get(redirectUrl2, mLogin.getLoginTokenBB().getCookies(), response2 -> {
                            if (response2.getBytes() == null) {
                                errorCallback.onError(new NetworkError(101714, 400, "Cannot get file!"));
                            } else if (isExternalStorageWritable()) {
                                String path = saveFileInDownloads(filename, response2.getBytes(), modulename);
                                callback.onResponse(path);
                            } else {
                                errorCallback.onError(new NetworkError(101713, 400, "External storage not writable!"));
                            }
                        }, error -> errorCallback.onError(new NetworkError(101717, error.networkResponse.statusCode, "Cannot get file!")));
                    } else {
                        errorCallback.onError(new NetworkError(101716, 400, "External storage not writable!"));
                    }
                }, error -> errorCallback.onError(new NetworkError(101712, error.networkResponse.statusCode, "Cannot get file!")));
            } else {
                errorCallback.onError(new NetworkError(101711, 400, "External storage not writable!"));
            }
        }, error -> errorCallback.onError(new NetworkError(101710, error.networkResponse.statusCode, "Cannot get file!")));
    }







    /* Checks if external storage is available for read and write */
    private boolean isExternalStorageWritable() {
        String state = Environment.getExternalStorageState();
        if (Environment.MEDIA_MOUNTED.equals(state)) {
            return true;
        }
        log.w("File system: Writing not possible!");
        return false;
    }

    /* 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;
        }
        log.w("File system: Reading not possible!");
        return false;
    }
    private String saveFileInDownloads(String filename, byte[] data, String moduleName) {
        // Saves file in folder: DOWNLOADS/moduleName
        File folder = new File(Environment.getExternalStoragePublicDirectory(
                Environment.DIRECTORY_DOWNLOADS), moduleName);
        if (!folder.mkdirs()) {
            log.w( "Directory not created");
        }
        String path = "";
        try {
            // TODO check if enough storage space is available
            FileOutputStream out = new FileOutputStream(folder.getPath()+"/"+filename);
            out.write(data);
            out.close();
            path = folder.getPath()+"/"+filename;
        } catch (Exception e) {
            log.w("File not saved!");
            e.printStackTrace();
        }
        return path;
    }
}
