/*
 * Copyright 2018 Okta, 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.okta.authn.sdk.example.resources.authn;

import com.okta.authn.sdk.AuthenticationException;
import com.okta.authn.sdk.AuthenticationFailureException;
import com.okta.authn.sdk.client.AuthenticationClient;
import com.okta.authn.sdk.example.ExampleAuthenticationStateHandler;
import com.okta.authn.sdk.example.models.authn.Factor;
import com.okta.authn.sdk.example.views.authn.ChangePasswordView;
import com.okta.authn.sdk.example.views.authn.LoginView;
import com.okta.authn.sdk.example.views.authn.MfaActivateView;
import com.okta.authn.sdk.example.views.authn.MfaEnrollSelectionView;
import com.okta.authn.sdk.example.views.authn.MfaEnrollView;
import com.okta.authn.sdk.example.views.authn.MfaRequiredView;
import com.okta.authn.sdk.example.views.authn.MfaVerifyView;
import com.okta.authn.sdk.example.views.authn.PasswordRecoveryView;
import com.okta.authn.sdk.example.views.authn.PasswordResetView;
import com.okta.authn.sdk.example.views.authn.RecoveryChallengeView;
import com.okta.authn.sdk.example.views.authn.RecoveryView;
import com.okta.authn.sdk.example.views.authn.UnlockAccountRecoveryView;
import com.okta.authn.sdk.example.views.authn.UnlockAccountView;
import com.okta.authn.sdk.resource.ActivateFactorRequest;
import com.okta.authn.sdk.resource.ActivatePassCodeFactorRequest;
import com.okta.authn.sdk.resource.AuthenticationResponse;
import com.okta.authn.sdk.resource.AuthenticationStatus;
import com.okta.authn.sdk.resource.FactorEnrollRequest;
import com.okta.authn.sdk.resource.VerifyFactorRequest;
import com.okta.authn.sdk.resource.VerifyPassCodeFactorRequest;
import com.okta.authn.sdk.resource.VerifyRecoveryRequest;
import com.okta.sdk.resource.user.factor.FactorProvider;
import com.okta.sdk.resource.user.factor.FactorType;
import com.okta.sdk.resource.user.factor.SmsFactorProfile;

import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Form;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import static com.okta.authn.sdk.example.ExampleAuthenticationStateHandler.getPreviousAuthResult;

@Path("/login")
@Produces(MediaType.TEXT_HTML)
public class LoginResource {

    private final AuthenticationClient authenticationClient;

    @Inject
    public LoginResource(AuthenticationClient authenticationClient) {
        this.authenticationClient = authenticationClient;
    }

    @GET
    public LoginView getLoginView(@Context HttpServletRequest request) {
        return new LoginView(Optional.empty());
    }

    @GET
    @Path("/change-password")
    public ChangePasswordView getChangePasswordView() {
        return new ChangePasswordView();
    }

    @GET
    @Path("/recover")
    public PasswordRecoveryView getPasswordRecoveryView() {
        return new PasswordRecoveryView();
    }

    @GET
    @Path("/reset")
    public PasswordResetView getPasswordResetView() {
        return new PasswordResetView();
    }

    @GET
    @Path("/unlock")
    public UnlockAccountView getUnlockAccountView() {
        return new UnlockAccountView();
    }

    @GET
    @Path("/unlock/recovery")
    public UnlockAccountRecoveryView getUnlockAccountRecoveryView() {
        return new UnlockAccountRecoveryView();
    }

    @GET
    @Path("/mfa/enroll")
    public MfaEnrollSelectionView getMfaEnrollSelectionView() {

        List<Factor> factors = getPreviousAuthResult().getFactors().stream()
            .map(authFactor -> {
                    String shortType = MfaVerifyView.relativeLink(authFactor);
                    return new Factor(authFactor.getId(),
                                      shortType,
                                      authFactor.getProvider().name(),
                                      authFactor.getVendorName(),
                                      authFactor.getProfile(),
                                      "/login/mfa/enroll/" + shortType);})
            .collect(Collectors.toList());
        return new MfaEnrollSelectionView(factors);
    }

    @GET
    @Path("/mfa/enroll/{factorType}")
    public MfaEnrollView getMfaEnrollView(@PathParam("factorType") String factorType) {
        return new MfaEnrollView(getFactor(factorType, getPreviousAuthResult()));
    }

    @GET
    @Path("/mfa/activate/{factorType}")
    public MfaActivateView getMfaActivateView() {
        return new MfaActivateView(getPreviousAuthResult().getFactors().get(0));
    }

    @GET
    @Path("/mfa")
    public MfaRequiredView getRequireMfaView() {

        // grab previous AuthenticationStatus
        List<Factor> factors = getPreviousAuthResult().getFactors().stream()
            .map(authFactor -> {
                    String shortType = MfaVerifyView.relativeLink(authFactor);
                    return new Factor(authFactor.getId(),
                                      shortType,
                                      authFactor.getProvider().name(),
                                      authFactor.getVendorName(),
                                      authFactor.getProfile(),
                                      "/login/mfa/verify/" + shortType);})
            .collect(Collectors.toList());

        return new MfaRequiredView(factors);
    }

    @GET
    @Path("/mfa/verify/{type}")
    public MfaVerifyView getVerifyMfaView(@PathParam("type") String type) throws AuthenticationException {

        AuthenticationResponse authenticationResponse = getPreviousAuthResult();
        com.okta.authn.sdk.resource.Factor factor = getFactor(type, authenticationResponse);

        if (factor.getType().equals(FactorType.TOKEN_SOFTWARE_TOTP)) {
            return new MfaVerifyView(factor);
        } else {
            return new MfaVerifyView(challengeFactor(factor, authenticationResponse));
        }
    }

    @GET
    @Path("/mfa/resend/{type}")
    public MfaVerifyView getResendVerifyMfaView(@PathParam("type") String type) {

        AuthenticationResponse authenticationResponse = getPreviousAuthResult();
        com.okta.authn.sdk.resource.Factor factor = getFactor(type, authenticationResponse);
        return new MfaVerifyView(factor);
    }

    @POST
    public Response doLogin(@FormParam("username") String username,
                            @FormParam("password") String password) throws AuthenticationException {

        char[] pass = password != null ? password.toCharArray() : null;

        try {
            authenticationClient.authenticate(username, pass, null, new ExampleAuthenticationStateHandler());
            // the state handler will redirect
        } catch (AuthenticationFailureException e) {
            return Response.ok(new LoginView(Optional.of(e))).build();
        }
        return null;
    }

    @POST
    @Path("/reset")
    public void resetPassword(@FormParam("newPassword") String newPassword) throws AuthenticationException {
        authenticationClient.resetPassword(newPassword.toCharArray(),
                                            getPreviousAuthResult().getStateToken(),
                                            new ExampleAuthenticationStateHandler());
    }

    @POST
    @Path("/change-password")
    public void changePassword(@FormParam("oldPassword") String oldPassword,
                               @FormParam("newPassword") String newPassword) throws AuthenticationException {

        authenticationClient.changePassword(oldPassword.toCharArray(),
                                            newPassword.toCharArray(),
                                            getPreviousAuthResult().getStateToken(),
                                            new ExampleAuthenticationStateHandler());
    }

    @POST
    @Path("/recover")
    public RecoveryChallengeView recoverPassword(@FormParam("username") String username,
                                                 @FormParam("factor") String factorType) throws AuthenticationException {
        authenticationClient.recoverPassword(username, FactorType.valueOf(factorType), null, new ExampleAuthenticationStateHandler());
        return new RecoveryChallengeView();
    }

    @POST
    @Path("/unlock")
    public void unlockAccount(@FormParam("username") String username,
                              @FormParam("factor") String factorType) throws AuthenticationException {
        authenticationClient.unlockAccount(username, FactorType.valueOf(factorType), null, new ExampleAuthenticationStateHandler());
    }

    @POST
    @Path("/unlock/recovery")
    public void unlockAccountChallenge(@FormParam("passCode") String passCode) throws AuthenticationException {
        AuthenticationResponse previousAuthResult = getPreviousAuthResult();
        VerifyRecoveryRequest request = authenticationClient.instantiate(VerifyRecoveryRequest.class)
                .setStateToken(previousAuthResult.getStateToken())
                .setPassCode(passCode);
        authenticationClient.verifyUnlockAccount(previousAuthResult.getFactors().get(0).getType(), request, new ExampleAuthenticationStateHandler());
    }

    @GET
    @Path("/recovery")
    public RecoveryView getRecoveryView() {
        AuthenticationResponse previousAuthResult = getPreviousAuthResult();
        String question = previousAuthResult.getUser().getRecoveryQuestion().get("question");
        return new RecoveryView(question);
    }

    @POST
    @Path("/recovery")
    public void recoveryWithAnswer(@FormParam("answer") String answer) throws AuthenticationException {
        AuthenticationResponse previousAuthResult = getPreviousAuthResult();
        authenticationClient.answerRecoveryQuestion(answer,
                                                    previousAuthResult.getStateToken(),
                                                    new ExampleAuthenticationStateHandler());
    }

    @POST
    @Path("/recover/verify")
    public void recoverChallenge(@FormParam("passCode") String passCode) throws AuthenticationException {
        AuthenticationResponse previousAuthResult = getPreviousAuthResult();
        VerifyFactorRequest request = authenticationClient.instantiate(VerifyPassCodeFactorRequest.class)
                                                            .setPassCode(passCode)
                                                            .setStateToken(previousAuthResult.getStateToken());
        authenticationClient.verifyFactor(previousAuthResult.getFactors().get(0).getId(), request, new ExampleAuthenticationStateHandler());
    }

    @POST
    @Path("/mfa/verify/{type}")
    public void verifyMfa(@PathParam("type") String type,
                          @FormParam("clientData") String clientData,
                          @FormParam("signatureData") String signatureData,
                          @FormParam("passCode") String passCode) throws AuthenticationException {

        AuthenticationResponse previousAuthResult = getPreviousAuthResult();
        com.okta.authn.sdk.resource.Factor factor = getFactor(type, previousAuthResult);

        VerifyFactorRequest request = authenticationClient.instantiate(VerifyPassCodeFactorRequest.class)
                                                            .setPassCode(passCode)
                                                            .setStateToken(previousAuthResult.getStateToken());

        //TODO  this or the above method is likely wrong
        authenticationClient.verifyFactor(factor.getId(), request, new ExampleAuthenticationStateHandler());
    }

    @POST
    @Path("/mfa/resend/{type}")
    public MfaVerifyView verifyMfa(@PathParam("type") String type) throws AuthenticationException {

        AuthenticationResponse previousAuthResult = getPreviousAuthResult();
        com.okta.authn.sdk.resource.Factor factor = getFactor(type, previousAuthResult);
        authenticationClient.resendVerifyFactor(factor.getId(), previousAuthResult.getStateToken(), new ExampleAuthenticationStateHandler());
        return new MfaVerifyView(factor);
    }

    @POST
    @Path("/mfa/activate/{factorType}")
    public void activateMfa(@FormParam("passCode") String passCode) throws AuthenticationException {

        AuthenticationResponse previousAuthResult = getPreviousAuthResult();
        String factorId = previousAuthResult.getFactors().get(0).getId();

        ActivateFactorRequest request = authenticationClient.instantiate(ActivatePassCodeFactorRequest.class)
                        .setPassCode(passCode)
                        .setStateToken(previousAuthResult.getStateToken());

        authenticationClient.activateFactor(factorId, request, new ExampleAuthenticationStateHandler());
    }

    @POST
    @Path("/mfa/activate/{factorType}/resend")
    public void resendActivateMfa(@PathParam("factorType") String type) throws AuthenticationException {

        AuthenticationResponse previousAuthResult = getPreviousAuthResult();
        com.okta.authn.sdk.resource.Factor factor = getFactor(type, previousAuthResult);
        authenticationClient.resendActivateFactor(factor.getId(), previousAuthResult.getStateToken(), new ExampleAuthenticationStateHandler());
    }

    @POST
    @Path("/mfa/enroll/{factorType}")
    public void enrollMfa(@PathParam("factorType") String factorType, Form form) throws AuthenticationException {
        FactorEnrollRequest request = authenticationClient.instantiate(FactorEnrollRequest.class)
                .setProvider(FactorProvider.OKTA)
                .setStateToken(getPreviousAuthResult().getStateToken())
                .setFactorType(MfaVerifyView.fromRelativeLink(factorType))
                .setFactorProfile(authenticationClient.instantiate(SmsFactorProfile.class)
                    .setPhoneNumber(form.asMap().getFirst("phoneNumber")));
        authenticationClient.enrollFactor(request, new ExampleAuthenticationStateHandler());
    }

    private com.okta.authn.sdk.resource.Factor getFactor(String type, AuthenticationResponse authenticationResponse) {

        FactorType oktaType = MfaVerifyView.fromRelativeLink(type);
        return authenticationResponse.getFactors().stream()
                .filter(it -> it.getType().equals(oktaType))
                .findFirst().get();
    }

    private com.okta.authn.sdk.resource.Factor challengeFactor(com.okta.authn.sdk.resource.Factor factor, AuthenticationResponse authenticationResponse) throws AuthenticationException {

        AuthenticationResponse challengeResult = authenticationClient.challengeFactor(factor.getId(), authenticationResponse.getStateToken(), new ExampleAuthenticationStateHandler());

        // check the response type
        if (!challengeResult.getStatus().equals(AuthenticationStatus.MFA_CHALLENGE)) {
            throw new IllegalStateException("Challenge Result is empty, and other state was not triggered");
        }

        return challengeResult.getFactors().get(0);
    }
}