/*
 * Copyright [2017] Wikimedia Foundation
 *
 * 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.o19s.es.ltr.action;

import com.o19s.es.ltr.action.CachesStatsAction.CachesStatsNodesResponse;
import com.o19s.es.ltr.feature.store.index.Caches;
import org.elasticsearch.action.ActionRequestBuilder;
import org.elasticsearch.action.ActionType;
import org.elasticsearch.action.FailedNodeException;
import org.elasticsearch.action.support.nodes.BaseNodeResponse;
import org.elasticsearch.action.support.nodes.BaseNodesRequest;
import org.elasticsearch.action.support.nodes.BaseNodesResponse;
import org.elasticsearch.client.ElasticsearchClient;
import org.elasticsearch.cluster.ClusterName;
import org.elasticsearch.cluster.node.DiscoveryNode;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.io.stream.Writeable;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;

import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class CachesStatsAction extends ActionType<CachesStatsNodesResponse> {
    public static final String NAME = "cluster:admin/ltr/caches/stats";
    public static final CachesStatsAction INSTANCE = new CachesStatsAction();

    protected CachesStatsAction() {
        super(NAME, CachesStatsNodesResponse::new);
    }

    public static class CachesStatsNodesRequest extends BaseNodesRequest<CachesStatsNodesRequest> {
        public CachesStatsNodesRequest(StreamInput in) throws IOException {
            super(in);
        }

        public CachesStatsNodesRequest() {
            super((String[]) null);
        }
    }

    public static class CachesStatsNodesResponse extends BaseNodesResponse<CachesStatsNodeResponse> implements ToXContent {
        private StatDetails allStores;
        private Map<String, StatDetails> byStore;

        public CachesStatsNodesResponse(StreamInput in) throws IOException {
            super(in);
            allStores = new StatDetails(in);
            byStore = in.readMap(StreamInput::readString, StatDetails::new);
        }

        public CachesStatsNodesResponse(ClusterName clusterName, List<CachesStatsNodeResponse> nodes, List<FailedNodeException> failures) {
            super(clusterName, nodes, failures);
            allStores = new StatDetails();
            byStore = new HashMap<>();
            nodes.forEach((n) -> {
                allStores.doSum(n.allStores);
                n.byStore.forEach((k, v) -> byStore.merge(k, v, StatDetails::sum));
            });
        }

        @Override
        protected List<CachesStatsNodeResponse> readNodesFrom(StreamInput in) throws IOException {
            return in.readList(CachesStatsNodeResponse::new);
        }

        @Override
        protected void writeNodesTo(StreamOutput out, List<CachesStatsNodeResponse> nodes) throws IOException {
            out.writeList(nodes);
        }

        @Override
        public void writeTo(StreamOutput out) throws IOException {
            super.writeTo(out);
            allStores.writeTo(out);
            out.writeMap(byStore, StreamOutput::writeString, (o, s) -> s.writeTo(o));
        }

        @Override
        public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
            builder.field("all", allStores);
            builder.startObject("stores");
            for (Map.Entry<String, StatDetails> entry : byStore.entrySet()) {
                builder.field(entry.getKey(), entry.getValue());
            }
            builder.endObject();
            builder.startObject("nodes");
            for (CachesStatsNodeResponse resp : super.getNodes()) {
                builder.startObject(resp.getNode().getId());
                builder.field("name", resp.getNode().getName());
                builder.field("hostname", resp.getNode().getHostName());
                builder.field("stats", resp.allStores);
                builder.endObject();
            }
            builder.endObject();
            return builder;
        }

        public StatDetails getAll() {
            return allStores;
        }
    }
    public static class CachesStatsNodeResponse extends BaseNodeResponse {
        private StatDetails allStores;
        private Map<String, StatDetails> byStore;

        CachesStatsNodeResponse(DiscoveryNode node) {
            super(node);
            empty();
        }

        CachesStatsNodeResponse(StreamInput in) throws IOException {
            super(in);
            allStores = new StatDetails(in);
            byStore = in.readMap(StreamInput::readString, StatDetails::new);
        }

        @Override
        public void writeTo(StreamOutput out) throws IOException {
            super.writeTo(out);
            allStores.writeTo(out);
            out.writeMap(byStore, StreamOutput::writeString, (o, s) -> s.writeTo(o));
        }

        public void empty() {
            allStores = new StatDetails();
            byStore = new HashMap<>();
        }

        public CachesStatsNodeResponse initFromCaches(Caches caches) {
            allStores = new StatDetails();
            byStore = new HashMap<>();
            caches.perStoreStatsStream().forEach((en) -> {
                StatDetails details = new StatDetails(en.getValue());
                allStores.doSum(details);
                byStore.compute(en.getKey(), (k, v) -> StatDetails.sum(v, details));
            });
            return this;
        }

        public StatDetails getAllStores() {
            return allStores;
        }
    }
    public static class StatDetails implements Writeable, ToXContent {
        private Stat total;
        private Stat features;
        private Stat featuresets;
        private Stat models;

        StatDetails() {
            empty();
        }

        public StatDetails(Caches.PerStoreStats stats) {
            total = new Stat(stats.totalRam(), stats.totalCount());
            features = new Stat(stats.featureRam(), stats.featureCount());
            featuresets = new Stat(stats.featureSetRam(), stats.featureSetCount());
            models = new Stat(stats.modelRam(), stats.modelCount());
        }

        StatDetails(StreamInput in) throws IOException {
            readFrom(in);
        }

        public void readFrom(StreamInput in) throws IOException {
            total = new Stat(in);
            features = new Stat(in);
            featuresets = new Stat(in);
            models = new Stat(in);
        }

        @Override
        public void writeTo(StreamOutput out) throws IOException {
            total.writeTo(out);
            features.writeTo(out);
            featuresets.writeTo(out);
            models.writeTo(out);
        }

        public void empty() {
            total = new Stat(0, 0);
            features = new Stat(0, 0);
            featuresets = new Stat(0, 0);
            models = new Stat(0, 0);
        }

        public static StatDetails sum(StatDetails one, StatDetails two) {
            if (one != null && two != null) {
                one.doSum(two);
                return one;
            } else if (one != null) {
                return one;
            } else if (two != null) {
                return two;
            }
            return null;
        }

        public void doSum(StatDetails other) {
            total.sum(other.total);
            features.sum(other.features);
            featuresets.sum(other.featuresets);
            models.sum(other.models);
        }

        @Override
        public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
            return builder.startObject()
                    .field("total", total)
                    .field("features", features)
                    .field("featuresets", featuresets)
                    .field("models", models)
                    .endObject();
        }

        public Stat getTotal() {
            return total;
        }

        public Stat getFeatures() {
            return features;
        }

        public Stat getFeaturesets() {
            return featuresets;
        }

        public Stat getModels() {
            return models;
        }

        public static class Stat implements Writeable, ToXContent {
            private long ram;
            private int count;

            public Stat(StreamInput in) throws IOException {
                ram = in.readVLong();
                count = in.readVInt();
            }

            public Stat(long ram, int count) {
                this.ram = ram;
                this.count = count;
            }

            public void sum(Stat other) {
                ram += other.ram;
                count += other.count;
            }

            public long getRam() {
                return ram;
            }

            public int getCount() {
                return count;
            }

            @Override
            public void writeTo(StreamOutput out) throws IOException {
                out.writeVLong(ram);
                out.writeVInt(count);
            }

            @Override
            public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
                return builder.startObject()
                        .field("ram", ram)
                        .field("count", count)
                        .endObject();
            }
        }
    }

    public static class CachesStatsActionBuilder extends
        ActionRequestBuilder<CachesStatsNodesRequest, CachesStatsNodesResponse> {
        public CachesStatsActionBuilder(ElasticsearchClient client){
            super(client, INSTANCE, new CachesStatsNodesRequest());
        }
    }

}