// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.cosmos.implementation.directconnectivity.rntbd;

import com.azure.cosmos.implementation.directconnectivity.ServerProperties;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.handler.codec.http.HttpResponseStatus;

import java.util.Collections;
import java.util.HashMap;
import java.util.UUID;

import static com.azure.cosmos.implementation.directconnectivity.rntbd.RntbdConstants.CURRENT_PROTOCOL_VERSION;
import static com.azure.cosmos.implementation.directconnectivity.rntbd.RntbdConstants.RntbdContextHeader;
import static com.azure.cosmos.implementation.guava25.base.Preconditions.checkState;

public final class RntbdContext {

    private final UUID activityId;
    private final HttpResponseStatus status;
    private final String clientVersion;
    private final long idleTimeoutInSeconds;
    private final int protocolVersion;
    private final ServerProperties serverProperties;
    private final long unauthenticatedTimeoutInSeconds;

    private RntbdContext(final RntbdResponseStatus responseStatus, final Headers headers) {

        this.activityId = responseStatus.getActivityId();
        this.status = responseStatus.getStatus();

        this.clientVersion = headers.clientVersion.getValue(String.class);
        this.idleTimeoutInSeconds = headers.idleTimeoutInSeconds.getValue(Long.class);
        this.protocolVersion = headers.protocolVersion.getValue(Long.class).intValue();
        this.unauthenticatedTimeoutInSeconds = headers.unauthenticatedTimeoutInSeconds.getValue(Long.class);

        this.serverProperties = new ServerProperties(
            headers.serverAgent.getValue(String.class), headers.serverVersion.getValue(String.class)
        );
    }

    @JsonProperty
    public UUID activityId() {
        return this.activityId;
    }

    @JsonProperty
    public String clientVersion() {
        return this.clientVersion;
    }

    @JsonProperty
    public long idleTimeoutInSeconds() {
        return this.idleTimeoutInSeconds;
    }

    @JsonProperty
    public int protocolVersion() {
        return this.protocolVersion;
    }

    @JsonProperty
    public ServerProperties serverProperties() {
        return this.serverProperties;
    }

    @JsonIgnore
    public String serverVersion() {
        return this.serverProperties.getVersion();
    }

    @JsonIgnore
    public HttpResponseStatus status() {
        return this.status;
    }

    @JsonProperty
    public int getStatusCode() {
        return this.status.code();
    }

    @JsonProperty
    public long getUnauthenticatedTimeoutInSeconds() {
        return this.unauthenticatedTimeoutInSeconds;
    }

    public static RntbdContext decode(final ByteBuf in) {

        in.markReaderIndex();

        final RntbdResponseStatus responseStatus = RntbdResponseStatus.decode(in);
        final int statusCode = responseStatus.getStatusCode();
        final int headersLength = responseStatus.getHeadersLength();

        if (statusCode < 200 || statusCode >= 400) {
            if (!RntbdFramer.canDecodePayload(in, in.readerIndex() + headersLength)) {
                in.resetReaderIndex();
                return null;
            }
        }

        final Headers headers = Headers.decode(in.readSlice(headersLength));

        if (statusCode < 200 || statusCode >= 400) {

            final ObjectNode details = RntbdObjectMapper.readTree(in.readSlice(in.readIntLE()));
            final HashMap<String, Object> map = new HashMap<>(4);

            if (headers.clientVersion.isPresent()) {
                map.put("requiredClientVersion", headers.clientVersion.getValue());
            }

            if (headers.protocolVersion.isPresent()) {
                map.put("requiredProtocolVersion", headers.protocolVersion.getValue());
            }

            if (headers.serverAgent.isPresent()) {
                map.put("serverAgent", headers.serverAgent.getValue());
            }

            if (headers.serverVersion.isPresent()) {
                map.put("serverVersion", headers.serverVersion.getValue());
            }

            throw new RntbdContextException(responseStatus.getStatus(), details, Collections.unmodifiableMap(map));
        }

        return new RntbdContext(responseStatus, headers);
    }

    public void encode(final ByteBuf out) {

        final Headers headers = new Headers(this);
        final int length = RntbdResponseStatus.LENGTH + headers.computeLength();
        final RntbdResponseStatus responseStatus = new RntbdResponseStatus(length, this.status(), this.activityId());

        final int start = out.writerIndex();

        responseStatus.encode(out);
        headers.encode(out);
        headers.release();

        final int end = out.writerIndex();

        checkState(end - start == responseStatus.getLength());
    }

    public static RntbdContext from(final RntbdContextRequest request, final ServerProperties properties, final HttpResponseStatus status) {

        // NOTE TO CODE REVIEWERS
        // ----------------------
        // In its current form this method is meant to enable a limited set of test scenarios. It will be revised as
        // required to support test scenarios as they are developed.

        final Headers headers = new Headers(Unpooled.EMPTY_BUFFER);

        headers.clientVersion.setValue(request.getClientVersion());
        headers.idleTimeoutInSeconds.setValue(0);
        headers.protocolVersion.setValue(CURRENT_PROTOCOL_VERSION);
        headers.serverAgent.setValue(properties.getAgent());
        headers.serverVersion.setValue(properties.getVersion());
        headers.unauthenticatedTimeoutInSeconds.setValue(0);

        final int length = RntbdResponseStatus.LENGTH + headers.computeLength();
        final UUID activityId = request.getActivityId();

        final RntbdResponseStatus responseStatus = new RntbdResponseStatus(length, status, activityId);

        return new RntbdContext(responseStatus, headers);
    }

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

    private static final class Headers extends RntbdTokenStream<RntbdContextHeader> {

        final RntbdToken clientVersion;
        final RntbdToken idleTimeoutInSeconds;
        final RntbdToken protocolVersion;
        final RntbdToken serverAgent;
        final RntbdToken serverVersion;
        final RntbdToken unauthenticatedTimeoutInSeconds;

        private Headers(final RntbdContext context) {
            this(Unpooled.EMPTY_BUFFER);
            this.clientVersion.setValue(context.clientVersion());
            this.idleTimeoutInSeconds.setValue(context.idleTimeoutInSeconds());
            this.protocolVersion.setValue(context.protocolVersion());
            this.serverAgent.setValue(context.serverProperties().getAgent());
            this.serverVersion.setValue(context.serverProperties().getVersion());
            this.unauthenticatedTimeoutInSeconds.setValue(context.unauthenticatedTimeoutInSeconds);
        }

        Headers(final ByteBuf in) {
            super(RntbdContextHeader.set, RntbdContextHeader.map, in);
            this.clientVersion = this.get(RntbdContextHeader.ClientVersion);
            this.idleTimeoutInSeconds = this.get(RntbdContextHeader.IdleTimeoutInSeconds);
            this.protocolVersion = this.get(RntbdContextHeader.ProtocolVersion);
            this.serverAgent = this.get(RntbdContextHeader.ServerAgent);
            this.serverVersion = this.get(RntbdContextHeader.ServerVersion);
            this.unauthenticatedTimeoutInSeconds = this.get(RntbdContextHeader.UnauthenticatedTimeoutInSeconds);
        }

        static Headers decode(final ByteBuf in) {
            final Headers headers = new Headers(in);
            Headers.decode(headers);
            return headers;
        }
    }
}
