/*
 * Auto-CreamAPI
 * Copyright (C) 2020 Jeddunk
 *
 * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
 * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later
 * version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with this program. If not, see
 * <https://www.gnu.org/licenses/>.
 */

package xyz.jeddunk.autocreamapi.util;

import com.gargoylesoftware.htmlunit.BrowserVersion;
import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
import com.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import com.gargoylesoftware.htmlunit.util.Cookie;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import com.google.gson.reflect.TypeToken;
import kong.unirest.HttpResponse;
import kong.unirest.Unirest;
import me.xdrop.fuzzywuzzy.FuzzySearch;
import me.xdrop.fuzzywuzzy.model.BoundExtractedResult;
import org.jsoup.HttpStatusException;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import xyz.jeddunk.autocreamapi.pojo.App;
import xyz.jeddunk.autocreamapi.pojo.SteamAppsList;
import xyz.jeddunk.autocreamapi.util.env.MainEnv;

import java.io.*;
import java.lang.reflect.Type;
import java.net.URL;
import java.time.Duration;
import java.time.Instant;
import java.util.*;
import java.util.stream.Collectors;

public class SteamAppsListCache {

    final Logger logger = LoggerFactory.getLogger(SteamAppsListCache.class);

    private SteamAppsList list = new SteamAppsList();
    private final File cacheFile = new File("steamapps.json");
    private MainEnv env;
    private String cfduid = "";

    public SteamAppsListCache() {
        try {
            Class<?> envDefault = Class.forName("xyz.jeddunk.autocreamapi.util.env.Default");
            env = (MainEnv) envDefault.newInstance();
            // System.out.println(env.getKey());
        } catch (ClassNotFoundException e) {
            logger.debug("Default environment missing, using main environemnt...");
            env = new MainEnv();
        } catch (IllegalAccessException | InstantiationException e) {
            // Only thrown by newInstance()
            e.printStackTrace();
        }
        //boolean fileFound = true;
        try {
            getListFromFile();
        } catch (FileNotFoundException e) {
            logger.info("File does not exist, trying to create steamapps.json for the first time...");
            //fileFound = false;
            sync();
        } catch (JsonSyntaxException e) {
            logger.warn("File seems to be corrupt, trying to recreate steamapps.json...");
            //fileFound = false;
            sync();
        }
        if (Instant.now().isAfter(list.getTimestamp().plus(Duration.ofDays(3)))) {
            logger.info("List in file is not recent!");
            sync();
        }
    }

    private void sync() {
        getListFromApi();
        try {
            saveListToFile();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    public App getGame(int appId) {
        return list.getSteamAppsList().stream().filter(app -> app.getAppId() == appId).findFirst().orElse(null);
    }

    @SuppressWarnings("unused")
    public App getGame(String name) {
        return list.getSteamAppsList().stream()
                .filter(app -> app.getName().equalsIgnoreCase(name)).findFirst().orElse(null);
    }

    public List<App> findListOfGames(String name) {
        List<BoundExtractedResult<App>> match = FuzzySearch.extractSorted(name, list.getSteamAppsList(), app ->
                app.getName().toLowerCase().replaceAll("[^a-zA-Z0-9\\s+]", ""), 80);
        List<App> collect = match.stream().map(BoundExtractedResult::getReferent).collect(Collectors.toList());
        for (App app : collect) {
            if (app.getName().replaceAll("[^a-zA-Z0-9\\s+]", "")
                    .equalsIgnoreCase(name.replaceAll("[^a-zA-Z0-9\\s+]", ""))) {
                collect = new ArrayList<>();
                collect.add(app);
                break;
            }
        }
        return collect;
    }

    private void getListFromApi() {
        logger.info("Trying to get SteamAppList from API...");
        List<App> apps = getApps();
        list.setTimestamp(Instant.now());
        list.setSteamAppsList(apps);
    }

    private List<App> getApps() {
        HttpResponse<String> httpResponse =
                Unirest.get("https://api.steampowered.com/IStoreService/GetAppList/v1/" +
                        "?key=" + env.getKey() + "&include_games=1&max_results=50000").asString();
        return new Gson()
                .fromJson(httpResponse.getBody(), App.Rezponze.Download.class)
                .getResponse().getApps();
    }

    private void saveListToFile() throws IOException {
        logger.info("Trying to save SteamAppList to file \"" + cacheFile.getAbsolutePath() + "\"...");
        Gson gson = new Gson();
        String jsonString = gson.toJson(list);
        BufferedWriter fOut = new BufferedWriter(new FileWriter(cacheFile));
        fOut.write(jsonString);
        fOut.close();
        logger.info("Successfully saved SteamAppList to file  \"" + cacheFile.getAbsolutePath() + "\"...");
    }

    private void getListFromFile() throws FileNotFoundException, JsonSyntaxException {
        logger.info("Trying to get SteamAppList from file \"" + cacheFile.getAbsolutePath() + "\"...");
        BufferedReader fIn = new BufferedReader(new FileReader(cacheFile));
        String json = fIn.lines().collect(Collectors.joining());
        final Type type = new TypeToken<SteamAppsList>() {
        }.getType();
        Gson gson = new Gson();
        list = gson.fromJson(json, type);
        logger.info("Successfully got SteamAppList from file \"" + cacheFile.getAbsolutePath() + "\"...");
    }

    public Map<Integer, String> getDlcMap(String appId, boolean use_steamdb) {
        logger.info("Getting list of DLC for AppID: " + appId + "...");
        Map<Integer, String> steamStoreDLCs = new HashMap<>();
        Map<Integer, String> steamDbDLCs = new HashMap<>();

        // Steam Store
        try {
            logger.info("Trying to get DLC from Steam store page...");
            Document steamDoc = Jsoup
                    .connect("https://store.steampowered.com/app/" + appId + "/")
                    .cookie("birthtime", "470703601")
                    .cookie("wants_mature_content", "1")
                    .get();
            Elements steamDLCs = steamDoc.getElementsByClass("game_area_dlc_row");
            for (Element dlc : steamDLCs) {
                String dlc_id = dlc.attr("data-ds-appid");
                String dlc_name = dlc
                        .getElementsByClass("game_area_dlc_name")
                        .text().replace("\n", "").trim();
                steamStoreDLCs.put(Integer.parseInt(dlc_id), dlc_name);
            }
            logger.info("Got DLC from Steam store page successfully.");
        } catch (HttpStatusException e) {
            logger.error(e.getUrl());
            if (e.getStatusCode() == 404) {
                logger.error("App ID empty or not found! (HTTP Status Code: 404)");
            } else {
                logger.error("Error occurred while trying to get list of DLCs " +
                        "(HTTP Status Code: " + e.getStatusCode() + ")");
            }
        } catch (NullPointerException | IOException e) {
            // ignore
        }

        // SteamDB
        if (use_steamdb) {

            logger.info("Trying to get DLC from SteamDB...");
            if (cfduid.equals("")) {
                logger.info("Missing CF cookie, trying to get it...");
                WebClient client = new WebClient(BrowserVersion.CHROME);

                client.getOptions().setCssEnabled(false);
                client.getOptions().setJavaScriptEnabled(false);
                client.getOptions().setThrowExceptionOnFailingStatusCode(false);
                client.getOptions().setRedirectEnabled(true);
                client.getCache().setMaxSize(0);
                /*client.waitForBackgroundJavaScript(10000);
                client.setJavaScriptTimeout(10000);
                client.waitForBackgroundJavaScriptStartingBefore(10000);*/

                try {

                    String urlString = "https://steamdb.info/";

                    HtmlPage page = client.getPage(urlString);
                    //noinspection SynchronizationOnLocalVariableOrMethodParameter
                    synchronized (page) {
                        page.wait(8000);
                    }
                    //Print cookies for test purposes. Comment out in production.
                    URL url = new URL(urlString);
                    for (Cookie c : client.getCookies(url)) {
                        //System.out.println(c.getName() +"="+c.getValue());
                        if (c.getName().equals("__cfduid")) {
                            cfduid = c.getValue();
                            logger.info("Got CF cookie successfully.");
                        }
                    }

                    //This prints the content after bypassing Cloudflare.
                    // System.out.println(client.getPage(url).getWebResponse().getContentAsString());
                } catch (FailingHttpStatusCodeException | IOException | InterruptedException e) {
                    // e.printStackTrace();
                    logger.error("Could not get CF cookie, skipping SteamDB DLC list...");
                }
            }

            if ((!(cfduid.equals(""))) && (!(cfduid.equals("N/A")))) {
                try {
                    Document steamDbDoc = Jsoup
                            .connect("https://steamdb.info/app/" + appId + "/dlc/")
                            .cookie("__cfduid", cfduid)
                            .userAgent(BrowserVersion.CHROME.getUserAgent())
                            .get();
                    Element steamDbDlcSection = steamDbDoc.getElementById("dlc");
                    Elements steamDbDLCElements = steamDbDlcSection.getElementsByClass("app");
                    for (Element dlc : steamDbDLCElements) {
                        String dlc_id = dlc.attr("data-appid");
                        String dlc_name = "Unknown DLC " + dlc_id;
                        Elements td = dlc.getElementsByTag("td");
                        if (!td.isEmpty()) {
                            dlc_name = td.get(1).text().replace("\n", "").trim();
                        }
                        steamDbDLCs.put(Integer.parseInt(dlc_id), dlc_name);
                    }
                    logger.info("Got DLC from SteamDB successfully.");
                } catch (HttpStatusException e) {
                    logger.error(e.getUrl());
                    if (e.getStatusCode() == 404) {
                        logger.error("App ID empty or not found! (HTTP Status Code: 404)");
                    } else {
                        logger.error("Error occurred while trying to get list of DLCs " +
                                "(HTTP Status Code: " + e.getStatusCode() + ")");
                    }
                } catch (NullPointerException | IOException e) {
                    // ignore
                }
            }
        }

        logger.info("Merging both DLC lists...");
        Map<Integer, String> allDLCs = new HashMap<>(steamStoreDLCs);
        steamDbDLCs.forEach(allDLCs::putIfAbsent);
        logger.info("DLC list is done!");
        return allDLCs.entrySet().stream()
                .sorted(Map.Entry.comparingByKey())
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,
                        (oldValue, newValue) -> oldValue, LinkedHashMap::new));
    }
}
