/**
 * Copyright 2005-2013 Restlet S.A.S.
 * 
 * The contents of this file are subject to the terms of one of the following
 * open source licenses: Apache 2.0 or LGPL 3.0 or LGPL 2.1 or CDDL 1.0 or EPL
 * 1.0 (the "Licenses"). You can select the license that you prefer but you may
 * not use this file except in compliance with one of these Licenses.
 * 
 * You can obtain a copy of the Apache 2.0 license at
 * http://www.opensource.org/licenses/apache-2.0
 * 
 * You can obtain a copy of the LGPL 3.0 license at
 * http://www.opensource.org/licenses/lgpl-3.0
 * 
 * You can obtain a copy of the LGPL 2.1 license at
 * http://www.opensource.org/licenses/lgpl-2.1
 * 
 * You can obtain a copy of the CDDL 1.0 license at
 * http://www.opensource.org/licenses/cddl1
 * 
 * You can obtain a copy of the EPL 1.0 license at
 * http://www.opensource.org/licenses/eclipse-1.0
 * 
 * See the Licenses for the specific language governing permissions and
 * limitations under the Licenses.
 * 
 * Alternatively, you can obtain a royalty free commercial license with less
 * limitations, transferable or non-transferable, directly at
 * http://www.restlet.com/products/restlet-framework
 * 
 * Restlet is a registered trademark of Restlet S.A.S.
 */

package org.restlet.ext.openid;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.logging.Logger;

import org.openid4java.consumer.ConsumerManager;
import org.openid4java.consumer.VerificationResult;
import org.openid4java.discovery.DiscoveryInformation;
import org.openid4java.discovery.Identifier;
import org.openid4java.message.AuthRequest;
import org.openid4java.message.AuthSuccess;
import org.openid4java.message.MessageExtension;
import org.openid4java.message.ParameterList;
import org.openid4java.message.ax.AxMessage;
import org.openid4java.message.ax.FetchRequest;
import org.openid4java.message.ax.FetchResponse;
import org.restlet.Context;
import org.restlet.Request;
import org.restlet.Response;
import org.restlet.data.Form;
import org.restlet.data.MediaType;
import org.restlet.data.Reference;
import org.restlet.engine.Engine;
import org.restlet.representation.Representation;
import org.restlet.representation.StringRepresentation;

/**
 * Describes a relaying party, also known as a RP.
 * 
 * @author Martin Svensson
 */
public class RelayingParty {

    private static Representation getForm(AuthRequest authReq) {
        StringBuilder sb = new StringBuilder();
        sb.append("<html>");
        sb.append("<head>");
        sb.append("<title>OpenID HTML FORM Redirection</title>");
        sb.append("</head>");
        sb.append("<body onload=\"document.forms['openid-form-redirection'].submit();\">");
        sb.append("<form name=\"openid-form-redirection\" action=\"");
        sb.append(authReq.getOPEndpoint());
        sb.append("\" method=\"post\" accept-charset=\"utf-8\">");
        for (Object key : authReq.getParameterMap().keySet()) {
            sb.append(" <input type=\"hidden\" name=\"");
            sb.append(key.toString());
            // ${parameter.key}
            sb.append("\" value=\"");
            sb.append(authReq.getParameterMap().get(key));
            sb.append("\"/>");
        }
        sb.append("</form>");
        sb.append("</body>");
        sb.append("</html>");
        return new StringRepresentation(sb.toString(), MediaType.TEXT_HTML);
    }

    private final ConsumerManager cm;

    private final Map<String, DiscoveryInformation> sessions;

    public RelayingParty() {
        this(new ConsumerManager());
    }

    public RelayingParty(ConsumerManager cm) {
        this.cm = cm;
        this.sessions = new HashMap<String, DiscoveryInformation>();
    }

    public String authRequest(String identifier, boolean sessionAware,
            boolean addReturnTo, String returnTo,
            Set<AttributeExchange> optionalAttrs,
            Set<AttributeExchange> requiredAttrs, Request req, Response res)
            throws Exception {

        List<?> discoveries = cm.discover(identifier);
        DiscoveryInformation di = cm.associate(discoveries);

        // return
        String sessionId = null;
        if (sessionAware && di != null) {
            getLogger().info("save discovery information to session");
            sessionId = UUID.randomUUID().toString();
            this.sessions.put(sessionId, di);
            Reference ref = new Reference(returnTo);
            ref.addQueryParameter("sessionId", sessionId);
            returnTo = ref.toString();
        }
        if (addReturnTo) {
            Reference ref = new Reference(returnTo);
            ref.addQueryParameter("returnTo", "true");
            returnTo = ref.toString();
        }
        AuthRequest authReq = cm.authenticate(di, returnTo);
        FetchRequest fetch = null;

        // add attributes
        if (optionalAttrs != null) {
            fetch = FetchRequest.createFetchRequest();
            for (AttributeExchange o : optionalAttrs) {
                fetch.addAttribute(o.getName(), o.getSchema(), false);
            }
        }
        if (requiredAttrs != null) {
            if (fetch == null)
                fetch = FetchRequest.createFetchRequest();
            for (AttributeExchange r : requiredAttrs) {
                fetch.addAttribute(r.getName(), r.getSchema(), true);
            }
        }
        if (fetch != null)
            authReq.addExtension(fetch);

        if (di.isVersion2()) {
            getLogger().info("sending auth request using OpenId 2 form");
            res.setEntity(getForm(authReq));
        } else {
            getLogger().info(
                    "sending auth request using OpenId 1 query parameters");
            res.redirectTemporary(authReq.getDestinationUrl(true));
        }
        return sessionId;
    }

    public Logger getLogger() {
        Logger result = null;

        Context context = Context.getCurrent();

        if (context != null) {
            result = context.getLogger();
        }

        if (result == null) {
            result = Engine.getLogger(this, "org.restlet.ext.openid.RP");
        }

        return result;
    }

    public boolean hasReturnTo(Request request) {
        String val = request.getResourceRef().getQueryAsForm()
                .getFirstValue("returnTo");
        return "true".equals(val) == true ? true : false;
    }

    public Identifier verify(Map<AttributeExchange, String> axResp,
            Request req, boolean sessionAware) throws Exception {

        // TODO: Make sure it can handle form based returns as well!
        Form params = req.getResourceRef().getQueryAsForm();
        ParameterList response = new ParameterList(params.getValuesMap());

        String sessionId = sessionAware ? params.getFirstValue("sessionId")
                : null;

        // retrieve the previously stored discovery information
        DiscoveryInformation discovered = sessionId != null ? sessions
                .get(sessionId) : null;
        getLogger().info(
                "retrieved discovery information from session: (" + sessionId
                        + ") " + discovered);

        String received = req.getResourceRef().getHostIdentifier()
                + req.getResourceRef().getPath();
        if (req.getResourceRef().hasQuery()) {
            received += "?" + req.getResourceRef().getQuery();
        }
        VerificationResult verification = cm.verify(received, response,
                discovered);
        Identifier verified = verification.getVerifiedId();
        if (verified != null) {
            AuthSuccess authSuccess = (AuthSuccess) verification
                    .getAuthResponse();

            if (authSuccess.hasExtension(AxMessage.OPENID_NS_AX)) {
                FetchResponse fetchResp = (FetchResponse) authSuccess
                        .getExtension(AxMessage.OPENID_NS_AX);

                MessageExtension ext = authSuccess
                        .getExtension(AxMessage.OPENID_NS_AX);
                if (ext instanceof FetchResponse) {
                    @SuppressWarnings("unchecked")
                    List<String> aliases = (List<String>) fetchResp
                            .getAttributeAliases();
                    for (String alias : aliases) {
                        String value = fetchResp.getAttributeValue(alias);
                        axResp.put(AttributeExchange.valueOf(alias), value);
                    }
                }
            }
        }
        return verified;
    }

}
