/**********************************************************************
* This file is part of iDempiere ERP Open Source                      *
* http://www.idempiere.org                                            *
*                                                                     *
* Copyright (C) Contributors                                          *
*                                                                     *
* This program 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 2      *
* of the License, or (at your option) any later version.              *
*                                                                     *
* This program 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 this program; if not, write to the Free Software         *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,          *
* MA 02110-1301, USA.                                                 *
*                                                                     *
* Contributors:                                                       *
* - Trek Global Corporation                                           *
* - Heng Sin Low                                                      *
**********************************************************************/
package com.trekglobal.idempiere.rest.api.v1.auth.impl;

import java.security.SecureRandom;
import java.sql.Timestamp;
import java.time.Instant;
import java.util.regex.Pattern;

import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.xml.bind.DatatypeConverter;

import org.adempiere.util.LogAuthFailure;
import org.compiere.model.I_AD_Preference;
import org.compiere.model.MClient;
import org.compiere.model.MClientInfo;
import org.compiere.model.MOrg;
import org.compiere.model.MOrgInfo;
import org.compiere.model.MPreference;
import org.compiere.model.MRole;
import org.compiere.model.MRoleOrgAccess;
import org.compiere.model.MSession;
import org.compiere.model.MSysConfig;
import org.compiere.model.MTable;
import org.compiere.model.MUser;
import org.compiere.model.MUserOrgAccess;
import org.compiere.model.MWarehouse;
import org.compiere.model.PO;
import org.compiere.model.Query;
import org.compiere.util.Env;
import org.compiere.util.KeyNamePair;
import org.compiere.util.Language;
import org.compiere.util.Login;
import org.compiere.util.Msg;
import org.compiere.util.Util;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator.Builder;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTCreationException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.trekglobal.idempiere.rest.api.json.RestUtils;
import com.trekglobal.idempiere.rest.api.model.MRefreshToken;
import com.trekglobal.idempiere.rest.api.util.ErrorBuilder;
import com.trekglobal.idempiere.rest.api.v1.auth.AuthService;
import com.trekglobal.idempiere.rest.api.v1.auth.LoginCredential;
import com.trekglobal.idempiere.rest.api.v1.auth.LoginParameters;
import com.trekglobal.idempiere.rest.api.v1.auth.LogoutParameters;
import com.trekglobal.idempiere.rest.api.v1.auth.RefreshParameters;
import com.trekglobal.idempiere.rest.api.v1.auth.filter.RequestFilter;
import com.trekglobal.idempiere.rest.api.v1.jwt.LoginClaims;
import com.trekglobal.idempiere.rest.api.v1.jwt.TokenUtils;

/**
 * @author hengsin
 *
 */
public class AuthServiceImpl implements AuthService {

	private static LogAuthFailure logAuthFailure = new LogAuthFailure();

	public static final String ROLE_TYPES_WEBSERVICE = "NULL,WS";  //webservice+null

	private @Context HttpServletRequest request = null;

	/**
	 * default constructor
	 */
	public AuthServiceImpl() {
	}

	/* (non-Javadoc)
	 * @see org.idempiere.rest.api.v1.AuthService#login(org.idempiere.rest.api.v1.LoginCredential)
	 */
	@Override
	public Response authenticate(LoginCredential credential) {
		Login login = new Login(Env.getCtx());
		KeyNamePair[] clients = login.getClients(credential.getUserName(), credential.getPassword(), ROLE_TYPES_WEBSERVICE);
		if (clients == null || clients.length == 0) {
        	String loginErrMsg = login.getLoginErrMsg();
        	return unauthorized(loginErrMsg, credential.getUserName());
		} else {
			JsonArray clientNodes = new JsonArray();
			StringBuilder clientsSB = new StringBuilder();
			for(KeyNamePair client : clients) {
				JsonObject node = new JsonObject();
				node.addProperty("id", client.getKey());
				node.addProperty("name", client.getName());
				clientNodes.add(node);
				if (clientsSB.length() > 0)
					clientsSB.append(",");
				clientsSB.append(client.getKey());
			}
			if (credential.getParameters() != null) {
				LoginParameters parameters = credential.getParameters();
				String userName = credential.getUserName();
				Env.setContext(Env.getCtx(), RequestFilter.LOGIN_NAME, userName);
				return processLoginParameters(parameters, userName, clientsSB.toString());
			}
			JsonObject responseNode = new JsonObject();
			responseNode.add("clients", clientNodes);
			Builder builder = JWT.create()
					.withSubject(credential.getUserName())
					.withClaim(LoginClaims.Clients.name(), clientsSB.toString());
			
			//set client, role and org if user has access to 1 only
			boolean setLoginParameters = false;
			if (clients.length == 1 ) {
				int clientId = clients[0].getKey();
				String userName = credential.getUserName();
				KeyNamePair[] roles = login.getRoles(userName, clients[0], ROLE_TYPES_WEBSERVICE);
				if (roles.length == 1) {
					int roleId = roles[0].getKey();
					MUser user = MUser.get(Env.getCtx(), userName);
					MRole role = MRole.get(Env.getCtx(), roleId);
					int orgId = getOrgIdFirstOnly(user, role);
					int warehouseId = 0;
					if (orgId >= 0) {
						warehouseId = MOrgInfo.get(orgId).getM_Warehouse_ID();
					}
					Env.setContext(Env.getCtx(), RequestFilter.LOGIN_NAME, userName);
					String errorMessage = validateLoginParameters(userName, clientId, roleId, orgId, warehouseId);
					if (Util.isEmpty(errorMessage)) {
						addClientIdClaim(builder, clientId);
						responseNode.addProperty("clientId", clientId);
						addUserIdClaim(userName, responseNode, builder);
						builder.withClaim(LoginClaims.AD_Role_ID.name(), roleId);
						responseNode.addProperty("roleId", roleId);
						builder.withClaim(LoginClaims.AD_Org_ID.name(), orgId);
						responseNode.addProperty("organizationId", orgId);
						if (orgId > 0 && warehouseId > 0) {
							builder.withClaim(LoginClaims.M_Warehouse_ID.name(), warehouseId);
							responseNode.addProperty("warehouseId", warehouseId);
						}
						String defaultLanguage = getPreferenceUserLanguage(user.getAD_User_ID());
						addLanguageClaim(responseNode, builder, defaultLanguage);
						
						createSession(builder);
						addMenuTreeIdResponse(responseNode);
						setLoginParameters = true;
					}
				}
			}
			
			addIssuerAndExpiresAt(builder);
			try {
				createToken(responseNode, builder, setLoginParameters);
			} catch (IllegalArgumentException | JWTCreationException e) {
				e.printStackTrace();
				return Response.status(Status.BAD_REQUEST).build();
			}
			return Response.ok(responseNode.toString()).build();
		}
	}

	/**
	 * Get organization id if user or role has access to only 1 organization
	 * @param user
	 * @param role
	 * @return organization id if user or role has access to only 1 organization, -1 otherwise
	 */
	private int getOrgIdFirstOnly(MUser user, MRole role) {
		int orgId = -1;
		if (!role.isUseUserOrgAccess()) {
			MRoleOrgAccess[] roleOrgAccess = MRoleOrgAccess.getOfRole(Env.getCtx(), role.getAD_Role_ID());
			if (roleOrgAccess.length == 1)
				orgId = roleOrgAccess[0].getAD_Org_ID();
		} else {
			MUserOrgAccess[] userOrgAccess =  MUserOrgAccess.getOfUser(Env.getCtx(), user.getAD_User_ID());
			if (userOrgAccess.length == 1)
				orgId = userOrgAccess[0].getAD_Org_ID();
		}
		return orgId;
	}

	/**
	 * @param loginErrMsg
	 * @return
	 */
	private Response unauthorized(String loginErrMsg, String userName) {
    	if (Util.isEmpty(loginErrMsg))
    		loginErrMsg = Msg.getMsg(Env.getCtx(),"FailedLogin", true);
    	String x_Forward_IP = request.getHeader("X-Forwarded-For");
        if (x_Forward_IP == null) {
        	 x_Forward_IP = request.getRemoteAddr();
        }
    	logAuthFailure.log(x_Forward_IP, "/api", userName, loginErrMsg);

		return Response.status(Status.UNAUTHORIZED)
				.entity(new ErrorBuilder().status(Status.UNAUTHORIZED).title("Authenticate error").append(loginErrMsg).build().toString())
				.build();
	}

	/* (non-Javadoc)
	 * @see org.idempiere.rest.api.v1.AuthService#getRoles(int)
	 */
	@Override
	public Response getRoles(int clientId) {
		try {
			String userName = Env.getContext(Env.getCtx(), RequestFilter.LOGIN_NAME);
			MClient client = MClient.get(Env.getCtx(), clientId);
			String clients = Env.getContext(Env.getCtx(), RequestFilter.LOGIN_CLIENTS);
			boolean isValidClient = isValidClient(clientId, clients);
			if (!isValidClient)
				return unauthorized("Invalid client", userName);
			KeyNamePair knp = new KeyNamePair(client.getAD_Client_ID(), client.getName());
			Login login = new Login(Env.getCtx());
			KeyNamePair[] roles = login.getRoles(userName, knp, ROLE_TYPES_WEBSERVICE);
			JsonArray array = new JsonArray();
			if (roles != null) {
				for(KeyNamePair role : roles) {
					JsonObject node = new JsonObject();
					node.addProperty("id", role.getKey());
					node.addProperty("name", role.getName());
					array.add(node);
				}
			}
			JsonObject json = new JsonObject();
			json.add("roles", array);
			return Response.ok(json.toString()).build();
		} catch (IllegalArgumentException e) {
			e.printStackTrace();
			return Response.status(Status.BAD_REQUEST).build();
		}
	}

	/* (non-Javadoc)
	 * @see org.idempiere.rest.api.v1.AuthService#getOrganizations(int, int)
	 */
	@Override
	public Response getOrganizations(int clientId, int roleId) {
		try {
			String userName = Env.getContext(Env.getCtx(), RequestFilter.LOGIN_NAME);
			MClient client = MClient.get(Env.getCtx(), clientId);
			String clients = Env.getContext(Env.getCtx(), RequestFilter.LOGIN_CLIENTS);
			boolean isValidClient = isValidClient(clientId, clients);
			if (!isValidClient)
				return unauthorized("Invalid client", userName);
			KeyNamePair clientKeyNamePair = new KeyNamePair(client.getAD_Client_ID(), client.getName());
			Login login = new Login(Env.getCtx());
			KeyNamePair[] roles = login.getRoles(userName, clientKeyNamePair, ROLE_TYPES_WEBSERVICE);
			boolean isValidRole = isValidRole(roleId, roles);
			if (!isValidRole)
				return unauthorized("Invalid role", userName);
			Env.setContext(Env.getCtx(), Env.AD_CLIENT_ID, client.getAD_Client_ID());
			MUser user = MUser.get(Env.getCtx(), userName);
			Env.setContext(Env.getCtx(), Env.AD_USER_ID, user.getAD_User_ID());
			MRole role = MRole.get(Env.getCtx(), roleId);
			KeyNamePair knp = new KeyNamePair(role.getAD_Role_ID(), role.getName());
			KeyNamePair[] orgs = login.getOrgs(knp);
			JsonArray array = new JsonArray();
			if (orgs != null) {
				for(KeyNamePair org : orgs) {
					JsonObject node = new JsonObject();
					node.addProperty("id", org.getKey());
					node.addProperty("name", org.getName());
					array.add(node);
				}
			}
			JsonObject json = new JsonObject();
			json.add("organizations", array);
			return Response.ok(json.toString()).build();
		} catch (IllegalArgumentException e) {
			e.printStackTrace();
			return Response.status(Status.BAD_REQUEST).build();
		}
	}

	/* (non-Javadoc)
	 * @see org.idempiere.rest.api.v1.AuthService#getWarehouses(int, int, int)
	 */
	@Override
	public Response getWarehouses(int clientId, int roleId, int organizationId) {
		try {
			String userName = Env.getContext(Env.getCtx(), RequestFilter.LOGIN_NAME);
			MClient client = MClient.get(Env.getCtx(), clientId);
			String clients = Env.getContext(Env.getCtx(), RequestFilter.LOGIN_CLIENTS);
			boolean isValidClient = isValidClient(clientId, clients);
			if (!isValidClient)
				return unauthorized("Invalid client", userName);
			KeyNamePair clientKeyNamePair = new KeyNamePair(client.getAD_Client_ID(), client.getName());
			Login login = new Login(Env.getCtx());
			KeyNamePair[] roles = login.getRoles(userName, clientKeyNamePair, ROLE_TYPES_WEBSERVICE);
			boolean isValidRole = isValidRole(roleId, roles);
			if (!isValidRole)
				return unauthorized("Invalid role", userName);
			boolean isValidOrg = isValidOrg(organizationId, roleId, login);
			if (!isValidOrg)
				return unauthorized("Invalid organization", userName);
			Env.setContext(Env.getCtx(), Env.AD_CLIENT_ID, client.getAD_Client_ID());
			MUser user = MUser.get(Env.getCtx(), userName);
			Env.setContext(Env.getCtx(), Env.AD_USER_ID, user.getAD_User_ID());
			MRole role = MRole.get(Env.getCtx(), roleId);
			Env.setContext(Env.getCtx(), Env.AD_ROLE_ID, role.getAD_Role_ID());
			MOrg org = MOrg.get(organizationId);
			Env.setPredefinedVariables(Env.getCtx(), -1, MRole.getDefault().getPredefinedContextVariables());
			KeyNamePair knp = new KeyNamePair(org.getAD_Org_ID(), org.getName());
			KeyNamePair[] warehouses = login.getWarehouses(knp);
			JsonArray array = new JsonArray();
			if (warehouses != null) {
				for(KeyNamePair warehouse : warehouses) {
					JsonObject node = new JsonObject();
					node.addProperty("id", warehouse.getKey());
					node.addProperty("name", warehouse.getName());
					array.add(node);
				}
			}
			JsonObject json = new JsonObject();
			json.add("warehouses", array);
			return Response.ok(json.toString()).build();
		} catch (IllegalArgumentException e) {
			e.printStackTrace();
			return Response.status(Status.BAD_REQUEST).build();
		}
	}

	/* (non-Javadoc)
	 * @see org.idempiere.rest.api.v1.AuthService#getWarehouses(int, int, int)
	 */
	@Override
	public Response getClientLanguage(int clientId) {
		try {
			MClient client = MClient.get(Env.getCtx(), clientId);
			if (client == null || client.get_ID() <= 0) {
				return Response.status(Status.BAD_REQUEST).build();
			}
			
			JsonObject node = new JsonObject();
			node.addProperty("AD_Language", client.getAD_Language());
			return Response.ok(node.toString()).build();
		} catch (IllegalArgumentException e) {
			e.printStackTrace();
			return Response.status(Status.BAD_REQUEST).build();
		}
	}

	/* (non-Javadoc)
	 * @see org.idempiere.rest.api.v1.AuthService#updateLoginParameters(org.idempiere.rest.api.v1.LoginParameters)
	 */
	@Override
	public Response changeLoginParameters(LoginParameters parameters) {
		String userName = Env.getContext(Env.getCtx(), RequestFilter.LOGIN_NAME);
		String clients = Env.getContext(Env.getCtx(), RequestFilter.LOGIN_CLIENTS);
		return processLoginParameters(parameters, userName, clients);
	}

	/**
	 * @param parameters
	 * @param userName
	 * @param clients
	 * @param clientList 
	 * @return
	 */
	private Response processLoginParameters(LoginParameters parameters, String userName, String clients) {
		JsonObject responseNode = new JsonObject();
		Builder builder = JWT.create().withSubject(userName);
		String defaultLanguage = Language.getBaseAD_Language();
		int clientId = getClientId(parameters, responseNode);
		if (clientId == -2)
			return unauthorized("Client value not found", userName);
		boolean isValidClient = false;
		if (clientId == -1) {
			if (!Util.isEmpty(clients) && clients.indexOf(",") == -1) {			
				clientId = Integer.parseInt(clients);
				responseNode.addProperty("clientId", clientId);
				isValidClient = true;
			}
		} else {
			isValidClient = isValidClient(clientId, clients);
		}

		if (isValidClient) {
			addClientIdClaim(builder, clientId);
			MUser user = addUserIdClaim(userName, responseNode, builder);
			defaultLanguage = getPreferenceUserLanguage(user.getAD_User_ID());

			int roleId = getRoleId(clientId, parameters, responseNode);
			if (roleId == -2)
				return unauthorized("Role name not found", userName);
			else if (roleId == -3)
				return unauthorized("Role name not unique", userName);
			if (roleId == -1) {
				Login login = new Login(Env.getCtx());
				KeyNamePair[] roles = login.getRoles(userName, new KeyNamePair(clientId, MClient.get(clientId).getName()), ROLE_TYPES_WEBSERVICE);
				if (roles.length == 1) {
					roleId = roles[0].getKey();
					responseNode.addProperty("roleId", roles[0].getKey());
				}
				else
					return unauthorized("Missing roleId parameter", userName);
			}
			
			int orgId = getOrganizationId(clientId, parameters, responseNode);
			if (orgId == -2)
				return unauthorized("Organization name not found", userName);
			else if (orgId == -3)
				return unauthorized("Organization name not unique", userName);
			if (orgId == -1) {
				orgId = getOrgIdFirstOnly(user, MRole.get(Env.getCtx(), roleId));
				if (orgId == 0)
					return unauthorized("Missing organizationId parameter", userName);
				responseNode.addProperty("organizationId", orgId);
			}
			
			int warehouseId = getWarehouseId(clientId, parameters, responseNode);
			if (warehouseId == -2)
				return unauthorized("Warehouse name not found", userName);
			if (warehouseId == -1) {
				if (orgId > 0) {
					warehouseId = MOrgInfo.get(orgId).getM_Warehouse_ID();
					if (warehouseId > 0)
						responseNode.addProperty("warehouseId", warehouseId);
				}
			}
			String errorMessage = validateLoginParameters(userName, clientId, roleId, orgId, warehouseId);

			if (Util.isEmpty(errorMessage)) {
				builder.withClaim(LoginClaims.AD_Role_ID.name(), roleId);
				builder.withClaim(LoginClaims.AD_Org_ID.name(), orgId);
				if (orgId > 0 && warehouseId > 0)
					builder.withClaim(LoginClaims.M_Warehouse_ID.name(), warehouseId);
			} else {
				return unauthorized(errorMessage, userName);
			}
		} else {
			return unauthorized("Invalid clientId", userName);
		}
		if (parameters.getLanguage() != null) {
			for (String langAllowed : Env.getLoginLanguages()) {
				if (parameters.getLanguage().equals(langAllowed)) {
					defaultLanguage = parameters.getLanguage();
					break;
				}
			}
		}

		addLanguageClaim(responseNode, builder, defaultLanguage);

		createSession(builder);
		
		addMenuTreeIdResponse(responseNode);

		addIssuerAndExpiresAt(builder);
		try {
			createToken(responseNode, builder, true);
		} catch (IllegalArgumentException | JWTCreationException e) {
			e.printStackTrace();
			return Response.status(Status.BAD_REQUEST).build();
		}
		return Response.ok(responseNode.toString()).build();
	}

	/**
	 * Get client id from login parameters
	 * @param parameters
	 * @param responseNode
	 * @return client id or -1 (no clientId parameter) or -2 (client value not found)
	 */
	private int getClientId(LoginParameters parameters, JsonObject responseNode) {
		String s = parameters.getClientId();
		if (Util.isEmpty(s))
			return -1;
		int clientId = -1;
		if (Pattern.matches("\\d+", s.trim())) {
			clientId = Integer.parseInt(s.trim());
			try {
				PO.setCrossTenantSafe();
				if (MClient.get(clientId).get_ID() == clientId)
					return clientId;
			} finally {
				PO.clearCrossTenantSafe();
			}
		}
		Query query = new Query(Env.getCtx(), MClient.Table_Name, "IsActive='Y' AND Value=?", null);
		clientId = query.setParameters(s.trim()).firstId();
		if (clientId == -1) {
			return -2;
		} else {
			responseNode.addProperty("clientId", clientId);
			return clientId;
		}
	}

	/**
	 * Get organization id from login parameters
	 * @param clientId
	 * @param parameters
	 * @param responseNode
	 * @return organization id or -1 (no organizationId parameter) or -2 (organization name not found) or -3 (organization name not unique)
	 */
	private int getOrganizationId(int clientId, LoginParameters parameters, JsonObject responseNode) {
		String s = parameters.getOrganizationId();
		if (Util.isEmpty(s))
			return -1;
		int orgId = -1;
		if (Pattern.matches("\\d+", s.trim())) {
			orgId = Integer.parseInt(s.trim());
			if (MOrg.get(orgId) != null)
				return orgId;
		}
		Query query = new Query(Env.getCtx(), MOrg.Table_Name, "AD_Client_ID=? AND IsActive='Y' AND Name=?", null);
		try {
			orgId = query.setParameters(clientId, s.trim()).firstIdOnly();
		} catch (Exception e) {
			return -3;
		}
		if (orgId == -1) {
			return -2;
		} else {
			responseNode.addProperty("organizationId", orgId);
			return orgId;
		}
	}

	/**
	 * Get role id from login parameters
	 * @param clientId
	 * @param parameters
	 * @param responseNode 
	 * @return role id or -1 (no roleId parameter) or -2 (role name not found) or -3 (role name not unique)
	 */
	private int getRoleId(int clientId, LoginParameters parameters, JsonObject responseNode) {
		String s = parameters.getRoleId();
		if (Util.isEmpty(s))
			return -1;
		int roleId = -1;
		if (Pattern.matches("\\d+", s.trim())) {
			roleId = Integer.parseInt(s.trim());
			if (MRole.get(Env.getCtx(), roleId).get_ID() == roleId)
				return roleId;
		}
		Query query = new Query(Env.getCtx(), MRole.Table_Name, "AD_Client_ID=? AND IsActive='Y' AND Name=?", null);
		try {
			roleId = query.setParameters(clientId, s.trim()).firstIdOnly();
		} catch (Exception e) {
			return -3;
		}
		if (roleId == -1) {
			return -2;
		} else {
			responseNode.addProperty("roleId", roleId);
			return roleId;
		}
	}

	/**
	 * Get warehouse id from login parameters
	 * @param clientId
	 * @param parameters
	 * @param responseNode
	 * @return warehouse id or -1 (no warehouseId parameter) or -2 (warehouse name not found)
	 */
	private int getWarehouseId(int clientId, LoginParameters parameters, JsonObject responseNode) {
		String s = parameters.getWarehouseId();
		if (Util.isEmpty(s))
			return -1;
		int warehouseId = -1;
		if (Pattern.matches("\\d+", s.trim())) {
			warehouseId = Integer.parseInt(s.trim());
			if (warehouseId == 0 || MWarehouse.get(warehouseId) != null)
				return warehouseId;
		}
		Query query = new Query(Env.getCtx(), MOrg.Table_Name, "AD_Client_ID=? AND IsActive='Y' AND Name=?", null);
		warehouseId = query.setParameters(clientId, s.trim()).firstId();
		if (warehouseId == -1) {
			return -2;
		} else {
			responseNode.addProperty("warehouseId", warehouseId);
			return warehouseId;
		}
	}

	private void createToken(JsonObject responseNode, Builder builder, boolean createRefreshToken) {
		String token = builder.sign(Algorithm.HMAC512(TokenUtils.getTokenSecret()));
		responseNode.addProperty("token", token);
		if (createRefreshToken) {
			Timestamp absoluteExpiresAt = TokenUtils.getTokenAbsoluteExpiresAt();
			responseNode.addProperty("refresh_token", generateRefreshToken(token, null, absoluteExpiresAt));
		}
	}

	private void addIssuerAndExpiresAt(Builder builder) {
		Timestamp expiresAt = TokenUtils.getTokenExpiresAt();
		builder.withIssuer(TokenUtils.getTokenIssuer()).withExpiresAt(expiresAt).withKeyId(TokenUtils.getTokenKeyId());
	}

	private void addMenuTreeIdResponse(JsonObject responseNode) {
		MRole role = MRole.getDefault();
		int menuTreeId = role.getAD_Tree_Menu_ID();
		if (menuTreeId <= 0)
			menuTreeId = MClientInfo.get().getAD_Tree_Menu_ID();
		responseNode.addProperty("menuTreeId", menuTreeId);
	}

	private void addLanguageClaim(JsonObject responseNode, Builder builder, String defaultLanguage) {
		builder.withClaim(LoginClaims.AD_Language.name(), defaultLanguage);
		responseNode.addProperty("language", defaultLanguage);
	}

	private void addClientIdClaim(Builder builder, int clientId) {
		builder.withClaim(LoginClaims.AD_Client_ID.name(), clientId);
		Env.setContext(Env.getCtx(), Env.AD_CLIENT_ID, clientId);
	}

	private MUser addUserIdClaim(String userName, JsonObject responseNode, Builder builder) {
		MUser user = MUser.get(Env.getCtx(), userName);
		builder.withClaim(LoginClaims.AD_User_ID.name(), user.getAD_User_ID());
		responseNode.addProperty("userId", user.getAD_User_ID());
		return user;
	}

	private void createSession(Builder builder) {
		// Create AD_Session here and set the session in the token as another parameter
		MSession session = MSession.get(Env.getCtx());
		if (session == null){
			session = MSession.create(Env.getCtx());
			session.setWebSession("idempiere-rest");
			session.saveEx();
		}
		builder.withClaim(LoginClaims.AD_Session_ID.name(), session.getAD_Session_ID());
	}

	private boolean isValidClient(int clientID, String clients) {
		if (clientID >= 0 && !Util.isEmpty(clients)) {
			for (String allowedClient : clients.split(",")) {
				if (clientID == Integer.valueOf(allowedClient)) {
					return true;
				}
			}
		}
		return false;
	}

	private String validateLoginParameters(String userName, int clientId, int roleId, int orgId, int warehouseId) {
		MClient client = MClient.get(Env.getCtx(), clientId);
		KeyNamePair clientKeyNamePair = new KeyNamePair(client.getAD_Client_ID(), client.getName());
		Login login = new Login(Env.getCtx());
		KeyNamePair[] roles = login.getRoles(userName, clientKeyNamePair, ROLE_TYPES_WEBSERVICE);
		boolean isValidRole = isValidRole(roleId, roles);

		if (isValidRole) {
			boolean isValidOrg = isValidOrg(orgId, roleId, login);
			if (isValidOrg) {
				if (orgId > 0 && warehouseId > 0) {
					boolean warehouseValid = isValidWarehouse(orgId, warehouseId, login);
					if (!warehouseValid)
						return "Invalid warehouseId";
				}
			} else {
				return "Invalid organizationId";
			}
		} else {
			return "Invalid roleId";
		}

		return login.validateLogin(new KeyNamePair(orgId, ""));
	}

	private boolean isValidRole(int roleId, KeyNamePair[] roles) {
		if (roleId >= 0 && roles != null) {
			for (KeyNamePair roleAllowed : roles) {
				if (roleId == roleAllowed.getKey()) {
					return true;
				}
			}
		}
		return false;
	}

	private boolean isValidOrg(int orgId, int roleId, Login login) {
		if (orgId >= 0) {
			String userName = Env.getContext(Env.getCtx(), RequestFilter.LOGIN_NAME);
			MUser user = MUser.get(Env.getCtx(), userName);
			Env.setContext(Env.getCtx(), Env.AD_USER_ID, user.getAD_User_ID());
			MRole role = MRole.get(Env.getCtx(), roleId);
			KeyNamePair rolesKeyNamePair = new KeyNamePair(role.getAD_Role_ID(), role.getName());
			KeyNamePair[] orgs = login.getOrgs(rolesKeyNamePair);
			if (orgs != null) {
				for (KeyNamePair orgAllowed : orgs) {
					if (orgId == orgAllowed.getKey()) {
						return true;
					}
				}
			}
		}
		return false;
	}

	private boolean isValidWarehouse(int orgId, int warehouseId, Login login) {
		MOrg org = MOrg.get(orgId);
		KeyNamePair orgKeyNamePair = new KeyNamePair(org.getAD_Org_ID(), org.getName());
		KeyNamePair[] warehouses = login.getWarehouses(orgKeyNamePair);
		if (warehouses != null) {
			for (KeyNamePair allowedWarehouse : warehouses) {
				if (warehouseId == allowedWarehouse.getKey()) {
					return true;
				}
			}
		}
		return false;
	}

	/**
	 * Returns the user preference language
	 * if non exist - returns the client language
	 * */
	private String getPreferenceUserLanguage(int AD_User_ID) {
		Query query = new Query(Env.getCtx(),
    			MTable.get(Env.getCtx(), I_AD_Preference.Table_ID),
    			" Attribute=? AND AD_User_ID=? AND PreferenceFor = 'W'",
    			null);

    	MPreference preference = query.setOnlyActiveRecords(true)
    			.setParameters("Language", AD_User_ID)
    			.first();
   
    	return preference != null ? Language.getAD_Language(preference.getValue()) : MClient.get(Env.getCtx()).getAD_Language();
	}

	/* (non-Javadoc)
	 * @see org.idempiere.rest.api.v1.AuthService#getJWK()
	 */
	@Override
	public Response getJWK() {
		JsonObject jwks = new JsonObject();
		if (MSysConfig.getBooleanValue("IDEMPIERE_REST_EXPOSE_JWK", false)) {
			JsonArray keys = new JsonArray();
			JsonObject key = new JsonObject();
			try {
				key.addProperty("alg", Algorithm.HMAC512(TokenUtils.getTokenSecret()).getName());
			} catch (IllegalArgumentException e) {
				e.printStackTrace();
				return Response.status(Status.BAD_REQUEST).build();
			}
			key.addProperty("k", DatatypeConverter.printBase64Binary(TokenUtils.getTokenSecret().getBytes()));
			key.addProperty("kid", TokenUtils.getTokenKeyId());
			key.addProperty("kty", "oct");
			keys.add(key);

			jwks.add("keys", keys);
		}

		return Response.ok(jwks.toString()).build();
	}

	/**
	 * Refresh a token, returns a new auth token and a new refresh token
	 */
	@Override
	public Response tokenRefresh(RefreshParameters refresh) {
		String refreshToken = refresh.getRefresh_token();
		Integer refreshClientId = refresh.getClientId();
		if (refreshClientId == null && MSysConfig.getBooleanValue("REST_MANDATORY_CLIENT_ID_ON_REFRESH", true)) {
			return Response.status(Status.UNAUTHORIZED)
					.entity(new ErrorBuilder().status(Status.UNAUTHORIZED).title("Authenticate error").append("No clientId provided").build().toString())
					.build();
		}
		Integer refreshUserId = refresh.getUserId();
		if (refreshUserId == null && MSysConfig.getBooleanValue("REST_MANDATORY_USER_ID_ON_REFRESH", true)) {
			return Response.status(Status.UNAUTHORIZED)
					.entity(new ErrorBuilder().status(Status.UNAUTHORIZED).title("Authenticate error").append("No userId provided").build().toString())
					.build();
		}

		if (MRefreshToken.isParent(refreshToken)) {
			Env.setContext(Env.getCtx(), Env.AD_CLIENT_ID, 0);
			MRefreshToken.breachDetected(refreshToken);
			return Response.status(Status.UNAUTHORIZED)
					.entity(new ErrorBuilder().status(Status.UNAUTHORIZED).title("Authenticate error").append("Invalid refresh token").build().toString())
					.build();
		}

		Algorithm algorithm = Algorithm.HMAC512(TokenUtils.getTokenSecret());
		JWTVerifier verifier = JWT.require(algorithm)
		        .withIssuer(TokenUtils.getTokenIssuer())
		        .build(); //Reusable verifier instance

		// Verify the refresh token (signature)
		try {
			verifier.verify(refreshToken);
		} catch (JWTVerificationException e) {
			return Response.status(Status.UNAUTHORIZED)
					.entity(new ErrorBuilder().status(Status.UNAUTHORIZED).title("Authenticate error").append(e.getLocalizedMessage()).build().toString())
					.build();
		}

		// get the auth token from the refresh token
		MRefreshToken refreshTokenInDB = MRefreshToken.getValidForRefresh(refreshToken);
		if (refreshTokenInDB == null) {
			return Response.status(Status.UNAUTHORIZED)
					.entity(new ErrorBuilder().status(Status.UNAUTHORIZED).title("Authenticate error").append("Invalid refresh token").build().toString())
					.build();
		}
		if (refreshClientId != null && refreshTokenInDB.getAD_Client_ID() != refreshClientId) {
			return Response.status(Status.UNAUTHORIZED)
					.entity(new ErrorBuilder().status(Status.UNAUTHORIZED).title("Authenticate error").append("Invalid refresh token").build().toString())
					.build();
		}
		if (refreshUserId != null && refreshTokenInDB.getCreatedBy() != refreshUserId) {
			return Response.status(Status.UNAUTHORIZED)
					.entity(new ErrorBuilder().status(Status.UNAUTHORIZED).title("Authenticate error").append("Invalid refresh token").build().toString())
					.build();
		}

		String authToken = refreshTokenInDB.getToken();

		JWTVerifier decoder = JWT.require(algorithm)
		        .withIssuer(TokenUtils.getTokenIssuer())
		        .acceptExpiresAt(Instant.MAX.getEpochSecond()) // do not validate expiration of token
		        .build(); //Reusable verifier instance
		DecodedJWT jwt;
		try {
			jwt = decoder.verify(authToken);
		} catch (JWTVerificationException e) {
			return Response.status(Status.UNAUTHORIZED)
					.entity(new ErrorBuilder().status(Status.UNAUTHORIZED).title("Authenticate error").append(e.getLocalizedMessage()).build().toString())
					.build();
		}
		String userName = jwt.getSubject();

		Claim claim = jwt.getClaim(LoginClaims.AD_Client_ID.name());
		int clientId = -1;
		if (!claim.isNull() && !claim.isMissing())
			clientId = claim.asInt();

		claim = jwt.getClaim(LoginClaims.AD_User_ID.name());
		int userId = -1;
		if (!claim.isNull() && !claim.isMissing())
			userId = claim.asInt();

		claim = jwt.getClaim(LoginClaims.AD_Role_ID.name());
		int roleId = -1;
		if (!claim.isNull() && !claim.isMissing())
			roleId = claim.asInt();

		claim = jwt.getClaim(LoginClaims.AD_Org_ID.name());
		int orgId = -1;
		if (!claim.isNull() && !claim.isMissing())
			orgId = claim.asInt();

		claim = jwt.getClaim(LoginClaims.M_Warehouse_ID.name());
		int warehouseId = -1;
		if (!claim.isNull() && !claim.isMissing())
			warehouseId = claim.asInt();

		claim = jwt.getClaim(LoginClaims.AD_Language.name());
		String defaultLanguage = null;
		if (!claim.isNull() && !claim.isMissing())
			defaultLanguage = claim.asString();

		claim = jwt.getClaim(LoginClaims.AD_Session_ID.name());
		int sessionId = -1;
		if (!claim.isNull() && !claim.isMissing())
			sessionId = claim.asInt();

		JsonObject responseNode = new JsonObject();
		Builder builder = JWT.create().withSubject(userName);
		addClientIdClaim(builder, clientId);
		builder.withClaim(LoginClaims.AD_User_ID.name(), userId);
		builder.withClaim(LoginClaims.AD_Role_ID.name(), roleId);
		builder.withClaim(LoginClaims.AD_Org_ID.name(), orgId);
		if (orgId > 0 && warehouseId > 0)
			builder.withClaim(LoginClaims.M_Warehouse_ID.name(), warehouseId);
		builder.withClaim(LoginClaims.AD_Language.name(), defaultLanguage);
		builder.withClaim(LoginClaims.AD_Session_ID.name(), sessionId);

		addIssuerAndExpiresAt(builder);
		try {
			String token = builder.sign(Algorithm.HMAC512(TokenUtils.getTokenSecret()));
			responseNode.addProperty("token", token);
			responseNode.addProperty("refresh_token", generateRefreshToken(token, refreshToken, null));
		} catch (IllegalArgumentException | JWTCreationException e) {
			e.printStackTrace();
			return Response.status(Status.BAD_REQUEST).build();
		}
		return Response.ok(responseNode.toString()).build();
	}

	/**
	 * Generate a random refresh token
	 * @param previousRefreshToken 
	 * @return
	 */
	private String generateRefreshToken(String token, String previousRefreshToken, Timestamp absoluteExpiresAt) {
		SecureRandom secureRandom = new SecureRandom();
		byte[] randomBytes = new byte[32];
        secureRandom.nextBytes(randomBytes);
        StringBuilder hexString = new StringBuilder();
        for (byte b : randomBytes)
            hexString.append(String.format("%02x", b));
		Builder builder = JWT.create().withJWTId(hexString.toString());
		Timestamp inactiveExpiresAt = TokenUtils.getRefreshTokenExpiresAt();
		builder.withIssuer(TokenUtils.getTokenIssuer()).withKeyId(TokenUtils.getTokenKeyId());
		String refreshToken = builder.sign(Algorithm.HMAC512(TokenUtils.getTokenSecret()));

		// persist in database
		MRefreshToken refreshTokenInDB = new MRefreshToken(Env.getCtx(), PO.UUID_NEW_RECORD, null);
		refreshTokenInDB.setToken(token);
		refreshTokenInDB.setRefreshToken(refreshToken);
		refreshTokenInDB.setParentToken(previousRefreshToken);
		refreshTokenInDB.setExpiresAt(inactiveExpiresAt);
		refreshTokenInDB.setAbsoluteExpiresAt(absoluteExpiresAt);
		if (previousRefreshToken != null && absoluteExpiresAt == null) {
			MRefreshToken prt = MRefreshToken.get(previousRefreshToken);
			if (prt != null)
				refreshTokenInDB.setAbsoluteExpiresAt(prt.getAbsoluteExpiresAt());
		}
		refreshTokenInDB.save();

		return refreshToken;
	}

	/**
	 * Logout a token
	 */
	@Override
	public Response tokenLogout(LogoutParameters logout) {
		String token = logout.getToken();
		Algorithm algorithm = Algorithm.HMAC512(TokenUtils.getTokenSecret());
		JWTVerifier verifier = JWT.require(algorithm)
		        .withIssuer(TokenUtils.getTokenIssuer())
		        .acceptExpiresAt(Instant.MAX.getEpochSecond()) // do not validate expiration of token
		        .build(); //Reusable verifier instance

		// Verify the token (signature)
		DecodedJWT jwt;
		try {
			jwt = verifier.verify(token);
		} catch (JWTVerificationException e) {
			return Response.status(Status.UNAUTHORIZED)
					.entity(new ErrorBuilder().status(Status.UNAUTHORIZED).title("Authenticate error").append(e.getLocalizedMessage()).build().toString())
					.build();
		}

		Claim claim = jwt.getClaim(LoginClaims.AD_Session_ID.name());
		int sessionId = -1;
		if (!claim.isNull() && !claim.isMissing()) {
			sessionId = claim.asInt();
		} else {
			return Response.status(Status.NOT_FOUND)
					.entity(new ErrorBuilder().status(Status.NOT_FOUND).title("AD_Session_ID not found").build().toString())
					.build();
		}
		claim = jwt.getClaim(LoginClaims.AD_User_ID.name());
		if (!claim.isNull() && !claim.isMissing()) {
			int userId = claim.asInt();
			Env.setContext(Env.getCtx(), Env.AD_USER_ID, userId);
		}
		Env.setContext(Env.getCtx(), Env.AD_SESSION_ID, sessionId);
		MSession session = new MSession(Env.getCtx(), sessionId, null);
		session.logout();
		RestUtils.removeSavedCtx(sessionId);

		MRefreshToken.logout(token);

		JsonObject okResponse = new JsonObject();
		okResponse.addProperty("summary", "OK");
		
		return Response.ok(okResponse.toString()).build();
	}
	
	public void setRequest(HttpServletRequest request) {
	    this.request = request;
	}

}
