/*
 * Copyright (C) 2019 Authlete, Inc.
 *
 * 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.authlete.jaxrs.server.api.backchannel;


import static com.authlete.jaxrs.server.util.ExceptionUtil.internalServerErrorException;
import java.net.URI;
import java.util.Date;
import javax.net.ssl.SSLContext;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import org.glassfish.jersey.client.ClientProperties;
import com.authlete.common.dto.BackchannelAuthenticationCompleteRequest.Result;
import com.authlete.common.dto.BackchannelAuthenticationCompleteResponse;
import com.authlete.common.types.User;
import com.authlete.jaxrs.spi.BackchannelAuthenticationCompleteRequestHandlerSpiAdapter;


/**
 * Implementation of {@link com.authlete.jaxrs.spi.BackchannelAuthenticationCompleteRequestHandlerSpi
 * BackchannelAuthenticationCompleteRequestHandlerSpi} interface which needs to
 * be given to the constructor of {@link com.authlete.jaxrs.BackchannelAuthenticationCompleteRequestHandler
 * BackchannelAuthenticationCompleteRequestHandler}.
 *
 * @author Hideki Ikeda
 */
public class BackchannelAuthenticationCompleteHandlerSpiImpl extends BackchannelAuthenticationCompleteRequestHandlerSpiAdapter
{
    /**
     * The result of end-user authentication and authorization.
     */
    private final Result mResult;


    /**
     * The authenticated user.
     */
    private final User mUser;


    /**
     * The time when the user was authenticated in seconds since Unix epoch.
     */
    private long mUserAuthenticatedAt;


    /**
     * Requested ACRs.
     */
    private String[] mAcrs;


    /**
     * The description of the error.
     */
    private String mErrorDescription;


    /**
     * The URI of a document which describes the error in detail.
     */
    private URI mErrorUri;


    public BackchannelAuthenticationCompleteHandlerSpiImpl(
            Result result, User user, Date userAuthenticatedAt, String[] acrs,
            String errorDescription, URI errorUri)
    {
        // The result of end-user authentication and authorization.
        mResult = result;

        // The end-user.
        mUser = user;

        if (result != Result.AUTHORIZED)
        {
            // The description of the error.
            mErrorDescription = errorDescription;

            // The URI of a document which describes the error in detail.
            mErrorUri = errorUri;

            // The end-user has not authorized the client.
            return;
        }

        // The time at which end-user has been authenticated.
        mUserAuthenticatedAt = (userAuthenticatedAt == null) ? 0 : userAuthenticatedAt.getTime() / 1000L;

        // The requested ACRs.
        mAcrs = acrs;
    }


    @Override
    public Result getResult()
    {
        return mResult;
    }


    @Override
    public String getUserSubject()
    {
        return mUser.getSubject();
    }


    @Override
    public long getUserAuthenticatedAt()
    {
        return mUserAuthenticatedAt;
    }


    @Override
    public String getAcr()
    {
        // Note that this is a dummy implementation. Regardless of whatever
        // the actual authentication was, this implementation returns the
        // first element of the requested ACRs if it is available.
        //
        // Of course, this implementation is not suitable for commercial use.

        if (mAcrs == null || mAcrs.length == 0)
        {
            return null;
        }

        // The first element of the requested ACRs.
        String acr = mAcrs[0];

        if (acr == null || acr.length() == 0)
        {
            return null;
        }

        // Return the first element of the requested ACRs. Again,
        // this implementation is not suitable for commercial use.
        return acr;
    }


    @Override
    public Object getUserClaim(String claimName)
    {
        return mUser.getClaim(claimName, null);
    }


    @Override
    public void sendNotification(BackchannelAuthenticationCompleteResponse info)
    {
        // The URL of the consumption device's notification endpoint.
        URI clientNotificationEndpointUri = info.getClientNotificationEndpoint();

        // The token that is needed for client authentication at the consumption
        // device's notification endpoint.
        String notificationToken = info.getClientNotificationToken();

        // The notification content (JSON) to send to the consumption device.
        String notificationContent = info.getResponseContent();

        // Send the notification to the consumption device's notification endpoint.
        Response response =
                doSendNotification(clientNotificationEndpointUri, notificationToken, notificationContent);

        // The status of the response from the consumption device.
        Status status = Status.fromStatusCode(response.getStatusInfo().getStatusCode());

        // TODO: CIBA specification does not specify how to deal with responses
        // returned from the consumption device in case of error push notification.
        // Then, even in case of error push notification, the current implementation
        // treats the responses as in the case of successful push notification.

        // Check if the "HTTP 200 OK" or "HTTP 204 No Content".
        if (status == Status.OK || status == Status.NO_CONTENT)
        {
            // In this case, the request was successfully processed by the consumption
            // device since the specification says as follows.
            //
            //   CIBA Core spec, 10.2. Ping Callback and 10.3. Push Callback
            //     For valid requests, the Client Notification Endpoint SHOULD
            //     respond with an HTTP 204 No Content.  The OP SHOULD also accept
            //     HTTP 200 OK and any body in the response SHOULD be ignored.
            //
            return;
        }

        if (status.getFamily() == Status.Family.REDIRECTION)
        {
            // HTTP 3xx code. This case must be ignored since the specification
            // says as follows.
            //
            //   CIBA Core spec, 10.2. Ping Callback, 10.3. Push Callback
            //     The Client MUST NOT return an HTTP 3xx code.  The OP MUST
            //     NOT follow redirects.
            //
            return;
        }
    }


    private Response doSendNotification(URI clientNotificationEndpointUri,
            String notificationToken, String notificationContent)
    {
        // A web client to send a notification to the consumption device's notification
        // endpoint.
        Client webClient = createClient();

        try
        {
            // Send the notification to the consumption device..
            return webClient.target(clientNotificationEndpointUri).request()
                    // CIBA Core says "The OP MUST NOT follow redirects."
                    .property(ClientProperties.FOLLOW_REDIRECTS, Boolean.FALSE)
                    .header(HttpHeaders.AUTHORIZATION, "Bearer " + notificationToken)
                    .post(Entity.json(notificationContent));
        }
        catch (Throwable t)
        {
            // Failed to send the notification to the consumption device.
            throw internalServerErrorException(
                    t.getMessage() + ": Failed to send the notification to the consumption device");
        }
        finally
        {
            // Close the web client.
            webClient.close();
        }
    }


    @Override
    public String getErrorDescription()
    {
        return mErrorDescription;
    }


    @Override
    public URI getErrorUri()
    {
        return mErrorUri;
    }


    private Client createClient()
    {
        // SSLContext's for older TLS versions ("TLSv1" and "TLSv1.1") may not
        // include any FAPI cipher suites. Here we create an SSLContext with
        // "TLSv1.2" whose getDefaultSSLParameters().getCipherSuites() probably
        // includes FAPI cipher suites.
        SSLContext sc = createSslContext("TLSv1.2");

        return ClientBuilder.newBuilder().sslContext(sc).build();
    }


    private SSLContext createSslContext(String protocol)
    {
        try
        {
            // Get an SSL context for the protocol.
            SSLContext sc = SSLContext.getInstance(protocol);

            // Initialize the SSL context.
            sc.init(null, null, null);

            return sc;
        }
        catch (Exception e)
        {
            throw internalServerErrorException(
                    "Failed to get an SSLContext for " + protocol + ": " + e.getMessage());
        }
    }
}