package org.dcache.services.info;

import com.google.common.base.Splitter;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

import diskCacheV111.util.CacheException;
import diskCacheV111.util.TimeoutCacheException;

import dmg.cells.nucleus.CellEndpoint;
import dmg.cells.nucleus.CellMessageSender;
import dmg.cells.nucleus.CellPath;
import dmg.cells.nucleus.NoRouteToCellException;
import dmg.util.HttpException;
import dmg.util.HttpRequest;
import dmg.util.HttpResponseEngine;

import org.dcache.cells.CellStub;
import org.dcache.services.info.serialisation.JsonSerialiser;
import org.dcache.services.info.serialisation.PrettyPrintTextSerialiser;
import org.dcache.services.info.serialisation.XmlSerialiser;
import org.dcache.util.Args;
import org.dcache.vehicles.InfoGetSerialisedDataMessage;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Predicates.notNull;
import static com.google.common.base.Throwables.throwIfUnchecked;
import static com.google.common.collect.Iterables.find;
import static java.util.Arrays.asList;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.nio.charset.StandardCharsets.UTF_8;

/**
 * This class provides support for querying the info cell via the admin
 * web-interface.  It implements the HttpResponseEngine to handle requests at a
 * particular point (a specific alias).
 * <p>
 * Users may query the complete tree, or select a subtree by specifying the
 * path.
 * <p>
 * It supports several serialisers from which the user may chose, either by
 * specifying  the query parameter 'format', by specifying the HTTP Accept
 * header.  XML is the default if neither indicates which serialiser to use.
 * <p>
 * The implementation caches serialised data for one second.  This is a safety
 * feature to reducing the impact on info of pathologically broken clients that
 * make many requests per second.
 */
public class InfoHttpEngine implements HttpResponseEngine, CellMessageSender
{
    private static final Logger LOGGER = LoggerFactory.getLogger(HttpResponseEngine.class);

    private static final List<String> ENTIRE_TREE = new ArrayList<>();

    private final SerialisationHandler xmlSerialiser =
            new SerialisationHandler(XmlSerialiser.NAME, "text/xml");

    private final SerialisationHandler jsonSerialiser =
            new SerialisationHandler(JsonSerialiser.NAME, "text/json");

    private final SerialisationHandler prettyPrintSerialiser =
            new SerialisationHandler(PrettyPrintTextSerialiser.NAME, "text/x-ascii-art");

    private final Map<String,SerialisationHandler> mimetypeToSerialiser =
            ImmutableMap.<String,SerialisationHandler>builder().
            put("application/xml", xmlSerialiser).
            put("text/xml", xmlSerialiser).
            put("application/json", jsonSerialiser).
            put("text/x-ascii-art", prettyPrintSerialiser).
            build();

    private final Map<String,SerialisationHandler> queryParameterToSerialiser =
            ImmutableMap.<String,SerialisationHandler>builder().
            put("xml", xmlSerialiser).
            put("json", jsonSerialiser).
            put("pretty", prettyPrintSerialiser).
            build();
    private final String _infoCellName;

    private CellStub _info;

    /**
     * httpd-side class for each info-side serialiser.
     */
    private class SerialisationHandler
    {
        private final String _name;
        private final String _mimeType;

        LoadingCache<List<String>, String> resultCache = CacheBuilder.newBuilder()
                .maximumSize(10)
                .expireAfterWrite(1, TimeUnit.SECONDS)
                .build(new CacheLoader<List<String>, String>() {
                    @Override
                    public String load(List<String> path) throws InterruptedException, CacheException, NoRouteToCellException
                    {
                        InfoGetSerialisedDataMessage message =
                                (path == ENTIRE_TREE) ? new InfoGetSerialisedDataMessage(_name)
                                : new InfoGetSerialisedDataMessage(path, _name);
                        message = _info.sendAndWait(message);
                        return message.getSerialisedData();
                    }
                });

        public SerialisationHandler(String name, String mimeType)
        {
            _name = name;
            _mimeType = mimeType;
        }

        public void handleRequest(HttpRequest request) throws HttpException
        {
            String[] urlItems = request.getRequestTokens();
            OutputStream out = request.getOutputStream();

            List<String> path = urlItems.length == 1 ? ENTIRE_TREE :
                    Arrays.asList(urlItems).subList(1, urlItems.length);

            try {
                byte[] raw = resultCache.get(path).getBytes(UTF_8);
                request.printHttpHeader(raw.length);
                request.setContentType(this._mimeType);
                out.write(raw);
            } catch (ExecutionException e) {
                Throwable cause = e.getCause();
                if (cause instanceof TimeoutCacheException) {
                    throw new HttpException(503, "The info cell took too " +
                            "long to reply, suspect trouble (" +
                            cause.getMessage() + ")");
                }
                if (cause instanceof NoRouteToCellException) {
                    throw new HttpException(503, "Unable to locate the info cell");
                }
                if (cause instanceof CacheException) {
                    throw new HttpException(500, "Error when requesting " +
                            "info from info cell. (" + cause.getMessage() + ")");
                }
                if (cause instanceof InterruptedException) {
                    throw new HttpException(503, "Received interrupt " +
                            "whilst processing data. Please try again later.");
                }
                throwIfUnchecked(cause);
                throw new RuntimeException(cause);
            } catch (IOException e) {
                LOGGER.error("Failed to send response: {}", e.getMessage());
            }
        }
    }


    /**
     * The constructor simply creates a new nucleus for us to use when sending messages.
     */
    public InfoHttpEngine(String[] args)
    {
        _infoCellName = new Args(args).getOption("cell");
        checkArgument(_infoCellName != null, "-cell option is required for InfoHttpEngine handler.");
    }

    @Override
    public void setCellEndpoint(CellEndpoint endpoint)
    {
        _info = new CellStub(endpoint, new CellPath(_infoCellName), 4000, MILLISECONDS);
    }

    /**
     * Handle a request for data.  This either returns the cached contents (if
     * still valid), or queries the info cell for information.
     */
    @Override
    public void queryUrl(HttpRequest request) throws HttpException
    {
        LOGGER.info("Received request: {}", request);

        SerialisationHandler handler = find(asList(
                serialiserFromUri(request),
                serialiserFromHttpHeaders(request),
                xmlSerialiser), notNull());

        handler.handleRequest(request);
    }

    private SerialisationHandler serialiserFromUri(HttpRequest request) throws HttpException
    {
        SerialisationHandler serialiser = null;

        String argument = request.getParameter("format");
        if (argument != null) {
            serialiser = queryParameterToSerialiser.get(argument);
            if (serialiser == null) {
                throw new HttpException(415, "specified format does not exist");
            }
        }

        return serialiser;
    }

    private SerialisationHandler serialiserFromHttpHeaders(HttpRequest request)
    {
        String accept = request.getRequestAttributes().get("Accept");
        if (accept == null) {
            return null;
        }

        SerialisationHandler bestHandler = null;

        /*
         * Choose the best mime-type that the client will accept, taking
         * into account which formats we support, the client's preferences
         * (q values) and choosing the most specific (i.e. longest) mime-type.
         * Here is an example value (should be one line)
         *
         *     application/xml;q=0.5,application/json;q=0.8,
         *     application/x-proprietary-format
         *
         * For details, see
         *
         *     http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
         */
        double bestQ = 0;
        String bestEntry = "";
        for (String entry : Splitter.on(',').trimResults().split(accept)) {
            List<String> items = Splitter.on(';').trimResults().splitToList(entry);
            String mimeType = items.get(0);
            List<String> args = items.subList(1, items.size());

            StringBuilder sb = new StringBuilder().append(mimeType);
            double q = 1;
            for (String arg : args) {
                if (arg.startsWith("q=")) {
                    try {
                        q = Double.parseDouble(arg.substring(2));
                    } catch (NumberFormatException e) {
                        LOGGER.debug("malformed q value ('{}') in Accept: {}", q, e.toString());
                        q = 0;
                    }
                } else {
                    sb.append(';').append(arg);
                }
            }
            String entryWithoutQ = sb.toString();

            // REVISIT: no wildcard support for mimetypes; e.g. text/* or */*
            SerialisationHandler handler = mimetypeToSerialiser.get(mimeType);

            if (q >= bestQ && entryWithoutQ.length() > bestEntry.length() && handler != null) {
                bestHandler = handler;
                bestQ = q;
                bestEntry = entryWithoutQ;
            }
        }

        return bestHandler;
    }
}
