/*
 * Copyright (c) 2019 the Eclipse Milo Authors
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 */

package org.eclipse.milo.opcua.stack.client.transport.http;

import java.util.List;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageCodec;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.util.AttributeKey;
import org.eclipse.milo.opcua.stack.client.UaStackClient;
import org.eclipse.milo.opcua.stack.client.UaStackClientConfig;
import org.eclipse.milo.opcua.stack.client.transport.UaTransportRequest;
import org.eclipse.milo.opcua.stack.core.StatusCodes;
import org.eclipse.milo.opcua.stack.core.UaException;
import org.eclipse.milo.opcua.stack.core.serialization.OpcUaBinaryStreamDecoder;
import org.eclipse.milo.opcua.stack.core.serialization.OpcUaBinaryStreamEncoder;
import org.eclipse.milo.opcua.stack.core.serialization.UaResponseMessage;
import org.eclipse.milo.opcua.stack.core.transport.TransportProfile;
import org.eclipse.milo.opcua.stack.core.types.structured.EndpointDescription;
import org.eclipse.milo.opcua.stack.core.util.EndpointUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class OpcClientHttpCodec extends MessageToMessageCodec<HttpResponse, UaTransportRequest> {

    private static final AttributeKey<UaTransportRequest> KEY_PENDING_REQUEST =
        AttributeKey.newInstance("pendingRequest");

    private static final String UABINARY_CONTENT_TYPE =
        HttpHeaderValues.APPLICATION_OCTET_STREAM.toString();

    private final Logger logger = LoggerFactory.getLogger(getClass());

    private final UaStackClientConfig config;
    private final EndpointDescription endpoint;
    private final TransportProfile transportProfile;

    private final UaStackClient client;

    OpcClientHttpCodec(UaStackClient client) {
        this.client = client;

        config = client.getConfig();
        endpoint = config.getEndpoint();
        transportProfile = TransportProfile.fromUri(endpoint.getTransportProfileUri());
    }

    @Override
    protected void encode(
        ChannelHandlerContext ctx,
        UaTransportRequest transportRequest,
        List<Object> out) throws Exception {

        logger.debug("encoding: " + transportRequest.getRequest());

        ctx.channel().attr(KEY_PENDING_REQUEST).set(transportRequest);

        ByteBuf content = Unpooled.buffer();

        switch (transportProfile) {
            case HTTPS_UABINARY: {
                OpcUaBinaryStreamEncoder encoder =
                    new OpcUaBinaryStreamEncoder(client.getStaticSerializationContext());
                encoder.setBuffer(content);
                encoder.writeMessage(null, transportRequest.getRequest());
                break;
            }

            case HTTPS_UAXML: {
                // TODO put document into a SOAP message.
                throw new UaException(StatusCodes.Bad_InternalError,
                    "no encoder for transport: " + transportProfile);
            }

            default:
                throw new UaException(StatusCodes.Bad_InternalError,
                    "no encoder for transport: " + transportProfile);
        }

        String endpointUrl = endpoint.getEndpointUrl();

        FullHttpRequest httpRequest = new DefaultFullHttpRequest(
            HttpVersion.HTTP_1_1,
            HttpMethod.POST,
            EndpointUtil.getPath(endpointUrl),
            content
        );

        httpRequest.headers().set(HttpHeaderNames.HOST, EndpointUtil.getHost(endpointUrl));
        httpRequest.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
        httpRequest.headers().set(HttpHeaderNames.CONTENT_TYPE, UABINARY_CONTENT_TYPE);
        httpRequest.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes());
        httpRequest.headers().set("OPCUA-SecurityPolicy", config.getEndpoint().getSecurityPolicyUri());

        out.add(httpRequest);
    }

    @Override
    protected void decode(
        ChannelHandlerContext ctx,
        HttpResponse httpResponse,
        List<Object> out) throws Exception {

        logger.trace("channelRead0: " + httpResponse);

        UaTransportRequest transportRequest = ctx.channel()
            .attr(KEY_PENDING_REQUEST)
            .getAndSet(null);

        if (httpResponse instanceof FullHttpResponse) {
            String contentType = httpResponse.headers().get(HttpHeaderNames.CONTENT_TYPE);

            FullHttpResponse fullHttpResponse = (FullHttpResponse) httpResponse;
            ByteBuf content = fullHttpResponse.content();

            UaResponseMessage responseMessage;

            switch (transportProfile) {
                case HTTPS_UABINARY: {
                    if (!UABINARY_CONTENT_TYPE.equalsIgnoreCase(contentType)) {
                        throw new UaException(StatusCodes.Bad_DecodingError,
                            "unexpected content-type: " + contentType);
                    }

                    OpcUaBinaryStreamDecoder decoder =
                        new OpcUaBinaryStreamDecoder(client.getStaticSerializationContext());
                    decoder.setBuffer(content);
                    responseMessage = (UaResponseMessage) decoder.readMessage(null);
                    break;
                }

                case HTTPS_UAXML: {
                    // TODO extract document from SOAP message body
                    throw new UaException(StatusCodes.Bad_InternalError,
                        "no decoder for transport: " + transportProfile);
                }

                default:
                    throw new UaException(StatusCodes.Bad_InternalError,
                        "no decoder for transport: " + transportProfile);
            }

            transportRequest.getFuture().complete(responseMessage);
        } else {
            HttpResponseStatus status = httpResponse.status();

            if (status.equals(HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE)) {
                transportRequest.getFuture().completeExceptionally(
                    new UaException(StatusCodes.Bad_ResponseTooLarge));
            } else {
                transportRequest.getFuture().completeExceptionally(
                    new UaException(StatusCodes.Bad_UnexpectedError,
                        String.format("%s: %s", status.code(), status.reasonPhrase())));
            }
        }
    }

}
