/**
 * 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.fediz.service.idp.beans;

import java.io.IOException;
import java.io.StringReader;
import java.security.cert.X509Certificate;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.xml.namespace.QName;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.stream.XMLStreamException;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import org.apache.cxf.Bus;
import org.apache.cxf.BusFactory;
import org.apache.cxf.binding.soap.SoapFault;
import org.apache.cxf.fediz.core.FederationConstants;
import org.apache.cxf.fediz.core.exception.ProcessingException;
import org.apache.cxf.fediz.core.exception.ProcessingException.TYPE;
import org.apache.cxf.fediz.core.util.DOMUtils;
import org.apache.cxf.fediz.service.idp.IdpSTSClient;
import org.apache.cxf.fediz.service.idp.domain.Application;
import org.apache.cxf.fediz.service.idp.domain.Idp;
import org.apache.cxf.fediz.service.idp.domain.RequestClaim;
import org.apache.cxf.fediz.service.idp.util.LocalServerResolver;
import org.apache.cxf.fediz.service.idp.util.WebUtils;
import org.apache.cxf.staxutils.W3CDOMStreamWriter;
import org.apache.cxf.ws.security.tokenstore.SecurityToken;
import org.apache.cxf.ws.security.trust.STSClient;
import org.apache.cxf.ws.security.trust.STSUtils;
import org.apache.wss4j.dom.WSConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.webflow.execution.RequestContext;

/**
 * This class is responsible to ask for Security Tokens to STS.
 */

public class STSClientAction {

    private static final String HTTP_SCHEMAS_XMLSOAP_ORG_WS_2005_05_IDENTITY =
            "http://schemas.xmlsoap.org/ws/2005/05/identity";

    private static final String HTTP_DOCS_OASIS_OPEN_ORG_WS_SX_WS_TRUST_200512_BEARER =
            "http://docs.oasis-open.org/ws-sx/ws-trust/200512/Bearer";

    private static final String HTTP_DOCS_OASIS_OPEN_ORG_WS_SX_WS_TRUST_200512_PUBLICKEY =
            "http://docs.oasis-open.org/ws-sx/ws-trust/200512/PublicKey";

    private static final String HTTP_WWW_W3_ORG_2005_08_ADDRESSING = "http://www.w3.org/2005/08/addressing";

    private static final String HTTP_DOCS_OASIS_OPEN_ORG_WS_SX_WS_TRUST_200512 =
            "http://docs.oasis-open.org/ws-sx/ws-trust/200512/";

    private static final String HTTP_SCHEMAS_XMLSOAP_ORG_WS_2005_02_TRUST =
            "http://schemas.xmlsoap.org/ws/2005/02/trust";

    private static final String SECURITY_TOKEN_SERVICE = "SecurityTokenService";

    private static final Logger LOG = LoggerFactory.getLogger(STSClientAction.class);

    protected String namespace = HTTP_DOCS_OASIS_OPEN_ORG_WS_SX_WS_TRUST_200512;

    protected String wsdlLocation;

    protected String wsdlEndpoint;

    protected String wsdlService = SECURITY_TOKEN_SERVICE;

    protected String tokenType = WSConstants.WSS_SAML2_TOKEN_TYPE;

    protected Map<String, Object> properties;

    protected boolean use200502Namespace;

    protected int ttl = 1800;

    protected Bus bus;

    private boolean isPortSet;

    private String keyType = HTTP_DOCS_OASIS_OPEN_ORG_WS_SX_WS_TRUST_200512_BEARER;

    private String customSTSParameter;

    public String getWsdlLocation() {
        return wsdlLocation;
    }

    public void setWsdlLocation(String wsdlLocation) {
        this.wsdlLocation = wsdlLocation;
        isPortSet = !LocalServerResolver.isLocal(wsdlLocation);
        if (!isPortSet) {
            LOG.info("Port is 0 for 'wsdlLocation'. Port evaluated when processing first request.");
        }
    }

    public String getWsdlEndpoint() {
        return wsdlEndpoint;
    }

    public void setWsdlEndpoint(String wsdlEndpoint) {
        this.wsdlEndpoint = wsdlEndpoint;
    }

    public String getWsdlService() {
        return wsdlService;
    }

    public void setWsdlService(String wsdlService) {
        this.wsdlService = wsdlService;
    }

    public String getNamespace() {
        return namespace;
    }

    public void setNamespace(String namespace) {
        this.namespace = namespace;
    }

    public Bus getBus() {
        // do not store a referance to the default bus
        return (bus != null) ? bus : BusFactory.getDefaultBus();
    }

    public void setBus(Bus bus) {
        this.bus = bus;
    }

    public String getTokenType() {
        return tokenType;
    }

    public void setTokenType(String tokenType) {
        this.tokenType = tokenType;
    }

    public int getTtl() {
        return ttl;
    }

    public void setTtl(int ttl) {
        this.ttl = ttl;
    }

    public String getCustomSTSParameter() {
        return customSTSParameter;
    }

    public void setCustomSTSParameter(String customSTSParameter) {
        this.customSTSParameter = customSTSParameter;
    }

    /**
     * @param context the webflow request context
     * @param realm The client/application realm
     * @return a RP security token
     * @throws Exception
     */
    public Element submit(RequestContext context, String realm, String homeRealm) throws Exception {

        SecurityToken idpToken = getSecurityToken(context, homeRealm);
        if (idpToken == null || idpToken.getToken() == null) {
            LOG.warn("No IdPToken is found");
            throw new ProcessingException(TYPE.BAD_REQUEST);
        }

        Bus cxfBus = getBus();
        Idp idpConfig = (Idp)WebUtils.getAttributeFromFlowScope(context, "idpConfig");

        IdpSTSClient sts = new IdpSTSClient(cxfBus);
        sts.setAddressingNamespace(HTTP_WWW_W3_ORG_2005_08_ADDRESSING);

        Application serviceConfig = idpConfig.findApplication(realm);
        if (serviceConfig == null) {
            LOG.warn("No service config found for " + realm);
            throw new ProcessingException(TYPE.BAD_REQUEST);
        }

        // Parse wreq parameter - we only support parsing TokenType and KeyType for now
        String wreq = (String)WebUtils.getAttributeFromFlowScope(context, FederationConstants.PARAM_REQUEST);
        String stsTokenType = null;
        String stsKeyType = keyType;
        if (wreq != null) {
            try {
                Document wreqDoc = DOMUtils.readXml(new StringReader(wreq));
                Element wreqElement = wreqDoc.getDocumentElement();
                if (wreqElement != null && "RequestSecurityToken".equals(wreqElement.getLocalName())
                    && (STSUtils.WST_NS_05_12.equals(wreqElement.getNamespaceURI())
                        || HTTP_SCHEMAS_XMLSOAP_ORG_WS_2005_02_TRUST.equals(wreqElement.getNamespaceURI()))) {
                    Element tokenTypeElement = DOMUtils
                        .getFirstChildWithName(wreqElement, wreqElement.getNamespaceURI(), "TokenType");
                    if (tokenTypeElement != null) {
                        stsTokenType = tokenTypeElement.getTextContent();
                    }
                    Element keyTypeElement = DOMUtils
                        .getFirstChildWithName(wreqElement, wreqElement.getNamespaceURI(), "KeyType");
                    if (keyTypeElement != null) {
                        stsKeyType = keyTypeElement.getTextContent();
                    }
                }
            } catch (Exception e) {
                LOG.warn("Error parsing 'wreq' parameter: " + e.getMessage());
                throw new ProcessingException(TYPE.BAD_REQUEST);
            }
        }

        if (stsTokenType != null) {
            sts.setTokenType(stsTokenType);
        } else if (serviceConfig.getTokenType() != null && serviceConfig.getTokenType().length() > 0) {
            sts.setTokenType(serviceConfig.getTokenType());
        } else {
            sts.setTokenType(getTokenType());
        }

        if (serviceConfig.getPolicyNamespace() != null && serviceConfig.getPolicyNamespace().length() > 0) {
            sts.setWspNamespace(serviceConfig.getPolicyNamespace());
        }

        LOG.debug("TokenType {} set for realm {}", sts.getTokenType(), realm);

        sts.setKeyType(stsKeyType);
        if (HTTP_DOCS_OASIS_OPEN_ORG_WS_SX_WS_TRUST_200512_PUBLICKEY.equals(stsKeyType)) {
            HttpServletRequest servletRequest = WebUtils.getHttpServletRequest(context);
            if (servletRequest != null) {
                X509Certificate[] certs = (X509Certificate[])servletRequest
                    .getAttribute("javax.servlet.request.X509Certificate");
                if (certs != null && certs.length > 0) {
                    sts.setUseCertificateForConfirmationKeyInfo(true);
                    sts.setUseKeyCertificate(certs[0]);
                } else {
                    LOG.info("Can't send a PublicKey KeyType as no client certs are available");
                    sts.setKeyType(HTTP_DOCS_OASIS_OPEN_ORG_WS_SX_WS_TRUST_200512_BEARER);
                }
            }
        }

        processWsdlLocation(context);
        sts.setWsdlLocation(wsdlLocation);
        sts.setServiceQName(new QName(namespace, wsdlService));
        sts.setEndpointQName(new QName(namespace, wsdlEndpoint));
        if (use200502Namespace) {
            sts.setNamespace(HTTP_SCHEMAS_XMLSOAP_ORG_WS_2005_02_TRUST);
        }

        if (serviceConfig.getRequestedClaims() != null && !serviceConfig.getRequestedClaims().isEmpty()) {
            addClaims(sts, serviceConfig.getRequestedClaims());
            LOG.debug("Requested claims set for {}", realm);
        }

        sts.setEnableLifetime(true);
        setLifetime(sts, serviceConfig, realm);

        sts.setEnableAppliesTo(serviceConfig.isEnableAppliesTo());

        sts.setOnBehalfOf(idpToken.getToken());

        if (properties != null) {
            sts.setProperties(properties);
        }

        if (getCustomSTSParameter() != null) {
            String authRealmParameter = context.getRequestParameters().get(getCustomSTSParameter());
            LOG.debug("Found {} custom STS parameter {}", getCustomSTSParameter(), authRealmParameter);
            if (authRealmParameter != null) {
                sts.setCustomContent(authRealmParameter);
            }
        }

        final Element rpToken;
        try {
            rpToken = sts.requestSecurityTokenResponse(realm);
        } catch (SoapFault ex) {
            LOG.error("Error in retrieving a token {}", ex.getMessage());
            if (ex.getFaultCode() != null && "RequestFailed".equals(ex.getFaultCode().getLocalPart())) {
                throw new ProcessingException(TYPE.BAD_REQUEST);
            }
            throw ex;
        }

        if (LOG.isInfoEnabled()) {
            String id = getIdFromToken(rpToken);

            LOG.info("[RP_TOKEN={}] successfully created for realm [{}] on behalf of [IDP_TOKEN={}]", id,
                     realm, idpToken.getId());
        }
        return rpToken;
    }

    private String getIdFromToken(Element token) throws IOException, XMLStreamException {
        if (token != null) {
            NodeList nd = token.getElementsByTagNameNS(WSConstants.SAML2_NS, "Assertion");

            String identifier = "ID";
            if (nd.getLength() == 0) {
                nd = token.getElementsByTagNameNS(WSConstants.SAML_NS, "Assertion");
                identifier = "AssertionID";
            }

            if (nd.getLength() > 0) {
                Element e = (Element)nd.item(0);
                if (e.hasAttributeNS(null, identifier)) {
                    return e.getAttributeNS(null, identifier);
                }
            }
        }

        return "";
    }

    private SecurityToken getSecurityToken(RequestContext context, String homeRealm)
        throws ProcessingException {

        SecurityToken idpToken = (SecurityToken)WebUtils.getAttributeFromFlowScope(context, "idpToken");
        if (idpToken != null) {
            LOG.debug("[IDP_TOKEN={} successfully retrieved from cache for home realm [{}]", idpToken.getId(),
                      homeRealm);
        } else {
            LOG.error("IDP_TOKEN not found");
            throw new ProcessingException(TYPE.BAD_REQUEST);
        }
        return idpToken;
    }

    private void processWsdlLocation(RequestContext context) {
        if (!isPortSet) {
            String updatedUrl = LocalServerResolver.resolve(this.wsdlLocation, context);
            setSTSWsdlUrl(updatedUrl);
            LOG.info("STS WSDL URL updated to {}", updatedUrl);
        }
    }

    private void addClaims(STSClient sts, List<RequestClaim> requestClaimList)
        throws ParserConfigurationException, XMLStreamException {

        Element claims = createClaimsElement(requestClaimList);
        if (claims != null) {
            sts.setClaims(claims);
        }
    }

    private Element createClaimsElement(List<RequestClaim> realmClaims)
        throws ParserConfigurationException, XMLStreamException {
        if (realmClaims == null || realmClaims.isEmpty()) {
            return null;
        }

        W3CDOMStreamWriter writer = new W3CDOMStreamWriter();
        writer.writeStartElement("wst", "Claims", STSUtils.WST_NS_05_12);
        writer.writeNamespace("wst", STSUtils.WST_NS_05_12);
        writer.writeNamespace("ic", HTTP_SCHEMAS_XMLSOAP_ORG_WS_2005_05_IDENTITY);
        writer.writeAttribute("Dialect", HTTP_SCHEMAS_XMLSOAP_ORG_WS_2005_05_IDENTITY);

        if (!realmClaims.isEmpty()) {
            for (RequestClaim item : realmClaims) {
                LOG.debug("  {}", item.getClaimType().toString());
                writer.writeStartElement("ic", "ClaimType", HTTP_SCHEMAS_XMLSOAP_ORG_WS_2005_05_IDENTITY);
                writer.writeAttribute("Uri", item.getClaimType().toString());
                writer.writeAttribute("Optional", Boolean.toString(item.isOptional()));
                writer.writeEndElement();
            }
        }

        writer.writeEndElement();

        return writer.getDocument().getDocumentElement();
    }

    private synchronized void setSTSWsdlUrl(String wsdlUrl) {
        this.wsdlLocation = wsdlUrl;
        this.isPortSet = true;
    }

    public String getKeyType() {
        return keyType;
    }

    public void setKeyType(String keyType) {
        this.keyType = keyType;
    }

    public boolean isUse200502Namespace() {
        return use200502Namespace;
    }

    public void setUse200502Namespace(boolean use200502Namespace) {
        this.use200502Namespace = use200502Namespace;
    }

    private void setLifetime(STSClient sts, Application serviceConfig, String wtrealm) {
        if (serviceConfig.getLifeTime() > 0) {
            try {
                int lifetime = serviceConfig.getLifeTime();
                sts.setTtl(lifetime);
                sts.setEnableLifetime(lifetime > 0);
                LOG.debug("Lifetime set to {} seconds for realm {}", serviceConfig.getLifeTime(), wtrealm);
            } catch (NumberFormatException ex) {
                LOG.warn("Invalid lifetime configured for service provider " + wtrealm);
                sts.setTtl(this.ttl);
                sts.setEnableLifetime(this.ttl > 0);
            }
        } else {
            sts.setTtl(this.ttl);
            sts.setEnableLifetime(this.ttl > 0);
            LOG.debug("Lifetime set to {} seconds for realm {}", this.ttl, wtrealm);
        }
    }

    public Map<String, Object> getProperties() {
        return properties;
    }

    public void setProperties(Map<String, Object> properties) {
        this.properties = properties;
    }
}
