/*
 * Copyright 2017 JBoss Inc
 *
 * 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 io.apiman.gateway.engine.vertx.polling;

import io.apiman.gateway.engine.IEngineConfig;
import io.apiman.gateway.engine.Version;
import io.apiman.gateway.engine.async.AsyncInitialize;
import io.apiman.gateway.engine.async.AsyncResultImpl;
import io.apiman.gateway.engine.async.IAsyncHandler;
import io.apiman.gateway.engine.async.IAsyncResultHandler;
import io.apiman.gateway.engine.beans.Api;
import io.apiman.gateway.engine.beans.Client;
import io.apiman.gateway.engine.beans.Policy;
import io.apiman.gateway.engine.impl.InMemoryRegistry;
import io.apiman.gateway.engine.vertx.polling.fetchers.AccessTokenResourceFetcher;
import io.apiman.gateway.engine.vertx.polling.fetchers.FileResourceFetcher;
import io.apiman.gateway.engine.vertx.polling.fetchers.HttpResourceFetcher;
import io.apiman.gateway.engine.vertx.polling.fetchers.threescale.beans.Content;
import io.apiman.gateway.engine.vertx.polling.fetchers.threescale.beans.ProxyConfigRoot;
import io.apiman.gateway.engine.vertx.polling.fetchers.threescale.beans.Service;
import io.apiman.gateway.engine.vertx.polling.fetchers.threescale.beans.ServicesRoot;
import io.apiman.gateway.platforms.vertx3.common.verticles.Json;
import io.vertx.core.CompositeFuture;
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.impl.Arguments;
import io.vertx.core.json.DecodeException;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;

import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * URI loading registry that pulls configuration from a specified 3scale backend, this is
 * mapped to the internal apiman data model and
 * <ul>
 *   <li>accessToken: 3scale access token</li>
 *   <li>apiEndpoint: 3scale API endpoint</li>
 *   <li>environment: which environment (e.g. production, staging)</li>
 *   <li>policyConfigUri: apiman policy config to load as JSON from file
 *   ({@link FileResourceFetcher}) or HTTP/S ({@link HttpResourceFetcher}).
 *   See the corresponding fetcher for additional options.</li>
 * </ul>
 *
 * @author Marc Savy {@literal <marc@rhymewithgravy.com>}
 * @see FileResourceFetcher
 * @see HttpResourceFetcher
 * @see AccessTokenResourceFetcher
 */
@SuppressWarnings("nls")
public class ThreeScaleURILoadingRegistry extends InMemoryRegistry implements AsyncInitialize {
    private static volatile OneShotURILoader instance;
    private Vertx vertx;
    private Map<String, String> options;
    public static final String DEFAULT_NS = "DEFAULT";

    /**
     * @param vertx the vertx instance
     * @param vxConfig the engine config
     * @param options the options
     */
    public ThreeScaleURILoadingRegistry(Vertx vertx, IEngineConfig vxConfig, Map<String, String> options) {
        super();
        this.vertx = vertx;
        this.options = options;
    }

    public ThreeScaleURILoadingRegistry(Map<String, String> options) {
        this(Vertx.vertx(), null, options);
    }

    @Override
    public void initialize(IAsyncResultHandler<Void> resultHandler) {
        getURILoader(vertx, options).subscribe(this, resultHandler::handle);
    }

    private OneShotURILoader getURILoader(Vertx vertx, Map<String, String> options) {
        if (instance == null) {
            synchronized(ThreeScaleURILoadingRegistry.class) {
                if (instance == null) {
                    instance = new OneShotURILoader(vertx, options);
                }
            }
        }
        return instance;
    }

    public static void reloadData(IAsyncHandler<Void> doneHandler) {
        instance.reload(doneHandler);
    }

    public static void reset() {
        synchronized(ThreeScaleURILoadingRegistry.class) {
            instance = null;
        }
    }

    @Override
    public void publishApi(Api api, IAsyncResultHandler<Void> handler) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void retireApi(Api api, IAsyncResultHandler<Void> handler) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void registerClient(Client client, IAsyncResultHandler<Void> handler) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void unregisterClient(Client client, IAsyncResultHandler<Void> handler) {
        throw new UnsupportedOperationException();
    }

    protected void publishApiInternal(Api api, IAsyncResultHandler<Void> handler) {
        super.publishApi(api, handler);
    }

    protected void registerClientInternal(Client client, IAsyncResultHandler<Void> handler) {
        super.registerClient(client, handler);
    }

    private static final class OneShotURILoader {
        private Vertx vertx;
        private URI apiUri;
        private URI policyConfigUri;
        private Map<String, String> config;
        private List<IAsyncResultHandler<Void>> failureHandlers = new ArrayList<>();
        private Deque<ThreeScaleURILoadingRegistry> awaiting = new ArrayDeque<>();
        private List<ThreeScaleURILoadingRegistry> allRegistries = new ArrayList<>();
        private boolean dataProcessed = false;
        private List<ProxyConfigRoot> configs = new ArrayList<>();
        private List<Client> clients = new ArrayList<>();
        private List<Api> apis = new ArrayList<>();
        private Logger log = LoggerFactory.getLogger(OneShotURILoader.class);
        private IAsyncHandler<Void> reloadHandler;
        private List<Api> policyConfigApis = Collections.emptyList();
        private String environment;

        public OneShotURILoader(Vertx vertx, Map<String, String> config) {
            this.config = config;
            this.vertx = vertx;


            apiUri = URI.create(requireOpt("apiEndpoint", "apiEndpoint is required in configuration"));
            environment = config.getOrDefault("environment", "production");
            if (config.containsKey("policyConfigUri"))
                policyConfigUri = URI.create(config.get("policyConfigUri")); // Can be null.

            fetchResource();
        }

        private String requireOpt(String key, String errorMsg) {
            Arguments.require(config.containsKey(key), errorMsg);
            return config.get(key);
        }

        // Clear all registries and add back to processing queue.
        public synchronized void reload(IAsyncHandler<Void> reloadHandler) {
            this.reloadHandler = reloadHandler;
            awaiting.addAll(allRegistries);
            apis.clear();
            clients.clear();
            failureHandlers.clear();
            allRegistries.stream()
                .map(ThreeScaleURILoadingRegistry::getMap)
                .forEach(Map::clear);
            dataProcessed = false;
            // Load again from scratch.
            fetchResource();
        }

        private void fetchResource() {
            log.debug("Fetching 3scale services...");
            // Fetch list of all services
            getServicesRoot(servicesRoot -> {
                List<Service> services = servicesRoot.getServices();
                // Get all service IDs
                List<Long> sids = services.stream()
                        .map(service -> service.getService().getId())
                        .collect(Collectors.toList());
                // Get all configs for given service IDs.
                @SuppressWarnings("rawtypes")
                List<Future> configFutures = sids.stream()
                    .map(this::getConfig)
                    .collect(Collectors.toList());

                // If policyConfigUri is provided, then load API policy config.
                // NB: THESE ARE API BEANSs WITH *ONLY* POLICY CONFIG!
                if (policyConfigUri != null) {
                    configFutures.add(fetchPolicyConfig());
                }

                CompositeFuture.all(configFutures)
                    .setHandler(result -> {
                        if (result.succeeded()) {
                            processData();
                        } else {
                            failAll(result.cause());
                        }
                    });
           });
        }

        private Future<List<Api>> fetchPolicyConfig() {
            log.debug("Loading policy configuration from {0}...", policyConfigUri);
            Future<List<Api>> apiResultFuture = Future.future();
            new PolicyConfigLoader(vertx, policyConfigUri, config)
                .setApiResultHandler(apis -> {
                    this.policyConfigApis = apis;
                    apiResultFuture.complete();
                })
                .setExceptionHandler(apiResultFuture::fail)
                .load();
            return apiResultFuture;
        }

        @SuppressWarnings("rawtypes")
        private Future getConfig(long id) {
            Future future = Future.future();
            String path = String.format("/admin/api/services/%d/proxy/configs/%s/latest.json", id, environment);
            new AccessTokenResourceFetcher(vertx, config, joinPath(path))
                .exceptionHandler(future::fail)
                .fetch(buffer -> {
                    if (buffer.length() > 0) {
                        ProxyConfigRoot pc = Json.decodeValue(buffer.toString(), ProxyConfigRoot.class);
                        log.debug("Received Proxy Config: {0}", pc);
                        configs.add(pc);
                    }
                    future.complete();
                });
            return future;
        }

        private void getServicesRoot(Handler<ServicesRoot> resultHandler) {
            new AccessTokenResourceFetcher(vertx, config, joinPath("/admin/api/services.json"))
                .exceptionHandler(this::failAll)
                .fetch(buffer -> {
                    ServicesRoot sr = Json.decodeValue(buffer.toString(), ServicesRoot.class);
                    System.out.println("Received buffer");
                    //log.debug("Received Services: {0}", sr);
                    resultHandler.handle(sr);
                });
        }

        // Bit messy, refactor
        private URI joinPath(String path) {
            try {
                return new URL(apiUri.toURL(), path).toURI();
            } catch (MalformedURLException | URISyntaxException e) {
                throw new RuntimeException(e);
            }
        }

        private void processData() {
            if (configs.size() == 0) {
                log.warn("File loaded into registry was empty. No entities created.");
                return;
            }
            try {
                // Naive version initially.
                for (ProxyConfigRoot root : configs) {
                    // Reflects the remote data structure.
                    Content config = root.getProxyConfig().getContent();
                    Api api = new Api();
                    api.setApiId(config.getSystemName());
                    api.setOrganizationId(DEFAULT_NS);
                    api.setEndpoint(config.getProxy().getApiBackend());
                    api.setEndpointContentType("text/json"); // don't think there is an equivalent of this in 3scale
                    api.setEndpointType("rest"); //don't think there is an equivalent of this in 3scale
                    api.setParsePayload(false); // can let user override this?
                    api.setPublicAPI(true); // is there an equivalent of this?
                    api.setVersion(DEFAULT_NS); // don't think this is relevant anymore
                    setPolicies(api, root);

                    log.info("Processing - {0}: ", config);
                    log.info("Creating API - {0}: ", api);
                    apis.add(api);
                }

                dataProcessed = true;
                checkQueue();
            } catch (DecodeException e) {
                failAll(e);
            }
        }

        private void setPolicies(Api api, ProxyConfigRoot config) { // FIXME optimise
            // Add 3scale policy
            Policy pol = new Policy();
            pol.setPolicyImpl(determinePolicyImpl()); // TODO get version? Hmm! Env?
            pol.setPolicyJsonConfig(Json.encode(config));
            api.getApiPolicies().add(pol);
            // Add any policies user specified in remote config.
            policyConfigApis.stream()
                .filter(skeleton -> skeleton.getApiId().equals(api.getApiId()))
                // Apply policies from skeleton to 3scale API.
                .forEach(skeleton -> api.getApiPolicies().addAll(skeleton.getApiPolicies()));
        }

        private String determinePolicyImpl() {
            String version = config.getOrDefault("pluginVersion", Version.get().getVersionString());
            return "plugin:io.apiman.plugins:apiman-plugins-3scale-auth:" +
                    version +
                   ":war/io.apiman.plugins.auth3scale.Auth3Scale";
        }

        public synchronized void subscribe(ThreeScaleURILoadingRegistry registry, IAsyncResultHandler<Void> failureHandler) {
            Objects.requireNonNull(registry, "registry must be non-null.");
            Objects.requireNonNull(failureHandler, "failure handler must be non-null.");
            failureHandlers.add(failureHandler);
            allRegistries.add(registry);
            awaiting.add(registry);
            vertx.runOnContext(action -> checkQueue());
        }

        private void checkQueue() {
            if (dataProcessed && awaiting.size() > 0) {
                loadDataIntoRegistries();
            }
        }

        private void loadDataIntoRegistries() {
            ThreeScaleURILoadingRegistry reg = null;
            while ((reg = awaiting.poll()) != null) {
                log.debug("Loading data into registry {0}: ", reg);
                for (Api api : apis) {
                    reg.publishApiInternal(api, handleAnyFailure());
                    log.debug("Publishing: {0} ", api);
                }
                for (Client client : clients) {
                    reg.registerClientInternal(client, handleAnyFailure());
                    log.debug("Registering: {0} ", client);
                }
            }
            if (reloadHandler != null)
                reloadHandler.handle((Void) null);
        }

        private IAsyncResultHandler<Void> handleAnyFailure() {
            return result -> {
                if (result.isError()) {
                    failAll(result.getError());
                    throw new RuntimeException(result.getError());
                }
            };
        }

        private void failAll(Throwable cause) {
            AsyncResultImpl<Void> failure = AsyncResultImpl.create(cause);
            failureHandlers.stream().forEach(failureHandler -> {
                vertx.runOnContext(run -> failureHandler.handle(failure));
            });
        }

    }

}
