package allen.town.podcast.sync.gpoddernet;

import android.util.Log;

import androidx.annotation.NonNull;

import allen.town.podcast.net.sync.gpoddernet.BuildConfig;
import allen.town.podcast.sync.HostnameParser;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

import allen.town.podcast.sync.gpoddernet.mapper.ResponseMapper;
import allen.town.podcast.sync.gpoddernet.model.GpodnetDevice;
import allen.town.podcast.sync.gpoddernet.model.GpodnetEpisodeActionPostResponse;
import allen.town.podcast.sync.gpoddernet.model.GpodnetPodcast;
import allen.town.podcast.sync.gpoddernet.model.GpodnetTag;
import allen.town.podcast.sync.gpoddernet.model.GpodnetUploadChangesResponse;
import allen.town.podcast.sync.model.EpisodeAction;
import allen.town.podcast.sync.model.EpisodeActionChanges;
import allen.town.podcast.sync.model.ISyncService;
import allen.town.podcast.sync.model.SubscriptionChanges;
import allen.town.podcast.sync.model.SyncServiceException;
import allen.town.podcast.sync.model.UploadChangesResponse;
import okhttp3.Credentials;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;

/**
 * Communicates with the gpodder.net service.
 */
public class GpodnetService implements ISyncService {
    public static final String TAG = "GpodnetService";
    public static final String DEFAULT_BASE_HOST = "gpodder.net";
    private static final int UPLOAD_BULK_SIZE = 30;
    private static final MediaType TEXT = MediaType.parse("plain/text; charset=utf-8");
    private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
    private String baseScheme;
    private int basePort;
    private final String baseHost;
    private final String deviceId;
    private String username;
    private String password;
    private boolean loggedIn = false;

    private final OkHttpClient httpClient;

    public GpodnetService(OkHttpClient httpClient, String baseHosturl,
                          String deviceId, String username, String password)  {
        this.httpClient = httpClient;
        this.deviceId = deviceId;
        this.username = username;
        this.password = password;
        HostnameParser hostname = new HostnameParser(baseHosturl == null ? DEFAULT_BASE_HOST : baseHosturl);
        this.baseHost = hostname.host;
        this.basePort = hostname.port;
        this.baseScheme = hostname.scheme;
    }

    private void requireLoggedIn() {
        if (!loggedIn) {
            throw new IllegalStateException("Not logged in");
        }
    }

    /**
     * Returns the [count] most used tags.
     */
    public List<GpodnetTag> getTopTags(int count) throws GpodnetServiceException {
        URL url;
        try {
            url = new URI(baseScheme, null, baseHost, basePort,
                    String.format(Locale.US, "/api/2/tags/%d.json", count), null, null).toURL();
        } catch (MalformedURLException | URISyntaxException e) {
            e.printStackTrace();
            throw new GpodnetServiceException(e);
        }

        Request.Builder request = new Request.Builder().url(url);
        String response = executeRequest(request);
        try {
            JSONArray jsonTagList = new JSONArray(response);
            List<GpodnetTag> tagList = new ArrayList<>(jsonTagList.length());
            for (int i = 0; i < jsonTagList.length(); i++) {
                JSONObject jsonObject = jsonTagList.getJSONObject(i);
                String title = jsonObject.getString("title");
                String tag = jsonObject.getString("tag");
                int usage = jsonObject.getInt("usage");
                tagList.add(new GpodnetTag(title, tag, usage));
            }
            return tagList;
        } catch (JSONException e) {
            e.printStackTrace();
            throw new GpodnetServiceException(e);
        }
    }

    /**
     * Returns the [count] most subscribed podcasts for the given tag.
     *
     * @throws IllegalArgumentException if tag is null
     */
    public List<GpodnetPodcast> getPodcastsForTag(@NonNull GpodnetTag tag, int count)
            throws GpodnetServiceException {
        try {
            URL url = new URI(baseScheme, null, baseHost, basePort,
                    String.format(Locale.US, "/api/2/tag/%s/%d.json", tag.getTag(), count), null, null).toURL();
            Request.Builder request = new Request.Builder().url(url);
            String response = executeRequest(request);

            JSONArray jsonArray = new JSONArray(response);
            return readPodcastListFromJsonArray(jsonArray);

        } catch (JSONException | MalformedURLException | URISyntaxException e) {
            e.printStackTrace();
            throw new GpodnetServiceException(e);
        }
    }

    /**
     * Returns the toplist of podcast.
     *
     * @param count of elements that should be returned. Must be in range 1..100.
     * @throws IllegalArgumentException if count is out of range.
     */
    public List<GpodnetPodcast> getPodcastToplist(int count) throws GpodnetServiceException {
        if (count < 1 || count > 100) {
            throw new IllegalArgumentException("Count must be in range 1..100");
        }

        try {
            //TODO 这里也要可以选择语言
            URL url = new URI(baseScheme, null, baseHost, basePort,
                    String.format(Locale.US, "/toplist/%d.json", count), null, null).toURL();
            Request.Builder request = new Request.Builder().url(url);
            String response = executeRequest(request);

            JSONArray jsonArray = new JSONArray(response);
            return readPodcastListFromJsonArray(jsonArray);

        } catch (JSONException | MalformedURLException | URISyntaxException e) {
            e.printStackTrace();
            throw new GpodnetServiceException(e);
        }
    }

    /**
     * Searches the podcast directory for a given string.
     *
     * @param query          The search query
     * @param scaledLogoSize The size of the logos that are returned by the search query.
     *                       Must be in range 1..256. If the value is out of range, the
     *                       default value defined by the gpodder.net API will be used.
     */
    public List<GpodnetPodcast> searchPodcasts(String query, int scaledLogoSize) throws GpodnetServiceException {
        String parameters = (scaledLogoSize > 0 && scaledLogoSize <= 256) ? String
                .format(Locale.US, "q=%s&scale_logo=%d", query, scaledLogoSize) : String
                .format("q=%s", query);
        try {
            URL url = new URI(baseScheme, null, baseHost, basePort, "/search.json",
                    parameters, null).toURL();
            Request.Builder request = new Request.Builder().url(url);
            String response = executeRequest(request);

            JSONArray jsonArray = new JSONArray(response);
            return readPodcastListFromJsonArray(jsonArray);

        } catch (JSONException | MalformedURLException e) {
            e.printStackTrace();
            throw new GpodnetServiceException(e);
        } catch (URISyntaxException e) {
            e.printStackTrace();
            throw new IllegalStateException(e);
        }
    }

    /**
     * Returns all devices of a given user.
     * <p/>
     * This method requires authentication.
     *
     * @throws GpodnetServiceAuthenticationException If there is an authentication error.
     */
    public List<GpodnetDevice> getDevices() throws GpodnetServiceException {
        requireLoggedIn();
        try {
            URL url = new URI(baseScheme, null, baseHost, basePort,
                    String.format("/api/2/devices/%s.json", username), null, null).toURL();
            Request.Builder request = new Request.Builder().url(url);
            String response = executeRequest(request);
            JSONArray devicesArray = new JSONArray(response);
            return readDeviceListFromJsonArray(devicesArray);
        } catch (JSONException | MalformedURLException | URISyntaxException e) {
            e.printStackTrace();
            throw new GpodnetServiceException(e);
        }
    }

    /**
     * Configures the device of a given user.
     * <p/>
     * This method requires authentication.
     *
     * @param deviceId The ID of the device that should be configured.
     * @throws GpodnetServiceAuthenticationException If there is an authentication error.
     */
    public void configureDevice(@NonNull String deviceId, String caption, GpodnetDevice.DeviceType type)
            throws GpodnetServiceException {
        requireLoggedIn();
        try {
            URL url = new URI(baseScheme, null, baseHost, basePort,
                    String.format("/api/2/devices/%s/%s.json", username, deviceId), null, null).toURL();
            String content;
            if (caption != null || type != null) {
                JSONObject jsonContent = new JSONObject();
                if (caption != null) {
                    jsonContent.put("caption", caption);
                }
                if (type != null) {
                    jsonContent.put("type", type.toString());
                }
                content = jsonContent.toString();
            } else {
                content = "";
            }
            RequestBody body = RequestBody.create(JSON, content);
            Request.Builder request = new Request.Builder().post(body).url(url);
            executeRequest(request);
        } catch (JSONException | MalformedURLException | URISyntaxException e) {
            e.printStackTrace();
            throw new GpodnetServiceException(e);
        }
    }

    /**
     * Uploads the subscriptions of a specific device.
     * <p/>
     * This method requires authentication.
     *
     * @param deviceId      The ID of the device whose subscriptions should be updated.
     * @param subscriptions A list of feed URLs containing all subscriptions of the
     *                      device.
     * @throws IllegalArgumentException              If username, deviceId or subscriptions is null.
     * @throws GpodnetServiceAuthenticationException If there is an authentication error.
     */
    public void uploadSubscriptions(@NonNull String deviceId, @NonNull List<String> subscriptions)
            throws GpodnetServiceException {
        requireLoggedIn();
        try {
            URL url = new URI(baseScheme, null, baseHost, basePort,
                    String.format("/subscriptions/%s/%s.txt", username, deviceId), null, null).toURL();
            StringBuilder builder = new StringBuilder();
            for (String s : subscriptions) {
                builder.append(s);
                builder.append("\n");
            }
            RequestBody body = RequestBody.create(TEXT, builder.toString());
            Request.Builder request = new Request.Builder().put(body).url(url);
            executeRequest(request);
        } catch (MalformedURLException | URISyntaxException e) {
            e.printStackTrace();
            throw new GpodnetServiceException(e);
        }

    }

    /**
     * Updates the subscription list of a specific device.
     * <p/>
     * This method requires authentication.
     *
     * @param added    Collection of feed URLs of added feeds. This Collection MUST NOT contain any duplicates
     * @param removed  Collection of feed URLs of removed feeds. This Collection MUST NOT contain any duplicates
     * @return a GpodnetUploadChangesResponse. See {@link GpodnetUploadChangesResponse}
     * for details.
     * @throws GpodnetServiceException            if added or removed contain duplicates or if there
     *                                            is an authentication error.
     */
    @Override
    public UploadChangesResponse uploadSubscriptionChanges(List<String> added, List<String> removed)
            throws GpodnetServiceException {
        requireLoggedIn();
        try {
            URL url = new URI(baseScheme, null, baseHost, basePort,
                    String.format("/api/2/subscriptions/%s/%s.json", username, deviceId), null, null).toURL();

            final JSONObject requestObject = new JSONObject();
            requestObject.put("add", new JSONArray(added));
            requestObject.put("remove", new JSONArray(removed));

            RequestBody body = RequestBody.create(JSON, requestObject.toString());
            Request.Builder request = new Request.Builder().post(body).url(url);

            final String response = executeRequest(request);
            return GpodnetUploadChangesResponse.fromJSONObject(response);
        } catch (JSONException | MalformedURLException | URISyntaxException e) {
            e.printStackTrace();
            throw new GpodnetServiceException(e);
        }

    }

    /**
     * Returns all subscription changes of a specific device.
     * <p/>
     * This method requires authentication.
     *
     * @param timestamp A timestamp that can be used to receive all changes since a
     *                  specific point in time.
     * @throws GpodnetServiceAuthenticationException If there is an authentication error.
     */
    @Override
    public SubscriptionChanges getSubscriptionChanges(long timestamp) throws GpodnetServiceException {
        requireLoggedIn();
        String params = String.format(Locale.US, "since=%d", timestamp);
        String path = String.format("/api/2/subscriptions/%s/%s.json", username, deviceId);
        try {
            URL url = new URI(baseScheme, null, baseHost, basePort, path, params, null).toURL();
            Request.Builder request = new Request.Builder().url(url);

            String response = executeRequest(request);
            JSONObject changes = new JSONObject(response);
            return ResponseMapper.readSubscriptionChangesFromJsonObject(changes);
        } catch (URISyntaxException e) {
            e.printStackTrace();
            throw new IllegalStateException(e);
        } catch (JSONException | MalformedURLException e) {
            e.printStackTrace();
            throw new GpodnetServiceException(e);
        }

    }

    /**
     * Updates the episode actions
     * <p/>
     * This method requires authentication.
     *
     * @param episodeActions Collection of episode actions.
     * @return a GpodnetUploadChangesResponse. See {@link GpodnetUploadChangesResponse}
     * for details.
     * @throws GpodnetServiceException            if added or removed contain duplicates or if there
     *                                            is an authentication error.
     */
    @Override
    public UploadChangesResponse uploadEpisodeActions(List<EpisodeAction> episodeActions) throws SyncServiceException {
        requireLoggedIn();
        UploadChangesResponse response = null;
        for (int i = 0; i < episodeActions.size(); i += UPLOAD_BULK_SIZE) {
            response = uploadEpisodeActionsPartial(episodeActions,
                    i, Math.min(episodeActions.size(), i + UPLOAD_BULK_SIZE));
        }
        return response;
    }

    private UploadChangesResponse uploadEpisodeActionsPartial(List<EpisodeAction> episodeActions, int from, int to)
            throws SyncServiceException {
        try {
            Log.d(TAG, "Uploading partial actions " + from + " to " + to + " of " + episodeActions.size());
            URL url = new URI(baseScheme, null, baseHost, basePort,
                    String.format("/api/2/episodes/%s.json", username), null, null).toURL();

            final JSONArray list = new JSONArray();
            for (int i = from; i < to; i++) {
                EpisodeAction episodeAction = episodeActions.get(i);
                JSONObject obj = episodeAction.writeToJsonObject();
                if (obj != null) {
                    obj.put("device", deviceId);
                    list.put(obj);
                }
            }

            RequestBody body = RequestBody.create(JSON, list.toString());
            Request.Builder request = new Request.Builder().post(body).url(url);

            final String response = executeRequest(request);
            return GpodnetEpisodeActionPostResponse.fromJSONObject(response);
        } catch (JSONException | MalformedURLException | URISyntaxException e) {
            e.printStackTrace();
            throw new SyncServiceException(e);
        }
    }

    /**
     * Returns all subscription changes of a specific device.
     * <p/>
     * This method requires authentication.
     *
     * @param timestamp A timestamp that can be used to receive all changes since a
     *                  specific point in time.
     * @throws SyncServiceException If there is an authentication error.
     */
    @Override
    public EpisodeActionChanges getEpisodeActionChanges(long timestamp) throws SyncServiceException {
        requireLoggedIn();
        String params = String.format(Locale.US, "since=%d", timestamp);
        String path = String.format("/api/2/episodes/%s.json", username);
        try {
            URL url = new URI(baseScheme, null, baseHost, basePort, path, params, null).toURL();
            Request.Builder request = new Request.Builder().url(url);

            String response = executeRequest(request);
            JSONObject json = new JSONObject(response);
            return ResponseMapper.readEpisodeActionsFromJsonObject(json);
        } catch (URISyntaxException e) {
            e.printStackTrace();
            throw new IllegalStateException(e);
        } catch (JSONException | MalformedURLException e) {
            e.printStackTrace();
            throw new SyncServiceException(e);
        }

    }

    /**
     * Logs in a specific user. This method must be called if any of the methods
     * that require authentication is used.
     *
     * @throws IllegalArgumentException If username or password is null.
     */
    @Override
    public void login() throws GpodnetServiceException {
        URL url;
        try {
            url = new URI(baseScheme, null, baseHost, basePort,
                    String.format("/api/2/auth/%s/login.json", username), null, null).toURL();
        } catch (MalformedURLException | URISyntaxException e) {
            e.printStackTrace();
            throw new GpodnetServiceException(e);
        }
        RequestBody requestBody = RequestBody.create(TEXT, "");
        Request request = new Request.Builder().url(url).post(requestBody).build();
        try {
            String credential = Credentials.basic(username, password, Charset.forName("UTF-8"));
            Request authRequest = request.newBuilder().header("Authorization", credential).build();
            Response response = httpClient.newCall(authRequest).execute();
            checkStatusCode(response);
            response.body().close();
            this.loggedIn = true;
        } catch (Exception e) {
            e.printStackTrace();
            throw new GpodnetServiceException(e);
        }
    }

    private String executeRequest(@NonNull Request.Builder requestB) throws GpodnetServiceException {
        Request request = requestB.build();
        String responseString;
        Response response;
        ResponseBody body = null;
        try {

            response = httpClient.newCall(request).execute();
            checkStatusCode(response);
            body = response.body();
            responseString = getStringFromResponseBody(body);
        } catch (IOException e) {
            e.printStackTrace();
            throw new GpodnetServiceException(e);
        } finally {
            if (body != null) {
                body.close();
            }
        }
        return responseString;
    }

    private String getStringFromResponseBody(@NonNull ResponseBody body) throws GpodnetServiceException {
        ByteArrayOutputStream outputStream;
        int contentLength = (int) body.contentLength();
        if (contentLength > 0) {
            outputStream = new ByteArrayOutputStream(contentLength);
        } else {
            outputStream = new ByteArrayOutputStream();
        }
        try {
            byte[] buffer = new byte[8 * 1024];
            InputStream in = body.byteStream();
            int count;
            while ((count = in.read(buffer)) > 0) {
                outputStream.write(buffer, 0, count);
            }
            return outputStream.toString("UTF-8");
        } catch (IOException e) {
            e.printStackTrace();
            throw new GpodnetServiceException(e);
        }
    }

    private void checkStatusCode(@NonNull Response response) throws GpodnetServiceException {
        int responseCode = response.code();
        if (responseCode != HttpURLConnection.HTTP_OK) {
            if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) {
                throw new GpodnetServiceAuthenticationException("Wrong username or password");
            } else {
                if (BuildConfig.DEBUG) {
                    try {
                        Log.d(TAG, response.body().string());
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                if (responseCode >= 500) {
                    throw new GpodnetServiceBadStatusCodeException("Gpodder.net is currently unavailable (code "
                            + responseCode + ")", responseCode);
                } else {
                    throw new GpodnetServiceBadStatusCodeException("Unable to connect to Gpodder.net (code "
                            + responseCode + ": " + response.message() + ")", responseCode);
                }
            }
        }
    }

    private List<GpodnetPodcast> readPodcastListFromJsonArray(@NonNull JSONArray array) throws JSONException {
        List<GpodnetPodcast> result = new ArrayList<>(array.length());
        for (int i = 0; i < array.length(); i++) {
            result.add(readPodcastFromJsonObject(array.getJSONObject(i)));
        }
        return result;
    }

    private GpodnetPodcast readPodcastFromJsonObject(JSONObject object) throws JSONException {
        String url = object.getString("url");

        String title;
        Object titleObj = object.opt("title");
        if (titleObj instanceof String) {
            title = (String) titleObj;
        } else {
            title = url;
        }

        String description;
        Object descriptionObj = object.opt("description");
        if (descriptionObj instanceof String) {
            description = (String) descriptionObj;
        } else {
            description = "";
        }

        int subscribers = object.getInt("subscribers");

        Object logoUrlObj = object.opt("logo_url");
        String logoUrl = (logoUrlObj instanceof String) ? (String) logoUrlObj : null;
        if (logoUrl == null) {
            Object scaledLogoUrl = object.opt("scaled_logo_url");
            if (scaledLogoUrl instanceof String) {
                logoUrl = (String) scaledLogoUrl;
            }
        }

        String website = null;
        Object websiteObj = object.opt("website");
        if (websiteObj instanceof String) {
            website = (String) websiteObj;
        }
        String mygpoLink = object.getString("mygpo_link");

        String author = null;
        Object authorObj = object.opt("author");
        if (authorObj instanceof String) {
            author = (String) authorObj;
        }
        return new GpodnetPodcast(url, title, description, subscribers, logoUrl, website, mygpoLink, author);
    }

    private List<GpodnetDevice> readDeviceListFromJsonArray(@NonNull JSONArray array) throws JSONException {
        List<GpodnetDevice> result = new ArrayList<>(array.length());
        for (int i = 0; i < array.length(); i++) {
            result.add(readDeviceFromJsonObject(array.getJSONObject(i)));
        }
        return result;
    }

    private GpodnetDevice readDeviceFromJsonObject(JSONObject object) throws JSONException {
        String id = object.getString("id");
        String caption = object.getString("caption");
        String type = object.getString("type");
        int subscriptions = object.getInt("subscriptions");
        return new GpodnetDevice(id, caption, type, subscriptions);
    }

    @Override
    public void logout() {

    }

    public void setCredentials(String username, String password) {
        this.username = username;
        this.password = password;
    }
}
