/*
 * 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.search.action;

import org.elasticsearch.ExceptionsHelper;
import org.elasticsearch.action.ActionResponse;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.xcontent.StatusToXContentObject;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.rest.action.RestActions;

import java.io.IOException;

import static org.elasticsearch.rest.RestStatus.OK;

/**
 * A response of an async search request.
 */
public class AsyncStatusResponse extends ActionResponse implements StatusToXContentObject {
    private final String id;
    private final boolean isRunning;
    private final boolean isPartial;
    private final long startTimeMillis;
    private final long expirationTimeMillis;
    private final int totalShards;
    private final int successfulShards;
    private final int skippedShards;
    private final int failedShards;
    private final RestStatus completionStatus;

    public AsyncStatusResponse(String id,
            boolean isRunning,
            boolean isPartial,
            long startTimeMillis,
            long expirationTimeMillis,
            int totalShards,
            int successfulShards,
            int skippedShards,
            int failedShards,
            RestStatus completionStatus) {
        this.id = id;
        this.isRunning = isRunning;
        this.isPartial = isPartial;
        this.startTimeMillis = startTimeMillis;
        this.expirationTimeMillis = expirationTimeMillis;
        this.totalShards = totalShards;
        this.successfulShards = successfulShards;
        this.skippedShards = skippedShards;
        this.failedShards = failedShards;
        this.completionStatus = completionStatus;
    }

    public static AsyncStatusResponse getStatusFromAsyncSearchResponseWithExpirationTime(AsyncSearchResponse asyncSearchResponse,
            long expirationTimeMillis) {
        int totalShards = 0;
        int successfulShards = 0;
        int skippedShards = 0;
        int failedShards = 0;
        RestStatus completionStatus = null;
        SearchResponse searchResponse = asyncSearchResponse.getSearchResponse();
        if (searchResponse != null) {
            totalShards = searchResponse.getTotalShards();
            successfulShards = searchResponse.getSuccessfulShards();
            skippedShards = searchResponse.getSkippedShards();
            failedShards = searchResponse.getFailedShards();
        }
        if (asyncSearchResponse.isRunning() == false) {
            if (searchResponse != null) {
                completionStatus = searchResponse.status();
            } else {
                Exception failure = asyncSearchResponse.getFailure();
                if (failure != null) {
                    completionStatus = ExceptionsHelper.status(ExceptionsHelper.unwrapCause(failure));
                }
            }
        }
        return new AsyncStatusResponse(
            asyncSearchResponse.getId(),
            asyncSearchResponse.isRunning(),
            asyncSearchResponse.isPartial(),
            asyncSearchResponse.getStartTime(),
            expirationTimeMillis,
            totalShards,
            successfulShards,
            skippedShards,
            failedShards,
            completionStatus
        );
    }

    public AsyncStatusResponse(StreamInput in) throws IOException {
        this.id = in.readString();
        this.isRunning = in.readBoolean();
        this.isPartial = in.readBoolean();
        this.startTimeMillis = in.readLong();
        this.expirationTimeMillis = in.readLong();
        this.totalShards = in.readVInt();
        this.successfulShards = in.readVInt();
        this.skippedShards = in.readVInt();
        this.failedShards = in.readVInt();
        this.completionStatus = (this.isRunning == false) ? RestStatus.readFrom(in) : null;
    }

    @Override
    public void writeTo(StreamOutput out) throws IOException {
        out.writeString(id);
        out.writeBoolean(isRunning);
        out.writeBoolean(isPartial);
        out.writeLong(startTimeMillis);
        out.writeLong(expirationTimeMillis);
        out.writeVInt(totalShards);
        out.writeVInt(successfulShards);
        out.writeVInt(skippedShards);
        out.writeVInt(failedShards);
        if (isRunning == false) {
            RestStatus.writeTo(out, completionStatus);
        }
    }

    @Override
    public RestStatus status() {
        return OK;
    }

    @Override
    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
        builder.startObject();
        builder.field("id", id);
        builder.field("is_running", isRunning);
        builder.field("is_partial", isPartial);
        builder.timeField("start_time_in_millis", "start_time", startTimeMillis);
        builder.timeField("expiration_time_in_millis", "expiration_time", expirationTimeMillis);
        RestActions.buildBroadcastShardsHeader(builder, params, totalShards, successfulShards, skippedShards, failedShards, null);
        if (isRunning == false) { // completion status information is only available for a completed search
            builder.field("completion_status", completionStatus.getStatus());
        }
        builder.endObject();
        return builder;
    }

    /**
     * Returns the id of the async search status request.
     */
    public String getId() {
        return id;
    }

    /**
     * Returns {@code true} if the search is still running in the cluster,
     * or {@code false} if the search has been completed.
     */
    public boolean isRunning() {
        return isRunning;
    }

    /**
     * Returns {@code true} if the search results are partial.
     * This could be either because async search hasn't finished yet,
     * or if it finished and some shards have failed.
     */
    public boolean isPartial() {
        return isPartial;
    }

    /**
     * Returns a timestamp when the search tasks started, in milliseconds since epoch.
     */
    public long getStartTime() {
        return startTimeMillis;
    }

    /**
     * Returns a timestamp when the search will be expired, in milliseconds since epoch.
     */
    public long getExpirationTime() {
        return expirationTimeMillis;
    }

    /**
     * Returns the total number of shards the search is executed on.
     */
    public int getTotalShards() {
        return totalShards;
    }

    /**
     * Returns the number of successful shards the search was executed on.
     */
    public int getSuccessfulShards() {
        return successfulShards;
    }

    /**
     * Returns the number of skipped shards due to pre-filtering.
     */
    public int getSkippedShards() {
        return skippedShards;
    }

    /**
     * Returns the number of failed shards the search was executed on.
     */
    public int getFailedShards() {
        return failedShards;
    }

    /**
     * For a completed async search returns the completion status.
     * For a still running async search returns {@code null}.
     */
    public RestStatus getCompletionStatus() {
        return completionStatus;
    }
}
