/*
 * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
 * or more contributor license agreements. Licensed under the Elastic License;
 * you may not use this file except in compliance with the Elastic License.
 */

package org.elasticsearch.xpack.core;

import org.elasticsearch.Version;
import org.elasticsearch.common.Strings;
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.unit.ByteSizeValue;
import org.elasticsearch.common.xcontent.ToXContentObject;
import org.elasticsearch.common.xcontent.XContentBuilder;

import java.io.IOException;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;

/**
 * {@link DataTiersFeatureSetUsage} represents the xpack usage for data tiers.
 * This includes things like the number of nodes per tier, indices, shards, etc.
 * See {@link TierSpecificStats} for the stats that are tracked on a per-tier
 * basis.
 */
public class DataTiersFeatureSetUsage extends XPackFeatureSet.Usage {
    private final Map<String, TierSpecificStats> tierStats;

    public DataTiersFeatureSetUsage(StreamInput in) throws IOException {
        super(in);
        this.tierStats = in.readMap(StreamInput::readString, TierSpecificStats::new);
    }

    public DataTiersFeatureSetUsage(Map<String, TierSpecificStats> tierStats) {
        super(XPackField.DATA_TIERS, true, true);
        this.tierStats = tierStats;
    }

    @Override
    public Version getMinimalSupportedVersion() {
        return Version.V_7_10_0;
    }

    public Map<String, TierSpecificStats> getTierStats() {
        return Collections.unmodifiableMap(tierStats);
    }

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

    @Override
    protected void innerXContent(XContentBuilder builder, Params params) throws IOException {
        super.innerXContent(builder, params);
        for (Map.Entry<String, TierSpecificStats> tierStats : tierStats.entrySet()) {
            builder.field(tierStats.getKey(), tierStats.getValue());
        }
    }

    @Override
    public int hashCode() {
        return Objects.hash(tierStats);
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        DataTiersFeatureSetUsage other = (DataTiersFeatureSetUsage) obj;
        return Objects.equals(available, other.available) &&
            Objects.equals(enabled, other.enabled) &&
            Objects.equals(tierStats, other.tierStats);
    }

    @Override
    public String toString() {
        return Strings.toString(this);
    }

    /**
     * {@link TierSpecificStats} represents statistics about nodes in a single
     * tier, for example, how many nodes there are, the index count, shard
     * count, etc.
     */
    public static class TierSpecificStats implements Writeable, ToXContentObject {

        public final int nodeCount;
        public final int indexCount;
        public final int totalShardCount;
        public final int primaryShardCount;
        public final long docCount;
        public final long totalByteCount;
        public final long primaryByteCount;
        public final long primaryByteCountMedian;
        public final long primaryShardBytesMAD;

        public TierSpecificStats(StreamInput in) throws IOException {
            this.nodeCount = in.readVInt();
            this.indexCount = in.readVInt();
            this.totalShardCount = in.readVInt();
            this.primaryShardCount = in.readVInt();
            this.docCount = in.readVLong();
            this.totalByteCount = in.readVLong();
            this.primaryByteCount = in.readVLong();
            this.primaryByteCountMedian = in.readVLong();
            this.primaryShardBytesMAD = in.readVLong();
        }

        public TierSpecificStats(int nodeCount, int indexCount, int totalShardCount, int primaryShardCount, long docCount,
                                 long totalByteCount, long primaryByteCount, long primaryByteCountMedian, long primaryShardBytesMAD) {
            this.nodeCount = nodeCount;
            this.indexCount = indexCount;
            this.totalShardCount = totalShardCount;
            this.primaryShardCount = primaryShardCount;
            this.docCount = docCount;
            this.totalByteCount = totalByteCount;
            this.primaryByteCount = primaryByteCount;
            this.primaryByteCountMedian = primaryByteCountMedian;
            this.primaryShardBytesMAD = primaryShardBytesMAD;
        }

        @Override
        public void writeTo(StreamOutput out) throws IOException {
            out.writeVInt(this.nodeCount);
            out.writeVInt(this.indexCount);
            out.writeVInt(this.totalShardCount);
            out.writeVInt(this.primaryShardCount);
            out.writeVLong(this.docCount);
            out.writeVLong(this.totalByteCount);
            out.writeVLong(this.primaryByteCount);
            out.writeVLong(this.primaryByteCountMedian);
            out.writeVLong(this.primaryShardBytesMAD);
        }

        @Override
        public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
            builder.startObject();
            builder.field("node_count", nodeCount);
            builder.field("index_count", indexCount);
            builder.field("total_shard_count", totalShardCount);
            builder.field("primary_shard_count", primaryShardCount);
            builder.field("doc_count", docCount);
            builder.humanReadableField("total_size_bytes", "total_size", new ByteSizeValue(totalByteCount));
            builder.humanReadableField("primary_size_bytes", "primary_size", new ByteSizeValue(primaryByteCount));
            builder.humanReadableField("primary_shard_size_avg_bytes", "primary_shard_size_avg",
                new ByteSizeValue(primaryShardCount == 0 ? 0 : (primaryByteCount / primaryShardCount)));
            builder.humanReadableField("primary_shard_size_median_bytes", "primary_shard_size_median",
                new ByteSizeValue(primaryByteCountMedian));
            builder.humanReadableField("primary_shard_size_mad_bytes", "primary_shard_size_mad",
                new ByteSizeValue(primaryShardBytesMAD));
            builder.endObject();
            return builder;
        }

        @Override
        public int hashCode() {
            return Objects.hash(this.nodeCount, this.indexCount, this.totalShardCount, this.primaryShardCount, this.totalByteCount,
                this.primaryByteCount, this.docCount, this.primaryByteCountMedian, this.primaryShardBytesMAD);
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            TierSpecificStats other = (TierSpecificStats) obj;
            return nodeCount == other.nodeCount &&
                indexCount == other.indexCount &&
                totalShardCount == other.totalShardCount &&
                primaryShardCount == other.primaryShardCount &&
                docCount == other.docCount &&
                totalByteCount == other.totalByteCount &&
                primaryByteCount == other.primaryByteCount &&
                primaryByteCountMedian == other.primaryByteCountMedian &&
                primaryShardBytesMAD == other.primaryShardBytesMAD;
        }

        @Override
        public String toString() {
            return Strings.toString(this);
        }
    }
}
