package dev.rusatom.keycloak.modules.esia;

import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;
import java.util.stream.Collectors;

import javax.ws.rs.GET;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;

import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider;
import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper;
import org.keycloak.broker.provider.AuthenticationRequest;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.broker.social.SocialIdentityProvider;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import dev.rusatom.keycloak.modules.CliSigner;
import dev.rusatom.keycloak.modules.MessageUtils;
import dev.rusatom.keycloak.modules.Signer;
import dev.rusatom.keycloak.modules.StringUtils;

/**
 *
 * @author Ivan Kovalenko, Marina Sergeeva
 */

public class EsiaIdentityProvider extends AbstractOAuth2IdentityProvider<EsiaIdentityProviderConfig>
		implements SocialIdentityProvider<EsiaIdentityProviderConfig> {
	private static final String ESIA_DEFAULT_PHONE_DOMAIN = "phone.esia.gosuslugi.ru";

    private static final String HEADER_IF_NONE_MATCH = "If-None-Match";

	public static final String OAUTH2_GRANT_TYPE_CLIENT_CREDENTIALS = "client_credentials";

	public static final String ESIA_PREFIX = "esia.";
	public static final String ESIA_JSON_PREFIX = ESIA_PREFIX + "_JSON.";
	public static final String ESIA_PREFIX_JSON = ESIA_JSON_PREFIX;
	/**
	 * Запрос кода подтверждения.
	 */
	private static final String AUTH_PATH = "/aas/oauth2/ac";

	/**
	 * Обмен кода подтверждения на токен.
	 */
	private static final String TOKEN_PATH = "/aas/oauth2/te";


	/**
	 * Запрос контактов пользователя.
	 */

	public static final String OAUTH2_PARAMETER_TIMESTAMP = "timestamp";

	public static final String OAUTH2_PARAMETER_TOKEN_TYPE = "token_type";
	public static final String OAUTH2_PARAMETER_TOKEN_TYPE_VALUE = "Bearer";

	protected static final String timestampFormat = "yyyy.MM.dd HH:mm:ss Z";

	private static final String OIDC_PARAMETER_ACCESS_TYPE = "access_type";

	// TODO async esia data retrieval 
	// private static final String ACCESS_TYPE_OFFLINE = "offline";
	private static final String ACCESS_TYPE_ONLINE = "online";

	private static final String SCOPE_OPENID = "openid";
	private static final String ESIA_OID = "urn:esia:sbj_id";
	public static final String PROVIDER_NAME = "Esia";

	/**
	 * Создает объект OAuth-авторизации через
	 * <a href="https://esia.gosuslugi.ru">ESIA</a>.
	 *
	 * @param session Сессия Keycloak.
	 * @param config  Конфигурация OAuth-авторизации.
	 */
	public EsiaIdentityProvider(KeycloakSession session, EsiaIdentityProviderConfig config) {
		super(session, config);
		config.setAuthorizationUrl(config.getEsiaUrl() + AUTH_PATH);
		config.setTokenUrl(config.getEsiaUrl() + TOKEN_PATH);

		String defaultScope = config.getAllScopes();
		if (!defaultScope.contains(SCOPE_OPENID)) {
			config.setDefaultScope((SCOPE_OPENID + " " + defaultScope).trim());
		}
	}


	public KeycloakSession getSession() {
		return session;
	}

	private String getClientSecret(String state) {
		String scope = getConfig().getDefaultScope();
		String client_id = getConfig().getClientId();
		String timestamp = getTimestampString();
		return getClientSecret(scope, timestamp, client_id, state);
	}
		
	private String getClientSecret(String scope, String timestamp, String client_id, String state) {
		String secret = "";
		try {
			Signer signer = new CliSigner();
			logger.debug(scope + timestamp + client_id + state);
			secret = signer.signString(scope + timestamp + client_id + state);
		} catch (Exception e) {
			logger.error("Signature failed! " + e.getMessage());
			throw new RuntimeException(e);
		}
		return secret;
	}

	public String getTimestampString() {
		SimpleDateFormat sdf = new SimpleDateFormat(timestampFormat);
		return sdf.format(new Date());
	}

	private String getState() {
		return UUID.randomUUID().toString();
	}

	@Override
	protected UriBuilder createAuthorizationUrl(AuthenticationRequest request) {
		// Заменяем параметр state в request на uuid, так как ЕСИА принимает только в
		// формате uuid
		// и добавляем state, сгенерированный keycloack в redirect url, поскольку далее
		// он используется keycloack
		// при вызове this.callback.authenticated(federatedIdentity);
		String scope = getConfig().getDefaultScope();
		String timestampString = getTimestampString();
		String clientId = getConfig().getClientId();
		String uuidState = getState();
		
		String clientSecret = getClientSecret(scope, timestampString, clientId, uuidState);
		
		String redirectUriWithKeycloakState = request.getRedirectUri() + "?kState=" + request.getState().getEncoded();
		UriBuilder builder = super.createAuthorizationUrl(request);

		builder.replaceQueryParam(OAUTH2_PARAMETER_REDIRECT_URI, redirectUriWithKeycloakState);
		builder.queryParam(OAUTH2_PARAMETER_TIMESTAMP, timestampString);
		builder.queryParam(OAUTH2_PARAMETER_CLIENT_SECRET, clientSecret);
		builder.queryParam(OIDC_PARAMETER_ACCESS_TYPE, ACCESS_TYPE_ONLINE);
		builder.replaceQueryParam(OAUTH2_PARAMETER_STATE, uuidState);
		return builder;
	}
	
	@Override
	protected String getDefaultScopes() {
		return SCOPE_OPENID;
	}

	@Override
	public Object callback(RealmModel realm, AuthenticationCallback callback, EventBuilder event) {
		return new Endpoint(callback, realm, event);
	}

	public SimpleHttp fetchJson(String accessToken, String url) {
		return buildUserInfoRequest(accessToken, url);
	}


	protected String[] checkScopes(String myScopes, String incomingScopes) {
		ArrayList<String> calculatedScopes = new ArrayList<String>();
		// DISCUSS switch to loop over myScopes and contains scope+? check ?
		for(String scopeIncoming: incomingScopes.split(" ")) {
			for(String scopeMy: myScopes.split(" ")) {
				//logger.debug("checking scope : " + scopeIncoming + " vs " + scopeMy);
				if(scopeMy.equals(scopeIncoming) || scopeIncoming.startsWith(scopeMy+"?")) {
					//logger.debug("^^^^^^^^^^^^^^^^^^^^ match");
					calculatedScopes.add(scopeMy);
				}
			}
		}
		logger.debug("My Scopes is : " + calculatedScopes.toString());
		return calculatedScopes.toArray(new String[0]);
	}

	public String getAttribute(BrokeredIdentityContext user, String k, String prefix) {
		return user.getUserAttribute(prefix + k.toUpperCase());
		//user.setUserAttribute(k.toUpperCase(), v);			
	}

	public void setAttribute(BrokeredIdentityContext user, String k, String v, String prefix) {
		user.setUserAttribute(prefix + k.toUpperCase(), v);
		//user.setUserAttribute(k.toUpperCase(), v);			
	}
	
	static final String ESIA_LAST_NAME = "lastName";
	static final String ESIA_FIRST_NAME = "firstName";

	static final String HEADER_ACCEPT = "Accept";

	private static final String ESIA_ETAG_PREFIX = ESIA_PREFIX + "_ETAG.";

	private static final String ESIA_SERVICE_PREFIX = ESIA_PREFIX + "_INT.";

	protected ObjectMapper oM = new ObjectMapper();

	String getUrl(String path, Object... args) {
		return String.format(getConfig().getEsiaUrl() + path, args);
	}

	void setEmailFromPhone(BrokeredIdentityContext user, String phone){
		String phoneStr =  phone.replaceAll("[^0-9]", "");
		String phoneDomain = System.getenv("ESIA_PHONE_DOMAIN");
		if (StringUtils.isNullOrEmpty(phoneDomain)){
			phoneDomain = ESIA_DEFAULT_PHONE_DOMAIN;
		}
		user.setEmail(phoneStr + "@" + phoneDomain);
	}
	
	String prepareAcceptHeader(String acceptHeader) {
		return acceptHeader.replace(EsiaIdentityProviderConfig.ESIA_TEST_URL, getConfig().getEsiaUrl());
	}
	
	String getEtag(BrokeredIdentityContext user, String name) {
			logger.debug("Searching etag " + ESIA_ETAG_PREFIX + name.toUpperCase());
			return user.getUserAttribute(ESIA_ETAG_PREFIX + name.toUpperCase());
	}
	void setEtag(BrokeredIdentityContext user, String name, String eTag) {
		if (eTag!=null && !eTag.equals("")) {
			setAttribute(user, name, eTag, ESIA_ETAG_PREFIX);
		}
	}

	
	private JsonNode fetchJson(String accessToken, String url, BrokeredIdentityContext user, String name,
			String acceptHeader) throws IOException  {
		SimpleHttp sh =  fetchJson(accessToken, url)
				.header(HEADER_ACCEPT, prepareAcceptHeader(acceptHeader));
		int httpStatus = sh.asResponse().getStatus();
		logger.debug(String.format("Response http code is  %s", httpStatus));
		if (httpStatus == 304) {
			return null;
		} else if (httpStatus < 299 && httpStatus >= 200) {
			try {
				String incomingEtag = sh.asResponse().getFirstHeader("Etag");
				logger.debug("Remote eTag is " + incomingEtag);
				setEtag(user, ESIA_JSON_PREFIX.replace(ESIA_PREFIX, "") + name, incomingEtag);
			}catch(Exception e) {
				logger.debug(e.getLocalizedMessage());
				logger.info(String.format("No etag for %s", name));
			}
			JsonNode esiaReturn = sh.asJson();
			setAttribute(user, name, oM.writeValueAsString(esiaReturn), ESIA_JSON_PREFIX);
			logger.debug(String.format("Response for %s is %s", name,  oM.writeValueAsString(esiaReturn))) ;
			return esiaReturn;
		} 
		return null;
	}
	
	private BrokeredIdentityContext createBrokeredIdentityContext(String oId, String accessToken, String scopes)
			throws IOException {
		BrokeredIdentityContext user = new BrokeredIdentityContext(oId);
		setAttribute(user, "scopes", scopes, ESIA_SERVICE_PREFIX);
		setAttribute(user, "subject_id", oId, ESIA_PREFIX);

		JsonNode esiaReturn = null;
		
		// Profile 
		PROFILE: {
			final String acceptHeader = "application/json; schema=\"https://esia-portal1.test.gosuslugi.ru/rs/model/prn/Person-1\"";
			final String path = "/rs/prns/%s";
			final String name = "PROFILE";
			final String myScopes = "fullname birthdate gender birthplace snils inn";
	
			if(checkScopes(myScopes, scopes).length > 0) {

				esiaReturn = fetchJson(accessToken, getUrl(path, oId), user, name, acceptHeader);
				if (esiaReturn == null) {
					break PROFILE;
				}
				String etag = getJsonProperty(esiaReturn,"eTag");
				user.setLastName(getJsonProperty(esiaReturn, ESIA_LAST_NAME));
				user.setFirstName(getJsonProperty(esiaReturn, ESIA_FIRST_NAME));
				for(String pI : "snils inn gender birthDate middleName citizenship lastName firstName".split(" ")) {
					setAttribute(user, pI, getJsonProperty(esiaReturn, pI), ESIA_PREFIX);
					setEtag(user, pI, etag);
					logger.debug(pI + " = " + getJsonProperty(esiaReturn, pI));
				}
			}		
		}
		// Addresses
		ADDRESSES: {
			final String acceptHeader = "application/json; schema=\"https://esia-portal1.test.gosuslugi.ru/rs/model/addrs/Addresses-1\"";
			final String myScopes = "id_doc contacts";
//			final String path = "/rs/prns/%s/addrs?embed=(elements)";
//			final String path = "/esia-rs/api/public/v5/prns/%s?embed=(addresses.elements)";
			final String path = "/esia-rs/api/public/v5/prns/%s/addrs?embed=(elements)";
			final String name = "ADDRS";
			if(checkScopes(myScopes, scopes).length > 0) {
				esiaReturn = fetchJson(accessToken, getUrl(path, oId), user, name, acceptHeader);
				if (esiaReturn == null) {
					break ADDRESSES;
				}
			}		
		}
		// Contacts
		CONTACTS: {
			final String acceptHeader = "application/json; schema=\"https://esia-portal1.test.gosuslugi.ru/rs/model/ctts/Contacts-1\"";
			final String myScopes = "contacts email mobile";
			final String path = "/rs/prns/%s/ctts?embed=(elements)";
			final String name = "CONTACTS";

			if(checkScopes(myScopes, scopes).length > 0) {
				esiaReturn = fetchJson(accessToken, getUrl(path, oId), user, name, acceptHeader);
				if (esiaReturn == null) {
					break CONTACTS;
				}			
				/* Not used anymore */
				String etag = getJsonProperty(esiaReturn,"eTag");

				esiaReturn.get("elements").forEach(e -> {
					switch(getJsonProperty(e, "type")) {
					case "EML":{
							user.setEmail(getJsonProperty(e, "value"));
							setAttribute(user, "email", getJsonProperty(e, "value"), ESIA_PREFIX);
							/* Using all json structure eTag is wrong - on change email changes only element eTag, not all json structure eTag */
							setEtag(user, "email", getJsonProperty(e, "eTag"));
							break;
						}
					case "MBT": {
							setAttribute(user, "mobile", getJsonProperty(e, "value"), ESIA_PREFIX);
							/* Using all json structure eTag is wrong - on change mobile changes only element eTag, not all json structure eTag */
							setEtag(user, "mobile", getJsonProperty(e, "eTag"));
							break;
						}
					case "PHN": {
							setAttribute(user, "phone", getJsonProperty(e, "value"), ESIA_PREFIX);
							/* Using all json structure eTag is wrong - on change phone changes only element eTag, not all json structure eTag */
							setEtag(user, "phone", getJsonProperty(e, "eTag"));
							break;
						}
					}
				});
				if (StringUtils.isNullOrEmpty(user.getEmail())) {
					if (StringUtils.isNullOrEmpty(getAttribute(user, "mobile", ESIA_PREFIX))) {
						if (StringUtils.isNullOrEmpty(getAttribute(user, "phone", ESIA_PREFIX))) {
							throw new IllegalArgumentException(MessageUtils.email(PROVIDER_NAME));
						}else{
							setEmailFromPhone(user, getAttribute(user, "phone", ESIA_PREFIX));
							logger.warn("User " + user.getId() + " was identified with email " + user.getEmail());
						}
					}else{
						setEmailFromPhone(user, getAttribute(user, "mobile", ESIA_PREFIX));
						logger.warn("User " + user.getId() + " was identified with email " + user.getEmail());
					}
				}
				
				logger.debug("User " + user.getId() + " was identified with email " + user.getEmail());
				AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, esiaReturn, getConfig().getAlias());
			}
		}
		// Docs
		DOCS: {
			final String acceptHeader = "application/json; schema=\"https://esia-portal1.test.gosuslugi.ru/rs/model/docs/Documents-1\"";
			final String myScopes = "docs id_doc medical_doc military_doc foreign_passport_doc drivers_licence_doc birth_cert_doc residence_doc temporary_residence_doc";
			final String path = "/rs/prns/%s/docs?embed=(elements)";
			final String name = "DOCS";

			if(checkScopes(myScopes, scopes).length > 0) {
				esiaReturn = fetchJson(accessToken, getUrl(path, oId), user, name, acceptHeader);
				if (esiaReturn == null) {
					break DOCS;
				}
				String etag = getJsonProperty(esiaReturn,"eTag");
				esiaReturn.get("elements").forEach(e -> {
					if ("RF_PASSPORT".equals(getJsonProperty(e, "type"))) {
						setAttribute(user, "rf_passport", String.format("%s %s", getJsonProperty(e,"series"), getJsonProperty(e,"number")), ESIA_PREFIX);
						setEtag(user, "rf_passport", etag);
					}
					if ("RF_DRIVING_LICENSE".equals(getJsonProperty(e, "type"))) {
						setAttribute(user, "rf_driving_license", String.format("%s %s", getJsonProperty(e,"series"), getJsonProperty(e,"number")), ESIA_PREFIX);
						setEtag(user, "rf_driving_license", etag);
					}
				});
			}
		}
		// Vehicles
		VEHICLES: {
			final String acceptHeader = "application/json; schema=\"https://esia-portal1.test.gosuslugi.ru/rs/model/vhls/Vehicles-1\"";
			final String myScopes = "vehicles";
			final String path = "/rs/prns/%s/vhls?embed=(elements)";
			final String name = "VEHICLES";

			if(checkScopes(myScopes, scopes).length > 0) {
				esiaReturn = fetchJson(accessToken, getUrl(path, oId), user, name, acceptHeader);
				if (esiaReturn == null) {
					break VEHICLES;
				}
			}

		}
		// Kids
		KIDS: {
			final String acceptHeader = "application/json; schema=\"https://esia-portal1.test.gosuslugi.ru/rs/model/kids/Kids-1\"";
			final String myScopes = "kids kid_fullname kid_birthdate kid_gender kid_snils kid_inn kid_birth_cert_doc kid_medical_doc kid_id_doc";
			final String path = "/rs/prns/%s/kids?embed=(elements)";
			final String name = "KIDS";

			if(checkScopes(myScopes, scopes).length > 0) {
				esiaReturn = fetchJson(accessToken, getUrl(path, oId), user, name, acceptHeader);
				if (esiaReturn == null) {
					break KIDS;
				}
			}
		}
		// Roles
		ROLES: {
			
			final String acceptHeader = "application/json; schema=\"https://esia-portal1.test.gosuslugi.ru/rs/model/roles/Roles-1\"";
			final String myScopes = "usr_org";
			final String path = "/rs/prns/%s/roles?embed=(elements)";
			final String name = "ORG-ROLES";

			if(checkScopes(myScopes, scopes).length > 0) {
				try {
					esiaReturn = fetchJson(accessToken, getUrl(path, oId)).header(HEADER_ACCEPT, prepareAcceptHeader(acceptHeader)).asJson();
					setAttribute(user, name, oM.writeValueAsString(esiaReturn), ESIA_JSON_PREFIX);
				}catch(Exception e) {
					logger.warn("Person roles failed");
					logger.debug(esiaReturn);
					break ROLES;
				}
			}
		}
		// Orgs 
		ORGS: {
			final String acceptHeader = "application/json; schema=\"https://esia-portal1.test.gosuslugi.ru/rs/model/orgs/Organizations-1\"";
			final String myScopes = "usr_org";
			final String path = "/rs/prns/%s/orgs?embed=(elements)";
			final String name = "ORGS";
			if(checkScopes(myScopes, scopes).length > 0) {
				esiaReturn = fetchJson(accessToken, getUrl(path, oId), user, name, acceptHeader);
				if (esiaReturn == null) {
					break ORGS;
				}
				if( getJsonProperty(esiaReturn,"error")!=null) {
					logger.warn("Some error occured with user organizations");
					logger.debug(esiaReturn);
					break ORGS;
				}

				// List person organizations oids			
				String[] orgOids = esiaReturn.findValuesAsText("oid").toArray(new String[0]);
				if (orgOids !=null && orgOids.length < 1) {
					logger.info("No orgs are there for user");
					logger.debug(esiaReturn);
					break ORGS;
				}
				
				// new token for ORGS
				OrgTools ot = new OrgTools();

				// {"error_description":"ESIA-007019: OAuthErrorEnum.noGrants","state":"eeb1a87e-1166-4885-a71c-823f26b91622","error":"invalid_scope"}
				String myOrgScopes = getConfig().getOrgScopes();
				if (myOrgScopes != null && myOrgScopes.split(" ").length < 1) {
					logger.info("No org scopes defined in default scope");
					logger.debug(myOrgScopes);
					break ORGS;
				}
				SimpleHttp response = ot.generateOrgTokenRequest(orgOids, myOrgScopes);
				String responseBody = response.asString();

				if (response.asStatus() != 200) {
					logger.warn("Invalid scopes for org scopes '" + myOrgScopes + "' and " + String.join(",",  orgOids) );
					logger.debug(responseBody);
					break ORGS;
				}
				String orgToken = extractTokenFromResponse(responseBody);
				JsonNode orgsInfo = parseAccessToken(responseBody);
				if( getJsonProperty(orgsInfo, "scope")==null) {
					logger.warn("No scopes returned for us");
					logger.debug(orgsInfo);
					break ORGS;
				}
				
				// TODO check whether our scopes are in sync with scopes granted 
				String incomingOrgScopes = getJsonProperty(orgsInfo,"scope");
				ot.checkOrgScopes(myOrgScopes, orgOids, incomingOrgScopes);
				logger.debug(incomingOrgScopes);
				
				for(String orgOid : orgOids) {
						for(OrgEndpoint orgData : ot.getPaths()) {
							 if (orgData.checkScope(incomingOrgScopes, orgOid)) {
								 final String url = getUrl(orgData.path, orgOid);
								 String key = String.format("%s.%s.%s", name, orgOid, orgData.type);
								 try {
									 JsonNode result = fetchJson(orgToken, url, user, key, orgData.acceptHeader);
									 if(result == null) {
										 logger.warn(key);
										 logger.warn(result);

										 continue;
									 }
								 }catch(Exception i) {
									 logger.warn("Org " + orgOid + " request failed " + url);
									 logger.warn(i);
								 }
							 }
						}
					
			    };
			}
		}
		// Avatars
		// TODO handle download binary value of avatar picture
		AVATARS: {
			final String acceptHeader = "application/json";
			final String myScopes = "usr_avt";
			final String path = "/esia-rs/api/public/v1/pso/%s/avt/%s";
			final String name = "AVATAR";
			final String defaultFormat = "square";
			// TODO handle avatar with two pictures
			//final String[] formats = {"circle", defaultFormat};

			if(checkScopes(myScopes, scopes).length > 0) {
				try {
					esiaReturn = fetchJson(accessToken, getUrl(path, oId, defaultFormat), user, name, acceptHeader);
					if (esiaReturn == null) {
						break AVATARS;
					}
					String avatarUrl = getConfig().getEsiaUrl() + getJsonProperty(esiaReturn,"url");
					logger.info("Avatar was found in " + avatarUrl);
					setAttribute(user, name + "." + defaultFormat, avatarUrl, ESIA_PREFIX );
					
				}catch(Exception e) {
						logger.warn("Avatar was not found - " + oId);
						logger.debug(e);
						break AVATARS;
				}
			}
		}

		user.setUsername(user.getEmail());

		return user;
	}
	private class OrgTools {
		
		final static String ESIA_ORG_SCOPE_TEMPLATE = "http://esia.gosuslugi.ru/%s?org_oid=%s";

		private String getOrgScopes(String scopes, String[] orgIds) {
			String scope = Arrays.stream(scopes.split(" ")).map(e -> Arrays.stream(orgIds)
					.map(orgId -> String.format(ESIA_ORG_SCOPE_TEMPLATE, e, orgId)).collect(Collectors.joining(" ")))
					.collect(Collectors.joining(" "));
			logger.debug(scope);
			return scope;
		}
		
		public boolean checkOrgScopes(String myOrgScopes, String[] orgOids, String incomingOrgScopesString) {
			String[] myScopes = getOrgScopes(myOrgScopes, orgOids).split(" ");
			String[] incomingOrgScopes = incomingOrgScopesString.split(" ");
			Arrays.sort(myScopes);
			Arrays.sort(incomingOrgScopes);
			logger.debug("requested Org Scopes " + String.join(" ", myScopes));
			logger.debug("returned Org Scopes " + String.join(" ", incomingOrgScopes));			
			return String.join(" ", myScopes).equalsIgnoreCase(String.join(" ", incomingOrgScopes));
		}
		public SimpleHttp generateOrgTokenRequest(String[] orgIds, String scopes) {
			String state = getSession().getAttribute(ESIA_STATE, String.class);
			String authorizationCode = getSession().getAttribute(ESIA_AUTH_CODE, String.class);

			String timestampString = getTimestampString();
			String myScopes = getOrgScopes(scopes, orgIds);
			String clientId = getConfig().getClientId();
			String clientSecret = getClientSecret(myScopes,timestampString, clientId, state);
			return SimpleHttp.doPost(getConfig().getTokenUrl(), getSession())
					.param(EsiaIdentityProvider.OAUTH2_PARAMETER_CLIENT_ID, clientId)
					.param(EsiaIdentityProvider.OAUTH2_PARAMETER_CODE, authorizationCode)
					.param(EsiaIdentityProvider.OAUTH2_PARAMETER_GRANT_TYPE,
							EsiaIdentityProvider.OAUTH2_GRANT_TYPE_CLIENT_CREDENTIALS)
					.param(EsiaIdentityProvider.OAUTH2_PARAMETER_STATE, state)
					.param(EsiaIdentityProvider.OAUTH2_PARAMETER_SCOPE, myScopes)
					.param(EsiaIdentityProvider.OAUTH2_PARAMETER_TIMESTAMP, timestampString)
					.param(EsiaIdentityProvider.OAUTH2_PARAMETER_TOKEN_TYPE,
							EsiaIdentityProvider.OAUTH2_PARAMETER_TOKEN_TYPE_VALUE)
					.param(EsiaIdentityProvider.OAUTH2_PARAMETER_CLIENT_SECRET, clientSecret);
		}

		public  ArrayList<OrgEndpoint> getPaths (){
			ArrayList<OrgEndpoint> orgs = new ArrayList<OrgEndpoint>();
			orgs.add(new OrgEndpoint() {{ 
				type = "profile"; 
				path = "/rs/orgs/%s";
				scope = "org_shortname org_fullname org_type org_ogrn org_inn org_leg org_kpp org_agencyterrange org_agencytype org_oktmo";
				}});
			orgs.add(new OrgEndpoint() {{ 
				type = "contacts"; 
				path = "/rs/orgs/%s/ctts?embed=(contacts.elements)";
				scope = "org_ctts";
				acceptHeader = "application/json; schema=\"https://esia-portal1.test.gosuslugi.ru/rs/model/ctts/Contacts-1\"";
				}});
			orgs.add(new OrgEndpoint() {{ 
				type = "addresses"; 
				path = "/rs/orgs/%s/addrs?embed=(elements)";
				scope = "org_addrs";
				acceptHeader = "application/json; schema=\"https://esia-portal1.test.gosuslugi.ru/rs/model/addrs/Addresses-1\"";
				}});
			orgs.add(new OrgEndpoint() {{ 
				type = "vehicles"; 
				path = "/rs/orgs/%s/vhls?embed=(vehicles.elements)";
				scope = "org_vhls";
				acceptHeader = "application/json; schema=\"https://esia-portal1.test.gosuslugi.ru/rs/model/vhls/Vehicles-1\"";
				}});
			orgs.add(new OrgEndpoint() {{ 
				type = "branches"; 
				path = "/rs/orgs/%s/brhs?embed=(branches.elements)";
				scope = "org_brhs org_brhs_ctts org_brhs_addrs";
				}});
			/* TODO Need to get token for this very url
			orgs.add(new OrgEndpoint() {{ 
				name = "employees"; 
				path = "/rs/orgs/%s/emps?embed=(elements.person)";
				scope = "org_emps";
				}});
			*/
			return orgs;
		}
	}
	
	class OrgEndpoint {
		String type;
		String path;
		String scope;
		String acceptHeader = "application/json";
		
		public boolean checkScope(String incomingOrgScopes, String orgOid) {
			for(String s: scope.split(" ")) {
				if(incomingOrgScopes.contains(String.format(OrgTools.ESIA_ORG_SCOPE_TEMPLATE, s, orgOid))){
					return true;
				}
			}
			return false;
		}
	}
		
	public JsonNode parseAccessToken(String accessToken) throws IOException {
		logger.debug("Access token is " + accessToken);
		String[] res = accessToken.split("\\.");
		if (res.length < 2) {
			throw new RuntimeException("Invalid AccessToken value.");
		}
		String decodedUrl = new String(Base64.getUrlDecoder().decode(res[1]));
		ObjectMapper objectMapper = new ObjectMapper();
		JsonNode userInfo = objectMapper.readTree(decodedUrl);
		return userInfo;
	}
	
	@Override
    public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, BrokeredIdentityContext context) {
		logger.debug("Called updateBrokeredUser with" + context.getUsername());
		context.getContextData().entrySet().stream()		
		.filter(x -> 
			 x.getKey().startsWith(Constants.USER_ATTRIBUTES_PREFIX + ESIA_PREFIX) && !x.getKey().startsWith(Constants.USER_ATTRIBUTES_PREFIX + ESIA_ETAG_PREFIX)
		)	
		.forEach(
				x -> {
					String userKey = x.getKey().replaceFirst(Constants.USER_ATTRIBUTES_PREFIX, "");
					String keyEtag = userKey.replace(ESIA_PREFIX, ESIA_ETAG_PREFIX);
					String storedEtag = user.getFirstAttribute(keyEtag);
					String incomingEtag = context.getUserAttribute(keyEtag);
					if(incomingEtag != null && storedEtag !=null && incomingEtag.equalsIgnoreCase(storedEtag)) {
						logger.debug(String.format("Not updating %s - etag is the same %s", userKey, storedEtag ));
					}else {
						logger.debug(String.format("Etag do not match for %s - %s - %s ", userKey, storedEtag, incomingEtag));
						user.setSingleAttribute(userKey, context.getUserAttribute(userKey));
						user.setSingleAttribute(keyEtag, incomingEtag);
						if (userKey.equals(ESIA_PREFIX+ESIA_LAST_NAME)) {
						       user.setLastName(context.getLastName());
						}
						if (userKey.equals(ESIA_PREFIX+ESIA_FIRST_NAME)) {
						       user.setFirstName(context.getFirstName());
						}
						
					}
				}
       );
    }
	
	@Override
	protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) {
		BrokeredIdentityContext user;

		try {
			JsonNode userInfo = parseAccessToken(accessToken);			
			logger.debug("doGetFederatedIdentity - userInfo");
			logger.debug(userInfo.toString());
			String oId = getJsonProperty(userInfo, ESIA_OID);
			logger.info("Returned scopes : " + getJsonProperty(userInfo, "scope"));
			user = createBrokeredIdentityContext(oId, accessToken, getJsonProperty(userInfo, "scope"));
			logger.debug("doGetFederatedIdentity - user");
			logger.debug(user.toString());

		} catch (IOException e) {
			logger.error("Unable to get esia user id" + e.getMessage());
			throw new RuntimeException(e);
		}
		return user;
	}


	public static final String ESIA_AUTH_CODE = "ESIA_AUTH_CODE";
	public static final String ESIA_STATE = "ESIA_STATE";
	
	public String extractTokenFromResponse(String response) {
		 // TODO validate marker identificacii section B.6.4 of technical recommendations
		 logger.debug("Token response : " + response);
		 return extractTokenFromResponse(response, OAUTH2_PARAMETER_ACCESS_TOKEN);
	 }
	
	protected class Endpoint extends AbstractOAuth2IdentityProvider<EsiaIdentityProviderConfig>.Endpoint {

		public Endpoint(AuthenticationCallback callback, RealmModel realm, EventBuilder event) {
			super(callback, realm, event);
		}

		@GET
		public Response authResponse(@QueryParam("kState") String kState, @QueryParam("state") String state,
				@QueryParam("code") String authorizationCode, @QueryParam("error") String error) {
			session.setAttribute(ESIA_AUTH_CODE, authorizationCode);
			session.setAttribute(ESIA_STATE, state);
			return super.authResponse(kState, authorizationCode, error);
		}

		@Override
		public SimpleHttp generateTokenRequest(String authorizationCode) {
			String state = getState();
			String scope = getConfig().getPersonScopes();
			String timestampString = getTimestampString();
			String clientId = getConfig().getClientId();
			
			String clientSecret = getClientSecret(scope, timestampString, clientId, state);

			return SimpleHttp.doPost(getConfig().getTokenUrl(), session)
					.param(OAUTH2_PARAMETER_CLIENT_ID, getConfig().getClientId())
					.param(OAUTH2_PARAMETER_CODE, authorizationCode)
					.param(OAUTH2_PARAMETER_GRANT_TYPE, OAUTH2_GRANT_TYPE_AUTHORIZATION_CODE)
					.param(OAUTH2_PARAMETER_CLIENT_SECRET, clientSecret)
					.param(OAUTH2_PARAMETER_STATE, state)
					.param(OAUTH2_PARAMETER_REDIRECT_URI, session.getContext().getUri().getAbsolutePath().toString())
					.param(OAUTH2_PARAMETER_SCOPE, scope)
					.param(OAUTH2_PARAMETER_TIMESTAMP, timestampString)
					.param(OAUTH2_PARAMETER_TOKEN_TYPE, OAUTH2_PARAMETER_TOKEN_TYPE_VALUE);
		}
	}
}
