/*
 * This file is part of ***  M y C o R e  ***
 * See http://www.mycore.de/ for details.
 *
 * MyCoRe is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * MyCoRe is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with MyCoRe.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.mycore.frontend.jersey;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Date;
import java.util.Optional;

import org.apache.logging.log4j.LogManager;
import org.mycore.common.MCRSession;
import org.mycore.common.MCRUserInformation;
import org.mycore.common.config.MCRConfiguration2;
import org.mycore.common.config.MCRConfigurationDir;
import org.mycore.common.config.MCRConfigurationException;
import org.mycore.common.events.MCRStartupHandler;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;

import jakarta.servlet.ServletContext;
import jakarta.ws.rs.core.Response;

public class MCRJWTUtil implements MCRStartupHandler.AutoExecutable {
    public static final String JWT_CLAIM_ROLES = "mcr:roles";

    public static final String JWT_CLAIM_IP = "mcr:ip";

    public static final String JWT_USER_ATTRIBUTE_PREFIX = "mcr:ua:";

    public static final String JWT_SESSION_ATTRIBUTE_PREFIX = "mcr:sa:";

    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    private static final String ROLES_PROPERTY = "MCR.Rest.JWT.Roles";

    private static Algorithm SHARED_SECRET;

    public static JWTCreator.Builder getJWTBuilder(MCRSession mcrSession) {
        return getJWTBuilder(mcrSession, null, null);
    }

    public static JWTCreator.Builder getJWTBuilder(MCRSession mcrSession,
        String[] userAttributes, String[] sessionAttributes) {
        MCRUserInformation userInformation = mcrSession.getUserInformation();
        String[] roles = MCRConfiguration2.getOrThrow(ROLES_PROPERTY, MCRConfiguration2::splitValue)
            .filter(userInformation::isUserInRole)
            .toArray(String[]::new);
        String subject = userInformation.getUserID();
        String email = userInformation.getUserAttribute(MCRUserInformation.ATT_EMAIL);
        String name = userInformation.getUserAttribute(MCRUserInformation.ATT_REAL_NAME);
        JWTCreator.Builder builder = JWT.create()
            .withIssuedAt(new Date())
            .withSubject(subject)
            .withArrayClaim("mcr:roles", roles)
            .withClaim("email", email)
            .withClaim("name", name);
        if (userAttributes != null) {
            for (String userAttribute : userAttributes) {
                String value = userInformation.getUserAttribute(userAttribute);
                if (value != null) {
                    builder.withClaim(JWT_USER_ATTRIBUTE_PREFIX + userAttribute, value);
                }
            }
        }
        if (sessionAttributes != null) {
            for (String sessionAttribute : sessionAttributes) {
                Object object = mcrSession.get(sessionAttribute);
                Optional.ofNullable(object).map(Object::toString)
                    .ifPresent((value) -> builder.withClaim(JWT_SESSION_ATTRIBUTE_PREFIX + sessionAttribute, value));
            }
        }
        return builder;
    }

    public static Algorithm getJWTAlgorithm() {
        return SHARED_SECRET;
    }

    public static Response getJWTLoginSuccessResponse(String jwt) throws IOException {
        ObjectNode response = OBJECT_MAPPER.createObjectNode();
        response.put("login_success", true);
        response.put("access_token", jwt);
        response.put("token_type", "Bearer");
        return Response.status(Response.Status.OK)
            .header("Authorization", "Bearer " + jwt)
            .entity(response)
            .build();
    }

    public static Response getJWTRenewSuccessResponse(String jwt) throws IOException {
        ObjectNode response = OBJECT_MAPPER.createObjectNode();
        response.put("executed", true);
        response.put("access_token", jwt);
        response.put("token_type", "Bearer");
        return Response.status(Response.Status.OK)
            .header("Authorization", "Bearer " + jwt)
            .entity(response)
            .build();
    }

    public static Response getJWTLoginErrorResponse(String errorDescription) throws IOException {
        ObjectNode response = OBJECT_MAPPER.createObjectNode();
        response.put("login_success", false);
        response.put("error", "login_failed");
        response.put("error_description", errorDescription);
        return Response.status(Response.Status.FORBIDDEN)
            .entity(response)
            .build();
    }

    @Override
    public String getName() {
        return "JSON WebToken Services";
    }

    @Override
    public int getPriority() {
        return 0;
    }

    @Override
    public void startUp(ServletContext servletContext) {
        if (servletContext != null) {
            File sharedSecretFile = MCRConfigurationDir.getConfigFile("jwt.secret");
            byte[] secret;
            if (!sharedSecretFile.isFile()) {
                secret = new byte[4096];
                try {
                    LogManager.getLogger().warn(
                        "Creating shared secret file ({}) for JSON Web Token."
                            + " This may take a while. Please wait...",
                        sharedSecretFile);
                    SecureRandom.getInstanceStrong().nextBytes(secret);
                    Files.write(sharedSecretFile.toPath(), secret, StandardOpenOption.CREATE_NEW);
                } catch (NoSuchAlgorithmException | IOException e) {
                    throw new MCRConfigurationException(
                        "Could not create shared secret in file: " + sharedSecretFile.getAbsolutePath(), e);
                }
            } else {
                try {
                    secret = Files.readAllBytes(sharedSecretFile.toPath());
                } catch (IOException e) {
                    throw new MCRConfigurationException(
                        "Could not create shared secret in file: " + sharedSecretFile.getAbsolutePath(), e);
                }
            }
            SHARED_SECRET = Algorithm.HMAC512(secret);
        }
    }

}
