/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you 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 org.apache.cxf.sts.operation;

import java.security.Principal;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.xml.bind.JAXBElement;

import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.sts.QNameConstants;
import org.apache.cxf.sts.RealmParser;
import org.apache.cxf.sts.STSConstants;
import org.apache.cxf.sts.event.STSValidateFailureEvent;
import org.apache.cxf.sts.event.STSValidateSuccessEvent;
import org.apache.cxf.sts.request.ReceivedToken;
import org.apache.cxf.sts.request.ReceivedToken.STATE;
import org.apache.cxf.sts.request.RequestRequirements;
import org.apache.cxf.sts.request.TokenRequirements;
import org.apache.cxf.sts.token.provider.TokenProvider;
import org.apache.cxf.sts.token.provider.TokenProviderParameters;
import org.apache.cxf.sts.token.provider.TokenProviderResponse;
import org.apache.cxf.sts.token.provider.TokenReference;
import org.apache.cxf.sts.token.validator.TokenValidatorParameters;
import org.apache.cxf.sts.token.validator.TokenValidatorResponse;
import org.apache.cxf.ws.security.sts.provider.STSException;
import org.apache.cxf.ws.security.sts.provider.model.LifetimeType;
import org.apache.cxf.ws.security.sts.provider.model.RequestSecurityTokenResponseType;
import org.apache.cxf.ws.security.sts.provider.model.RequestSecurityTokenType;
import org.apache.cxf.ws.security.sts.provider.model.RequestedReferenceType;
import org.apache.cxf.ws.security.sts.provider.model.RequestedSecurityTokenType;
import org.apache.cxf.ws.security.sts.provider.model.StatusType;
import org.apache.cxf.ws.security.sts.provider.operation.ValidateOperation;
import org.apache.wss4j.common.ext.WSSecurityException;

/**
 * An implementation of the ValidateOperation interface.
 */
public class TokenValidateOperation extends AbstractOperation implements ValidateOperation {

    private static final Logger LOG = LogUtils.getL7dLogger(TokenValidateOperation.class);
   
    public RequestSecurityTokenResponseType validate(
        RequestSecurityTokenType request, 
        Principal principal,
        Map<String, Object> messageContext
    ) {
        long start = System.currentTimeMillis();
        TokenValidatorParameters validatorParameters = new TokenValidatorParameters();
        
        try {
            RequestRequirements requestRequirements = parseRequest(request, messageContext);
            
            TokenRequirements tokenRequirements = requestRequirements.getTokenRequirements();
            
            validatorParameters.setStsProperties(stsProperties);
            validatorParameters.setPrincipal(principal);
            validatorParameters.setMessageContext(messageContext);
            validatorParameters.setTokenStore(getTokenStore());
            
            //validatorParameters.setKeyRequirements(keyRequirements);
            validatorParameters.setTokenRequirements(tokenRequirements);
            
            ReceivedToken validateTarget = tokenRequirements.getValidateTarget();
            if (validateTarget == null || validateTarget.getToken() == null) {
                throw new STSException("No element presented for validation", STSException.INVALID_REQUEST);
            }
            validatorParameters.setToken(validateTarget);
            
            if (tokenRequirements.getTokenType() == null) {
                tokenRequirements.setTokenType(STSConstants.STATUS);
                LOG.fine(
                    "Received TokenType is null, falling back to default token type: " 
                    + STSConstants.STATUS
                );
            }
            
            // Get the realm of the request
            String realm = null;
            if (stsProperties.getRealmParser() != null) {
                RealmParser realmParser = stsProperties.getRealmParser();
                realm = realmParser.parseRealm(messageContext);
            }
            validatorParameters.setRealm(realm);
            
            TokenValidatorResponse tokenResponse = validateReceivedToken(
                    principal, messageContext, realm, tokenRequirements, validateTarget);
            
            if (tokenResponse == null) {
                LOG.fine("No Token Validator has been found that can handle this token");
                tokenResponse = new TokenValidatorResponse();
                validateTarget.setState(STATE.INVALID);
                tokenResponse.setToken(validateTarget);
            }
            
            //
            // Create a new token (if requested)
            //
            TokenProviderResponse tokenProviderResponse = null;
            String tokenType = tokenRequirements.getTokenType();
            if (tokenResponse.getToken().getState() == STATE.VALID 
                && !STSConstants.STATUS.equals(tokenType)) {
                TokenProviderParameters providerParameters = 
                     createTokenProviderParameters(requestRequirements, principal, messageContext);
                
                processValidToken(providerParameters, validateTarget, tokenResponse);
                
                // Check if the requested claims can be handled by the configured claim handlers
                providerParameters.setClaimsManager(claimsManager);
                
                Map<String, Object> additionalProperties = tokenResponse.getAdditionalProperties();
                if (additionalProperties != null) {
                    providerParameters.setAdditionalProperties(additionalProperties);
                }
                realm = providerParameters.getRealm();
                for (TokenProvider tokenProvider : tokenProviders) {
                    boolean canHandle = false;
                    if (realm == null) {
                        canHandle = tokenProvider.canHandleToken(tokenType);
                    } else {
                        canHandle = tokenProvider.canHandleToken(tokenType, realm);
                    }
                    if (canHandle) {
                        try {
                            tokenProviderResponse = tokenProvider.createToken(providerParameters);
                        } catch (STSException ex) {
                            LOG.log(Level.WARNING, "", ex);
                            throw ex;
                        } catch (RuntimeException ex) {
                            LOG.log(Level.WARNING, "", ex);
                            throw new STSException(
                                "Error in providing a token", ex, STSException.REQUEST_FAILED
                            );
                        }
                        break;
                    }
                }
                if (tokenProviderResponse == null || tokenProviderResponse.getToken() == null) {
                    LOG.fine("No Token Provider has been found that can handle this token");
                    throw new STSException(
                        "No token provider found for requested token type: " + tokenType, 
                        STSException.REQUEST_FAILED
                    );
                }
            }
            
            // prepare response
            try {
                RequestSecurityTokenResponseType response =
                    createResponse(tokenResponse, tokenProviderResponse, tokenRequirements);
                STSValidateSuccessEvent event = new STSValidateSuccessEvent(validatorParameters,
                        System.currentTimeMillis() - start);
                publishEvent(event);
                return response;
            } catch (Throwable ex) {
                LOG.log(Level.WARNING, "", ex);
                throw new STSException("Error in creating the response", ex, STSException.REQUEST_FAILED);
            }
            
        } catch (RuntimeException ex) {
            STSValidateFailureEvent event = new STSValidateFailureEvent(validatorParameters,
                                                              System.currentTimeMillis() - start, ex);
            publishEvent(event);
            throw ex;
        }            
    }
    
    private RequestSecurityTokenResponseType createResponse(
        TokenValidatorResponse tokenResponse,
        TokenProviderResponse tokenProviderResponse,
        TokenRequirements tokenRequirements
    ) throws WSSecurityException {
        RequestSecurityTokenResponseType response = 
            QNameConstants.WS_TRUST_FACTORY.createRequestSecurityTokenResponseType();

        String context = tokenRequirements.getContext();
        if (context != null) {
            response.setContext(context);
        }
        
        // TokenType
        boolean valid = tokenResponse.getToken().getState() == STATE.VALID;
        String tokenType = tokenRequirements.getTokenType();
        if (valid || STSConstants.STATUS.equals(tokenType)) {
            JAXBElement<String> jaxbTokenType = 
                QNameConstants.WS_TRUST_FACTORY.createTokenType(tokenType);
            response.getAny().add(jaxbTokenType);
        }
        
        // Status
        StatusType statusType = QNameConstants.WS_TRUST_FACTORY.createStatusType();
        if (valid) {
            statusType.setCode(STSConstants.VALID_CODE);
            statusType.setReason(STSConstants.VALID_REASON);
        } else {
            statusType.setCode(STSConstants.INVALID_CODE);
            statusType.setReason(STSConstants.INVALID_REASON);
        }
        JAXBElement<StatusType> status = QNameConstants.WS_TRUST_FACTORY.createStatus(statusType);
        response.getAny().add(status);
        
        // RequestedSecurityToken
        if (valid && !STSConstants.STATUS.equals(tokenType) && tokenProviderResponse != null 
            && tokenProviderResponse.getToken() != null) {
            RequestedSecurityTokenType requestedTokenType = 
                QNameConstants.WS_TRUST_FACTORY.createRequestedSecurityTokenType();
            JAXBElement<RequestedSecurityTokenType> requestedToken = 
                QNameConstants.WS_TRUST_FACTORY.createRequestedSecurityToken(requestedTokenType);
            tokenWrapper.wrapToken(tokenProviderResponse.getToken(), requestedTokenType);
            response.getAny().add(requestedToken);
            
            // Lifetime
            LifetimeType lifetime = 
                createLifetime(tokenProviderResponse.getCreated(), tokenProviderResponse.getExpires());
            JAXBElement<LifetimeType> lifetimeType =
                QNameConstants.WS_TRUST_FACTORY.createLifetime(lifetime);
            response.getAny().add(lifetimeType);
            
            if (returnReferences) {
                // RequestedAttachedReference
                TokenReference attachedReference = tokenProviderResponse.getAttachedReference();
                RequestedReferenceType requestedAttachedReferenceType = null;
                if (attachedReference != null) {
                    requestedAttachedReferenceType = createRequestedReference(attachedReference, true);
                } else {
                    requestedAttachedReferenceType = 
                        createRequestedReference(
                            tokenProviderResponse.getTokenId(), tokenRequirements.getTokenType(), true
                        );
                }
    
                JAXBElement<RequestedReferenceType> requestedAttachedReference = 
                    QNameConstants.WS_TRUST_FACTORY.createRequestedAttachedReference(
                        requestedAttachedReferenceType
                    );
                response.getAny().add(requestedAttachedReference);
    
                // RequestedUnattachedReference
                TokenReference unAttachedReference = tokenProviderResponse.getUnAttachedReference();
                RequestedReferenceType requestedUnattachedReferenceType = null;
                if (unAttachedReference != null) {
                    requestedUnattachedReferenceType = 
                        createRequestedReference(unAttachedReference, false);
                } else {
                    requestedUnattachedReferenceType = 
                        createRequestedReference(
                            tokenProviderResponse.getTokenId(), tokenRequirements.getTokenType(), false
                        );
                }
                
                JAXBElement<RequestedReferenceType> requestedUnattachedReference = 
                    QNameConstants.WS_TRUST_FACTORY.createRequestedUnattachedReference(
                        requestedUnattachedReferenceType
                    );
                response.getAny().add(requestedUnattachedReference);
            }
        }
        
        return response;
    }
    
    
}
