/*
 * (C) Copyright 2015 Nuxeo SA (http://nuxeo.com/) and others.
 *
 * 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.
 *
 * Contributors:
 *     François Maturel
 */
package org.nuxeo.ecm.platform.ui.web.keycloak;

import static org.nuxeo.ecm.platform.ui.web.keycloak.KeycloakUserInfo.KeycloakUserInfoBuilder.aKeycloakUserInfo;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.keycloak.adapters.AdapterDeploymentContext;
import org.keycloak.adapters.spi.AuthOutcome;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.representations.AccessToken;
import org.nuxeo.ecm.platform.api.login.UserIdentificationInfo;
import org.nuxeo.ecm.platform.ui.web.auth.interfaces.NuxeoAuthenticationPlugin;
import org.nuxeo.ecm.platform.ui.web.auth.interfaces.NuxeoAuthenticationPluginLogoutExtension;
import org.nuxeo.runtime.api.Framework;
import org.nuxeo.usermapper.service.UserMapperService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Authentication plugin for handling auth flow with Keyloack
 *
 * @since 7.4
 */

public class KeycloakAuthenticationPlugin implements NuxeoAuthenticationPlugin,
        NuxeoAuthenticationPluginLogoutExtension {

    private static final Logger LOGGER = LoggerFactory.getLogger(KeycloakAuthenticationPlugin.class);

    private static final String PROTOCOL_CLASSPATH = "classpath:";

    public static final String KEYCLOAK_CONFIG_FILE_KEY = "keycloakConfigFilename";

    public static final String KEYCLOAK_MAPPING_NAME_KEY = "mappingName";

    public static final String DEFAULT_MAPPING_NAME = "keycloak";

    private String keycloakConfigFile = PROTOCOL_CLASSPATH + "keycloak.json";

    private KeycloakAuthenticatorProvider keycloakAuthenticatorProvider;

    protected String mappingName = DEFAULT_MAPPING_NAME;

    @Override
    public void initPlugin(Map<String, String> parameters) {
        LOGGER.info("INITIALIZE KEYCLOAK");

        if (parameters.containsKey(KEYCLOAK_CONFIG_FILE_KEY)) {
            keycloakConfigFile = PROTOCOL_CLASSPATH + parameters.get(KEYCLOAK_CONFIG_FILE_KEY);
        }

        if (parameters.containsKey(KEYCLOAK_MAPPING_NAME_KEY)) {
            mappingName = parameters.get(KEYCLOAK_MAPPING_NAME_KEY);
        }

        InputStream is = loadKeycloakConfigFile();
        KeycloakDeployment kd = KeycloakNuxeoDeployment.build(is);
        keycloakAuthenticatorProvider = new KeycloakAuthenticatorProvider(new AdapterDeploymentContext(kd));
        LOGGER.info("Keycloak is using a per-deployment configuration loaded from: " + keycloakConfigFile);
    }

    @Override
    public Boolean needLoginPrompt(HttpServletRequest httpRequest) {
        return Boolean.TRUE;
    }

    @Override
    public Boolean handleLoginPrompt(HttpServletRequest httpRequest, HttpServletResponse httpResponse, String baseURL) {
        return Boolean.TRUE;
    }

    @Override
    public List<String> getUnAuthenticatedURLPrefix() {
        // There are no unauthenticated URLs associated to login prompt.
        // If user is not authenticated, this plugin will have to redirect user to the keycloak sso login prompt
        return null;
    }

    @Override
    public UserIdentificationInfo handleRetrieveIdentity(HttpServletRequest httpRequest,
            HttpServletResponse httpResponse) {
        LOGGER.debug("KEYCLOAK will handle identification");

        KeycloakRequestAuthenticator authenticator = keycloakAuthenticatorProvider.provide(httpRequest, httpResponse);
        KeycloakDeployment deployment = keycloakAuthenticatorProvider.getResolvedDeployment();
        String keycloakNuxeoApp = deployment.getResourceName();

        AuthOutcome outcome = authenticator.authenticate();

        if (outcome == AuthOutcome.AUTHENTICATED) {
            AccessToken token = (AccessToken) httpRequest.getAttribute(KeycloakRequestAuthenticator.KEYCLOAK_ACCESS_TOKEN);

            KeycloakUserInfo keycloakUserInfo = getKeycloakUserInfo(token);

            UserMapperService ums = Framework.getService(UserMapperService.class);

            keycloakUserInfo.setRoles(getRoles(token, keycloakNuxeoApp));

            ums.getOrCreateAndUpdateNuxeoPrincipal(mappingName, keycloakUserInfo);

            return keycloakUserInfo;
        }
        return null;
    }

    @Override
    public Boolean handleLogout(HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
        LOGGER.debug("KEYCLOAK will handle logout");

        String uri = keycloakAuthenticatorProvider.logout(httpRequest, httpResponse);
        try {
            httpResponse.sendRedirect(uri);
        } catch (IOException e) {
            String message = "Could note handle logout with URI: " + uri;
            LOGGER.error(message);
            throw new RuntimeException(message);
        }
        return Boolean.TRUE;
    }

    /**
     * Get keycloak user's information from authentication token
     *
     * @param token the keycoak authentication token
     * @return keycloak user's information
     */
    private KeycloakUserInfo getKeycloakUserInfo(AccessToken token) {
        return aKeycloakUserInfo()
        // Required
        .withUserName(token.getEmail())
        // Optional
        .withFirstName(token.getGivenName()).withLastName(token.getFamilyName()).withCompany(
                token.getPreferredUsername()).withAuthPluginName("KEYCLOAK_AUTH")
        // The password is randomly generated has we won't use it
        .withPassword(UUID.randomUUID().toString()).build();
    }

    /**
     * Get keycloak user's roles from authentication token
     *
     * @param token the keycoak authentication token
     * @param keycloakNuxeoApp the keycoak resource name
     * @return keycloak user's roles
     */
    private Set<String> getRoles(AccessToken token, String keycloakNuxeoApp) {
        Set<String> allRoles = new HashSet<>();

        AccessToken.Access realmAccess = token.getRealmAccess();
        if (realmAccess != null && realmAccess.getRoles() != null) {
            allRoles.addAll(realmAccess.getRoles());
        }

        AccessToken.Access nuxeoResource = token.getResourceAccess(keycloakNuxeoApp);
        if (nuxeoResource != null) {
            Set<String> nuxeoRoles = nuxeoResource.getRoles();
            allRoles.addAll(nuxeoRoles);
        }
        return allRoles;
    }

    /**
     * Loads Keycloak from configuration file
     *
     * @return the configuration file as an {@link InputStream}
     */
    private InputStream loadKeycloakConfigFile() {

        if (keycloakConfigFile.startsWith(PROTOCOL_CLASSPATH)) {
            String classPathLocation = keycloakConfigFile.replace(PROTOCOL_CLASSPATH, "");

            LOGGER.debug("Loading config from classpath on location: " + classPathLocation);

            // Try current class classloader first
            InputStream is = getClass().getClassLoader().getResourceAsStream(classPathLocation);
            if (is == null) {
                is = Thread.currentThread().getContextClassLoader().getResourceAsStream(classPathLocation);
            }

            if (is != null) {
                return is;
            } else {
                String message = "Unable to find config from classpath: " + keycloakConfigFile;
                LOGGER.error(message);
                throw new RuntimeException(message);
            }
        } else {
            // Fallback to file
            try {
                LOGGER.debug("Loading config from file: " + keycloakConfigFile);
                return new FileInputStream(keycloakConfigFile);
            } catch (FileNotFoundException fnfe) {
                String message = "Config not found on " + keycloakConfigFile;
                LOGGER.error(message);
                throw new RuntimeException(message, fnfe);
            }
        }
    }

    public void setKeycloakAuthenticatorProvider(KeycloakAuthenticatorProvider keycloakAuthenticatorProvider) {
        this.keycloakAuthenticatorProvider = keycloakAuthenticatorProvider;
    }
}
