package com.faltro.houdoku.plugins.tracker;

import static com.faltro.houdoku.net.Requests.GET;
import static com.faltro.houdoku.net.Requests.POST;
import static com.faltro.houdoku.net.Requests.PATCH;

import com.faltro.houdoku.Houdoku;
import com.faltro.houdoku.data.Serializer;
import com.faltro.houdoku.exception.NotAuthenticatedException;
import com.faltro.houdoku.model.Statuses;
import com.faltro.houdoku.model.Statuses.Status;
import com.faltro.houdoku.model.Track;
import com.faltro.houdoku.net.KitsuInterceptor;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import okhttp3.*;

/**
 * This class contains implementation details for processing data from a specific "tracker" - a
 * website for users to track their reading.
 * 
 * <p>For method and field documentation, please see the Tracker/TrackerOAuth classes. Additionally,
 * the implementation of some common methods is done in the GenericTrackerOAuth class.
 *
 * @see GenericTrackerOAuth
 * @see TrackerOAuth
 * @see Tracker
 */
public class Kitsu extends GenericTrackerOAuth {
    public static final int ID = 1;
    public static final String NAME = "Kitsu";
    public static final String DOMAIN = "kitsu.io";
    public static final String PROTOCOL = "https";
    public static final String TOKEN_URL = "/api/oauth/token";
    public static final String CLIENT_ID = Houdoku.getKitsuId();
    public static final String CLIENT_SECRET = Houdoku.getKitsuSecret();
    private static final String ALGOLIA_URL = "https://AWQO5J657S-dsn.algolia.net/1/indexes/production_media/query";
    private static final String ALGOLIA_APP_ID = "AWQO5J657S";
    private static final String ALGOLIA_FILTER = "&facetFilters=%5B%22kind%3Amanga%22%5D&attributesToRetrieve=%5B%22synopsis%22%2C%22canonicalTitle%22%2C%22chapterCount%22%2C%22posterImage%22%2C%22startDate%22%2C%22subtype%22%2C%22endDate%22%2C%20%22id%22%5D";
    private final KitsuInterceptor interceptor = new KitsuInterceptor();
    private final OkHttpClient client =
            new OkHttpClient().newBuilder().addInterceptor(interceptor).build();
    private final HashMap<Status, String> statuses = new HashMap<Status, String>() {
        private static final long serialVersionUID = 1L;
        {
            put(Status.READING, "current");
            put(Status.PLANNING, "planned");
            put(Status.COMPLETED, "completed");
            put(Status.DROPPED, "dropped");
            put(Status.PAUSED, "on_hold");
            put(Status.REREADING, "current");
        }
    };

    public Kitsu() {
    }

    public Kitsu(String access_token) {
        this.authenticated = true;
        this.setAccessToken(access_token);
    }

    @Override
    public void generateToken(String username, String password) throws IOException {
        // TODO: store refresh_token from response and add support for using it when necessary 
        FormBody.Builder body = new FormBody.Builder();
        body.add("grant_type", "password");
        body.add("client_id", CLIENT_ID);
        body.add("client_secret", CLIENT_SECRET);
        body.add("username", username);
        body.add("password", password);
        Response response = POST(client, PROTOCOL + "://" + DOMAIN + TOKEN_URL, body.build());

        JsonObject json_data = new JsonParser().parse(response.body().string()).getAsJsonObject();
        JsonElement json_access_token = json_data.get("access_token");
        if (json_access_token != null) {
            this.setAccessToken(json_access_token.getAsString());
            this.authenticated = true;
        }
    }

    @Override
    public String authenticatedUserName() throws IOException, NotAuthenticatedException {
        return authenticatedUser().get("attributes").getAsJsonObject().get("name").getAsString();
    }

    @Override
    public String search(String query) throws IOException, NotAuthenticatedException {
        Map<String, String> headers = new HashMap<>();
        headers.put("X-Algolia-Application-Id", ALGOLIA_APP_ID);
        headers.put("X-Algolia-API-Key", algoliaKey());

        JsonObject json = new JsonObject();
        json.addProperty("params", "query=" + query + ALGOLIA_FILTER);

        Response response = POST(client, ALGOLIA_URL, json.toString(), headers);
        JsonObject json_response =
                new JsonParser().parse(response.body().string()).getAsJsonObject();

        JsonObject first = json_response.get("hits").getAsJsonArray().get(0).getAsJsonObject();
        return first.get("id").getAsString();
    }

    @Override
    public String getTitle(String id) throws IOException, NotAuthenticatedException {
        Response response = GET(client, PROTOCOL + "://" + DOMAIN + "/api/edge/manga/" + id);
        JsonObject json_response =
                new JsonParser().parse(response.body().string()).getAsJsonObject();

        if (!json_response.has("errors")) {
            return json_response.get("data").getAsJsonObject().get("attributes")
                .getAsJsonObject().get("canonicalTitle").getAsString();
        }

        return "";
    }

    @Override
    public Track getSeriesInList(String id) throws IOException, NotAuthenticatedException {
        if (!this.authenticated) {
            throw new NotAuthenticatedException();
        }

        HashMap<String, String> params = new HashMap<>();
        params.put("filter[manga_id]", id);
        params.put("filter[user_id]", authenticatedUser().get("id").getAsString());
        params.put("include", "manga");
        Response response = GET(client, PROTOCOL + "://" + DOMAIN + "/api/edge/library-entries",
                params);
        JsonObject json_response =
                new JsonParser().parse(response.body().string()).getAsJsonObject();

        JsonArray data = json_response.get("data").getAsJsonArray();
        if (data.size() > 0) {
            JsonObject entry = data.get(0).getAsJsonObject();
            JsonObject included = 
                    json_response.get("included").getAsJsonArray().get(0).getAsJsonObject();
            String listId = entry.get("id").getAsString();
            String title = included.get("attributes").getAsJsonObject()
                    .get("canonicalTitle").getAsString();
            int progress = entry.get("attributes").getAsJsonObject().get("progress").getAsInt();
            Status status = Statuses.get(
                entry.get("attributes").getAsJsonObject().get("status").getAsString());
            JsonElement rating = entry.get("attributes").getAsJsonObject().get("ratingTwenty");
            int score = rating.isJsonNull() ? 0 : rating.getAsInt() * 5;

            return new Track(id, listId, title, progress, status, score);
        }

        return null;
    }

    @Override
    public void update(String id, Track track, boolean safe, boolean can_add)
            throws IOException, NotAuthenticatedException {
        if (!this.authenticated) {
            throw new NotAuthenticatedException();
        }

        Track track_old = getSeriesInList(id);

        if (track_old == null) {
            if (!can_add) {
                // series isn't in the user's list and we aren't allowed to add it, so do nothing
                return;
            }
            // The series doesn't exist in the list, so add it. That method doesn't set any
            // properties (i.e. progress or status), so we'll continue with this method to do an
            // update request as well
            track_old = add(id);
        }

        Status status = track.getStatus() == null ? track_old.getStatus() : track.getStatus();
        int progress = track.getProgress() == null ? track_old.getProgress() : track.getProgress();
        int score = track.getScore() == null ? track_old.getScore() : track.getScore();

        // in safe mode, only update progress if current progress is greater than the desired
        if (safe) {
            Integer progress_old = track_old.getProgress();
            Integer progress_new = track.getProgress();
            if (progress_old != null && progress_new != null) {
                if (progress_old > progress_new) {
                    progress = progress_old;
                }
            }
        }

        Map<String, String> headers = new HashMap<>();
        headers.put("Content-Type", "application/vnd.api+json");

        JsonObject json_attributes = new JsonObject();
        json_attributes.addProperty("status", statuses.get(status));
        json_attributes.addProperty("progress", progress);
        json_attributes.addProperty("ratingTwenty", score / 5);

        JsonObject json_content = new JsonObject();
        json_content.addProperty("type", "libraryEntries");
        json_content.addProperty("id", track_old.getListId());
        json_content.add("attributes", json_attributes);

        JsonObject json = new JsonObject();
        json.add("data", json_content);

        PATCH(client, PROTOCOL + "://" + DOMAIN + "/api/edge/library-entries/"
                + track_old.getListId(), json.toString(), headers,
                MediaType.parse("application/vnd.api+json; charset=utf-8"));
    }

    @Override
    public void deauthenticate() {
        this.authenticated = false;
        this.setAccessToken(null);
    }

    /**
     * Retrieve the tracker's Algolia key for performing queries.
     *
     * @return the Algolia API key
     * @throws IOException               an IOException occurred when retrieving
     * @throws NotAuthenticatedException the user is not authenticated
     */
    private String algoliaKey() throws IOException, NotAuthenticatedException {
        Response response = GET(client, PROTOCOL + "://" + DOMAIN + "/api/edge/algolia-keys/media");
        JsonObject json_response =
                new JsonParser().parse(response.body().string()).getAsJsonObject();
        return json_response.get("media").getAsJsonObject().get("key").getAsString();
    }
    
    /**
     * Retrieve a user object for the authenticated user.
     *
     * @return a JsonObject with the authenticated user's information
     * @throws IOException               an IOException occurred when retrieving
     * @throws NotAuthenticatedException the user is not authenticated
     */
    private JsonObject authenticatedUser() throws IOException, NotAuthenticatedException {
        if (!this.authenticated) {
            throw new NotAuthenticatedException();
        }
        
        HashMap<String, String> params = new HashMap<>();
        params.put("filter[self]", "true");
        Response response = GET(client, PROTOCOL + "://" + DOMAIN + "/api/edge/users", params);
        JsonObject json_response =
                new JsonParser().parse(response.body().string()).getAsJsonObject();
        JsonElement data = json_response.get("data");

        if (data == null) {
            this.authenticated = false;
            throw new NotAuthenticatedException();
        }
        
        return data.getAsJsonArray().get(0).getAsJsonObject();
    }

    /**
     * Add an entry for a series to the user's list.
     * 
     * <p>This method should only be run if the series is known to not exist in the user's list.
     *
     * @param id the series id
     * @return a Track instance for the series in the user's list
     * @throws NotImplementedException   the operation has not yet been implemented for this tracker
     * @throws NotAuthenticatedException the user is not authenticated
     * @throws IOException               an IOException occurred when updating
     */
    private Track add(String id) throws NotAuthenticatedException, IOException {
        Map<String, String> headers = new HashMap<>();
        headers.put("Content-Type", "application/vnd.api+json");

        JsonObject json_user_data = new JsonObject();
        json_user_data.addProperty("id", authenticatedUser().get("id").getAsString());
        json_user_data.addProperty("type", "users");
        JsonObject json_user = new JsonObject();
        json_user.add("data", json_user_data);

        JsonObject json_media_data = new JsonObject();
        json_media_data.addProperty("id", id);
        json_media_data.addProperty("type", "manga");
        JsonObject json_media = new JsonObject();
        json_media.add("data", json_media_data);

        JsonObject json_attributes = new JsonObject();
        json_attributes.addProperty("status", statuses.get(Statuses.Status.PLANNING));
        json_attributes.addProperty("progress", 0);

        JsonObject json_relationships = new JsonObject();
        json_relationships.add("user", json_user);
        json_relationships.add("media", json_media);

        JsonObject json_content = new JsonObject();
        json_content.addProperty("type", "libraryEntries");
        json_content.add("attributes", json_attributes);
        json_content.add("relationships", json_relationships);

        JsonObject json = new JsonObject();
        json.add("data", json_content);

        POST(client, PROTOCOL + "://" + DOMAIN + "/api/edge/library-entries", json.toString(),
                headers, MediaType.parse("application/vnd.api+json; charset=utf-8"));
        return getSeriesInList(id);
    }

    private void setAccessToken(String token) {
        this.accessToken = token;
        interceptor.setToken(token);
    }
}