/*
 * Licensed to Jasig under one or more contributor license
 * agreements. See the NOTICE file distributed with this work
 * for additional information regarding copyright ownership.
 * Jasig 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 the following location:
 *
 *   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.jasig.cas.support.openid.authentication.principal;

import java.util.HashMap;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;

import org.jasig.cas.CentralAuthenticationService;
import org.jasig.cas.authentication.principal.AbstractWebApplicationService;
import org.jasig.cas.authentication.principal.Response;
import org.jasig.cas.ticket.TicketException;
import org.jasig.cas.util.ApplicationContextProvider;
import org.openid4java.association.Association;
import org.openid4java.message.AuthRequest;
import org.openid4java.message.Message;
import org.openid4java.message.MessageException;
import org.openid4java.message.ParameterList;
import org.openid4java.server.ServerManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;

/**
 * @author Scott Battaglia
 * @since 3.1
 */
public final class OpenIdService extends AbstractWebApplicationService {
    protected static final Logger LOGGER = LoggerFactory.getLogger(OpenIdService.class);

    private static final long serialVersionUID = 5776500133123291301L;

    private static final String CONST_PARAM_SERVICE = "openid.return_to";

    private final String identity;

    private final String artifactId;

    private final ParameterList requestParameters;

    protected OpenIdService(final String id, final String originalUrl,
            final String artifactId, final String openIdIdentity,
            final String signature, final ParameterList parameterList) {
        super(id, originalUrl, artifactId);
        this.identity = openIdIdentity;
        this.artifactId = artifactId;
        this.requestParameters = parameterList;
    }

    /**
     * Generates an Openid response.
     * If no ticketId is found, response is negative.
     * If we have a ticket id, then we check if we have an association.
     * If so, we ask OpenId server manager to generate the answer according with the existing association.
     * If not, we send back an answer with the ticket id as association handle.
     * This will force the consumer to ask a verification, which will validate the service ticket.
     * @param ticketId the service ticket to provide to the service.
     * @return the generated authentication answer
     */
    @Override
    public Response getResponse(final String ticketId) {
        final Map<String, String> parameters = new HashMap<String, String>();
        if (ticketId != null) {

            ServerManager manager = (ServerManager) ApplicationContextProvider.getApplicationContext().getBean("serverManager");
            CentralAuthenticationService cas = (CentralAuthenticationService) ApplicationContextProvider.getApplicationContext()
                                                .getBean("centralAuthenticationService");
            boolean associated = false;
            boolean associationValid = true;
            try {
                AuthRequest authReq = AuthRequest.createAuthRequest(requestParameters, manager.getRealmVerifier());
                Map parameterMap = authReq.getParameterMap();
                if (parameterMap != null && parameterMap.size() > 0) {
                    String assocHandle = (String) parameterMap.get("openid.assoc_handle");
                    if (assocHandle != null) {
                        Association association = manager.getSharedAssociations().load(assocHandle);
                        if (association != null) {
                            associated = true;
                            if (association.hasExpired()) {
                                associationValid = false;
                            }
                        }

                    }
                }
            } catch (final MessageException me) {
                LOGGER.error("Message exception : {}", me.getMessage(), me);
            }

            boolean successFullAuthentication = true;
            try {
                if (associated) {
                    if (associationValid) {
                        cas.validateServiceTicket(ticketId, this);
                        LOGGER.info("Validated openid ticket");
                    } else {
                        successFullAuthentication = false;
                    }
                }
            } catch (final TicketException te) {
                LOGGER.error("Could not validate ticket : {}", te.getMessage(), te);
                successFullAuthentication = false;
            }

            // We sign directly (final 'true') because we don't add extensions
            // response message can be either a DirectError or an AuthSuccess here.
            // Anyway, handling is the same : send the response message
            Message response = manager.authResponse(requestParameters,
                    this.identity,
                    this.identity,
                    successFullAuthentication,
                    true);
            parameters.putAll(response.getParameterMap());
            if (!associated) {
                parameters.put("openid.assoc_handle", ticketId);
            }
        } else {
            parameters.put("openid.mode", "cancel");
        }
        return Response.getRedirectResponse(getOriginalUrl(), parameters);
    }

    /**
     * Return that the service is already logged out.
     *
     * @return that the service is already logged out.
     */
    @Override
    public boolean isLoggedOutAlready() {
        return true;
    }

    public static OpenIdService createServiceFrom(
            final HttpServletRequest request) {
        final String service = request.getParameter(CONST_PARAM_SERVICE);
        final String openIdIdentity = request.getParameter("openid.identity");
        final String signature = request.getParameter("openid.sig");

        if (openIdIdentity == null || !StringUtils.hasText(service)) {
            return null;
        }

        final String id = cleanupUrl(service);
        final String artifactId = request.getParameter("openid.assoc_handle");
        ParameterList paramList = new ParameterList(request.getParameterMap());

        return new OpenIdService(id, service, artifactId, openIdIdentity,
                signature, paramList);
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result
                + (this.identity == null ? 0 : this.identity.hashCode());
        return result;
    }

    @Override
    public boolean equals(final Object obj) {
        if (this == obj) {
            return true;
        }
        if (!super.equals(obj)) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final OpenIdService other = (OpenIdService) obj;
        if (this.identity == null) {
            if (other.identity != null) {
                return false;
            }
        } else if (!this.identity.equals(other.identity)) {
            return false;
        }
        return true;
    }

    public String getIdentity() {
        return this.identity;
    }
}