/*
 * Copyright 2016 Sam Sun <me@samczsun.com>
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package com.samczsun.skype4j.internal;

import com.eclipsesource.json.JsonArray;
import com.eclipsesource.json.JsonObject;
import com.eclipsesource.json.JsonValue;
import com.samczsun.skype4j.exceptions.ConnectionException;
import com.samczsun.skype4j.internal.utils.Encoder;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;

public class Endpoints {
    private static Map<Class<?>, Converter<?>> converters = new HashMap<>();

    static {
        converters.put(InputStream.class, HttpURLConnection::getInputStream);
        converters.put(HttpURLConnection.class, in -> in);
        converters.put(JsonObject.class, in -> Utils.parseJsonObject(in.getInputStream()));
        converters.put(JsonArray.class, in -> Utils.parseJsonArray(in.getInputStream()));
        converters.put(String.class, in -> StreamUtils.readFully(in.getInputStream()));
        converters.put(BufferedImage.class, in -> ImageIO.read(in.getInputStream()));
    }

    public static <T> T convert(Class<?> type, SkypeImpl skype, HttpURLConnection in) throws IOException {
        return (T) converters.get(type).convert(in);
    }

    public static final Provider<String> AUTHORIZATION = skype -> "skype_token " + skype.getSkypeToken();
    public static final Provider<String> COOKIE = skype -> "skypetoken_asm=" + skype.getSkypeToken();
    public static final Endpoints ACCEPT_CONTACT_REQUEST = new Endpoints(
            "https://api.skype.com/users/self/contacts/auth-request/%s/accept").skypetoken();
    public static final Endpoints GET_JOIN_URL = new Endpoints("https://api.scheduler.skype.com/threads").skypetoken();
    public static final Endpoints CHAT_INFO_URL = new Endpoints(
            "https://%sclient-s.gateway.messenger.live.com/v1/threads/%s/?view=msnp24Equivalent").cloud().regtoken();
    public static final Endpoints CONVERSATION_PROPERTY_SELF = new Endpoints(
            "https://%sclient-s.gateway.messenger.live.com/v1/users/ME/conversations/%s/properties?name=%s")
            .cloud()
            .regtoken();
    public static final Endpoints SEND_MESSAGE_URL = new Endpoints(
            "https://%sclient-s.gateway.messenger.live.com/v1/users/ME/conversations/%s/messages").cloud().regtoken();
    public static final Endpoints MODIFY_MEMBER_URL = new Endpoints(
            "https://%sclient-s.gateway.messenger.live.com/v1/threads/%s/members/%s").cloud().regtoken();
    public static final Endpoints CONVERSATION_PROPERTY_GLOBAL = new Endpoints(
            "https://%sclient-s.gateway.messenger.live.com/v1/threads/%s/properties?name=%s").cloud().regtoken();
    public static final Endpoints ADD_MEMBER_URL = new Endpoints(
            "https://client-s.gateway.messenger.live.com/v1/threads/%s/members/8:%s").regtoken();
    public static final Endpoints LOGIN_URL = new Endpoints("https://api.skype.com/login/skypetoken");
    public static final Endpoints PING_URL = new Endpoints("https://web.skype.com/api/v1/session-ping").skypetoken();
    public static final Endpoints TOKEN_AUTH_URL = new Endpoints("https://api.asm.skype.com/v1/skypetokenauth");
    public static final Endpoints LOGOUT_URL = new Endpoints(
            "https://login.skype.com/logout?client_id=578134&redirect_uri=https%3A%2F%2Fweb.skype.com&intsrc=client-_-webapp-_-production-_-go-signin");
    public static final Endpoints ENDPOINTS_URL = new Endpoints(
            "https://client-s.gateway.messenger.live.com/v1/users/ME/endpoints");
    public static final Endpoints AUTH_REQUESTS_URL = new Endpoints(
            "https://api.skype.com/users/self/contacts/auth-request").skypetoken();
    public static final Endpoints TROUTER_URL = new Endpoints("https://go.trouter.io/v2/a");
    public static final Endpoints POLICIES_URL = new Endpoints("https://prod.tpc.skype.com/v1/policies").skypetoken();
    public static final Endpoints REGISTRATIONS = new Endpoints(
            "https://prod.registrar.skype.com/v2/registrations").skypetoken();
    public static final Endpoints THREAD_URL = new Endpoints("https://%sclient-s.gateway.messenger.live.com/v1/threads")
            .cloud()
            .regtoken();
    public static final Endpoints SUBSCRIPTIONS_URL = new Endpoints(
            "https://%sclient-s.gateway.messenger.live.com/v1/users/ME/endpoints/SELF/subscriptions")
            .cloud()
            .regtoken();
    public static final Endpoints MESSAGINGSERVICE_URL = new Endpoints(
            "https://%sclient-s.gateway.messenger.live.com/v1/users/ME/endpoints/%s/presenceDocs/messagingService")
            .cloud()
            .regtoken();
    public static final Endpoints POLL = new Endpoints(
            "https://%sclient-s.gateway.messenger.live.com/v1/users/ME/endpoints/SELF/subscriptions/%s/poll")
            .cloud()
            .regtoken();
    public static final Endpoints NEW_GUEST = new Endpoints("https://join.skype.com/api/v1/users/guests");
    public static final Endpoints LEAVE_GUEST = new Endpoints("https://join.skype.com/guests/leave?threadId=%s");
    public static final Endpoints ACTIVE = new Endpoints(
            "https://%sclient-s.gateway.messenger.live.com/v1/users/ME/endpoints/%s/active").cloud().regtoken();
    public static final Endpoints LOAD_CHATS = new Endpoints(
            "https://client-s.gateway.messenger.live.com/v1/users/ME/conversations?startTime=%s&pageSize=%s&view=msnp24Equivalent&targetType=Passport|Skype|Lync|Thread|PSTN|Agent")
            .regtoken();
    public static final Endpoints LOAD_MESSAGES = new Endpoints(
            "https://client-s.gateway.messenger.live.com/v1/users/ME/conversations/%s/messages?startTime=0&pageSize=%s&view=msnp24Equivalent|supportsMessageProperties&targetType=Passport|Skype|Lync|Thread")
            .regtoken();
    public static final Endpoints OBJECTS = new Endpoints("https://api.asm.skype.com/v1/objects").defaultHeader(
            "Authorization", AUTHORIZATION);
    public static final Endpoints UPLOAD_IMAGE = new Endpoints(
            "https://api.asm.skype.com/v1/objects/%s/content/%s").defaultHeader("Authorization", AUTHORIZATION);
    public static final Endpoints IMG_STATUS = new Endpoints(
            "https://api.asm.skype.com/v1/objects/%s/views/%s/status").defaultHeader("Cookie", COOKIE);
    public static final Endpoints FETCH_IMAGE = new Endpoints(
            "https://api.asm.skype.com/v1/objects/%s/views/%s").defaultHeader("Authorization", AUTHORIZATION);
    public static final Endpoints VISIBILITY = new Endpoints(
            "https://%sclient-s.gateway.messenger.live.com/v1/users/ME/presenceDocs/messagingService")
            .cloud()
            .regtoken();
    public static final Endpoints SEARCH_SKYPE_DIRECTORY = new Endpoints(
            "https://api.skype.com/search/users/any?keyWord=%s&contactTypes[]=skype").skypetoken();
    public static final Endpoints GET_ALL_CONTACTS = new Endpoints(
            "https://contacts.skype.com/contacts/v1/users/%s/contacts?delta&$filter=type%%20eq%%20%%27skype%%27%%20or%%20type%%20eq%%20%%27msn%%27%%20or%%20type%%20eq%%20%%27pstn%%27%%20or%%20type%%20eq%%20%%27agent%%27%%20or%%20type%%20eq%%20%%27lync%%27&reason=%s")
            .skypetoken();
    public static final Endpoints GET_CONTACT_BY_ID = new Endpoints(
            "https://contacts.skype.com/contacts/v1/users/%s/contacts?$filter=id%%20eq%%20%%27%s%%27&reason=default").skypetoken();
    public static final Endpoints BLOCK_CONTACT = new Endpoints(
            "https://api.skype.com/users/self/contacts/%s/block").skypetoken();
    public static final Endpoints UNBLOCK_CONTACT = new Endpoints(
            "https://api.skype.com/users/self/contacts/%s/unblock").skypetoken();
    public static final Endpoints AUTHORIZE_CONTACT = new Endpoints(
            "https://api.skype.com/users/self/contacts/auth-request/%s/accept").skypetoken();
    public static final Endpoints UNAUTHORIZE_CONTACT = new Endpoints(
            "https://client-s.gateway.messenger.live.com/v1/users/ME/contacts/8:%s").regtoken();
    public static final Endpoints DECLINE_CONTACT_REQUEST = new Endpoints(
            "https://api.skype.com/users/self/contacts/auth-request/%s/decline").skypetoken();
    public static final Endpoints UNAUTHORIZE_CONTACT_SELF = new Endpoints(
            "https://api.skype.com/users/self/contacts/%s").skypetoken();
    public static final Endpoints AUTHORIZATION_REQUEST = new Endpoints(
            "https://api.skype.com/users/self/contacts/auth-request/%s").skypetoken();
    @Deprecated
    public static final Endpoints CONTACT_INFO = new Endpoints(
            "https://api.skype.com/users/self/contacts/profiles").skypetoken();
    public static final Endpoints PROFILE_INFO = new Endpoints("https://api.skype.com/users/batch/profiles").skypetoken();
    public static final Endpoints RECONNECT_WEBSOCKET = new Endpoints(
            "https://go.trouter.io/v2/h?ccid=%s&dom=web.skype.com");
    public static final Endpoints ELIGIBILITY_CHECK = new Endpoints("https://web.skype.com/api/v2/eligibility-check").skypetoken();

    public static final Endpoints AGENT_INFO = new Endpoints("https://api.aps.skype.com/v1/agents?agentId=%s").skypetoken();

    // todo implement
    // what other scopes are there?
    public static final Endpoints LANGUAGES_GET = new Endpoints("https://dev.microsofttranslator.com/api/languages?scope=text").skypetoken();

    public static final Endpoints NEW_KEY = new Endpoints("https://kes.skype.com/v2/swx/newkey").skypetoken();
    public static final Endpoints PETOKEN = new Endpoints("https://static.asm.skype.com/pes/v1/petoken").defaultHeader("Authorization", AUTHORIZATION);
    public static final Endpoints PROFILE = new Endpoints("https://api.skype.com/users/self/profile").skypetoken();

    private boolean requiresCloud;
    private boolean requiresRegToken;
    private boolean requiresSkypeToken;

    private Map<String, Provider<String>> providers = new HashMap<>();

    private String url;

    public String url() {
        return this.url;
    }

    private Endpoints(String url) {
        this.url = url;
    }

    public static EndpointConnection<HttpURLConnection> custom(String url, SkypeImpl skype, String... args) {
        if (skype.isShutdownRequested()) {
            throw new IllegalStateException("API is shut down");
        }
        return new EndpointConnection(new Endpoints(url), skype, args).as(HttpURLConnection.class);
    }

    public EndpointConnection<HttpURLConnection> open(SkypeImpl skype, Object... args) {
        if (skype.isShutdownRequested()) {
            throw new IllegalStateException("API is shut down");
        }
        return new EndpointConnection(this, skype, args).as(HttpURLConnection.class);
    }

    private Endpoints cloud() {
        this.requiresCloud = true;
        return this;
    }

    private Endpoints regtoken() {
        this.requiresRegToken = true;
        return this;
    }

    private Endpoints skypetoken() {
        this.requiresSkypeToken = true;
        return this;
    }

    private Endpoints defaultHeader(String key, Provider<String> val) {
        this.providers.put(key, val);
        return this;
    }

    public static class EndpointConnection<E_TYPE> {
        private Class<E_TYPE> clazz = (Class<E_TYPE>) HttpURLConnection.class;
        private Endpoints endpoint;
        private SkypeImpl skype;
        private Object[] args;
        private Map<String, String> headers = new HashMap<>();
        private Map<String, String> cookies = new HashMap<>();
        private Map<Predicate<Integer>, UncheckedFunction<E_TYPE>> errors = new HashMap<>();
        private URL url;
        private String cause;
        private boolean dontConnect;
        private boolean redirect = true;

        private EndpointConnection(Endpoints endpoint, SkypeImpl skype, Object[] args) {
            this.endpoint = endpoint;
            this.skype = skype;
            this.args = args;
            header("User-Agent",
                    "Mozilla/5.0 (Windows NT 10; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.73 Safari/537.36 Skype4J/" + SkypeImpl.VERSION);
        }

        public EndpointConnection<E_TYPE> header(String key, String val) {
            this.headers.put(key, val);
            return this;
        }

        public EndpointConnection<E_TYPE> cookie(String key, String val) {
            this.cookies.put(key, val);
            return this;
        }

        public EndpointConnection<E_TYPE> cookies(Map<String, String> cookies) {
            this.cookies.putAll(cookies);
            return this;
        }

        public EndpointConnection<E_TYPE> on(int code, UncheckedFunction<E_TYPE> action) {
            return on(x -> x == code, action);
        }

        public EndpointConnection<E_TYPE> on(Predicate<Integer> check, UncheckedFunction<E_TYPE> result) {
            this.errors.put(check, result);
            return this;
        }

        public EndpointConnection<E_TYPE> expect(int code, String cause) {
            return expect(x -> x == code, cause);
        }

        public EndpointConnection<E_TYPE> expect(Predicate<Integer> check, String cause) {
            this.cause = cause;
            return on(check, (connection) -> convert(clazz, skype, connection));
        }

        public EndpointConnection<E_TYPE> noRedirects() {
            this.redirect = false;
            return this;
        }

        public <NEW_E_TYPE> EndpointConnection<NEW_E_TYPE> as(Class<NEW_E_TYPE> clazz) {
            this.clazz = (Class<E_TYPE>) clazz;
            return (EndpointConnection<NEW_E_TYPE>) this;
        }

        public EndpointConnection<E_TYPE> dontConnect() {
            this.dontConnect = true;
            return this;
        }

        public E_TYPE get() throws ConnectionException {
            return connect("GET", new byte[0]);
        }

        public E_TYPE delete() throws ConnectionException {
            return connect("DELETE", new byte[0]);
        }

        public E_TYPE post() throws ConnectionException {
            return connect("POST", new byte[0]);
        }

        public E_TYPE post(String data) throws ConnectionException {
            return connect("POST", data);
        }

        public E_TYPE post(JsonValue json) throws ConnectionException {
            return header("Content-Type", "application/json").connect("POST", json.toString());
        }

        public E_TYPE put() throws ConnectionException {
            return connect("PUT", new byte[0]);
        }

        public E_TYPE put(String data) throws ConnectionException {
            return connect("PUT", data);
        }

        public E_TYPE put(JsonValue json) throws ConnectionException {
            return header("Content-Type", "application/json").connect("PUT", json.toString());
        }

        public E_TYPE connect(String method, String data) throws ConnectionException {
            return this.connect(method, data != null ? data.getBytes(StandardCharsets.UTF_8) : new byte[0]);
        }

        public E_TYPE connect(String method, byte[] rawData) throws ConnectionException {
            if (!cookies.isEmpty()) {
                header("Cookie", serializeCookies(cookies));
            }
            if (endpoint.requiresRegToken) {
                header("RegistrationToken", skype.getRegistrationToken());
            }
            if (endpoint.requiresSkypeToken) {
                header("X-SkypeToken", skype.getSkypeToken());
            }
            if (this.redirect) {
                this.on(code -> (code >= 301 && code <= 303) || code == 307 || code == 308, connection -> {
                    skype.updateCloud(connection.getHeaderField("Location"));
                    this.url = new URL(connection.getHeaderField("Location"));
                    return this.connect(method, rawData);
                });
            }
            for (Map.Entry<String, Provider<String>> provider : endpoint.providers.entrySet()) {
                header(provider.getKey(), provider.getValue().provide(skype));
            }
            HttpURLConnection connection = null;
            try {
                if (this.url == null) { //todo could fail if cloud is updated?
                    String surl = endpoint.url;
                    if (endpoint.requiresCloud) {
                        Object[] format = new Object[args.length + 1];
                        format[0] = skype.getCloud();
                        for (int i = 1; i < format.length; i++) {
                            format[i] = args[i - 1].toString();
                        }
                        surl = String.format(surl, format);
                    } else if (args.length > 0) {
                        Object[] format = new Object[args.length];
                        for (int i = 0; i < format.length; i++) {
                            format[i] = args[i].toString();
                        }
                        surl = String.format(surl, args);
                    }
                    this.url = new URL(surl);
                }
                connection = (HttpURLConnection) url.openConnection();
                connection.setRequestMethod(method);
                connection.setInstanceFollowRedirects(false);
                for (Map.Entry<String, String> ent : headers.entrySet()) {
                    connection.setRequestProperty(ent.getKey(), ent.getValue());
                }
                if (!method.equalsIgnoreCase("GET")) {
                    connection.setDoOutput(true);
                    if (rawData != null) {
                        connection.getOutputStream().write(rawData);
                    } else {
                        connection.getOutputStream().write(new byte[0]);
                    }
                }
                if (!this.dontConnect) {
                    if (connection.getHeaderField("Set-RegistrationToken") != null) {
                        skype.setRegistrationToken(connection.getHeaderField("Set-RegistrationToken"));
                    }
                    for (Map.Entry<Predicate<Integer>, UncheckedFunction<E_TYPE>> entry : errors.entrySet()) {
                        if (entry.getKey().test(connection.getResponseCode())) {
                            try {
                                return entry.getValue().apply(connection);
                            } catch (Throwable t) {
                                Utils.sneakyThrow(t);
                            }
                        }
                    }
                    throw ExceptionHandler.generateException(cause == null ? this.url.toString() : cause, connection);
                } else if (HttpURLConnection.class.isAssignableFrom(clazz)) {
                    return (E_TYPE) connection;
                } else {
                    throw new IllegalArgumentException(
                            "DontConnect requested but did not request cast to HttpURLConnection");
                }
            } catch (IOException e) {
                throw ExceptionHandler.generateException(cause, e);
            } finally {
                if (clazz != InputStream.class && clazz != HttpURLConnection.class) {
                    if (connection != null) {
                        connection.disconnect();
                    }
                }
            }
        }

        private String serializeCookies(Map<String, String> cookies) {
            StringBuilder result = new StringBuilder();
            for (Map.Entry<String, String> cookie : cookies.entrySet()) {
                result.append(Encoder.encode(cookie.getKey())).append("=").append(Encoder.encode(cookie.getValue())).append(";");
            }
            return result.toString();
        }
    }

    public interface Provider<T> {
        T provide(SkypeImpl skype);
    }

    public interface Converter<T> {
        T convert(HttpURLConnection connection) throws IOException;
    }

    public interface UncheckedFunction<R> extends Function<HttpURLConnection, R> {
        default R apply(HttpURLConnection httpURLConnection) {
            try {
                return apply0(httpURLConnection);
            } catch (Throwable t) {
                Utils.sneakyThrow(t);
            }
            return null;
        }

        R apply0(HttpURLConnection httpURLConnection) throws Throwable;
    }
}
