package ru.micord.ervu.security.esia.service;

import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.net.URI;
import java.net.URL;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import ervu.service.okopf.OkopfService;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.http.ResponseCookie;
import org.springframework.stereotype.Service;
import ru.micord.ervu.audit.constants.AuditConstants;
import ru.micord.ervu.audit.service.AuditService;
import org.springframework.web.util.WebUtils;
import ru.micord.ervu.security.esia.exception.EsiaException;
import ru.micord.ervu.security.esia.model.EmployeeModel;
import ru.micord.ervu.security.esia.model.EsiaAccessToken;
import ru.micord.ervu.security.esia.model.EsiaHeader;
import ru.micord.ervu.security.esia.model.EsiaTokenResponse;
import ru.micord.ervu.security.esia.model.FormUrlencoded;
import ru.micord.ervu.security.esia.model.OrganizationModel;
import ru.micord.ervu.security.esia.model.SignResponse;
import ru.micord.ervu.security.esia.EsiaAuthInfoStore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.util.StringUtils;
import ru.micord.ervu.security.esia.config.EsiaConfig;
import org.springframework.beans.factory.annotation.Value;
import ru.micord.ervu.kafka.model.Brhs;
import ru.micord.ervu.kafka.model.Employee;
import ru.micord.ervu.kafka.model.ErvuOrgResponse;
import ru.micord.ervu.kafka.model.OrgInfo;
import ru.micord.ervu.kafka.service.ReplyingKafkaService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import ru.micord.ervu.security.webbpm.jwt.JwtAuthentication;
import ru.micord.ervu.security.webbpm.jwt.helper.SecurityHelper;
import ru.micord.ervu.security.webbpm.jwt.service.JwtTokenService;
import ru.micord.ervu.security.webbpm.jwt.model.Token;

import ru.cg.webbpm.modules.core.runtime.api.LocalizedException;
import ru.cg.webbpm.modules.core.runtime.api.MessageBundleUtils;

import static ru.micord.ervu.security.SecurityConstants.PRNS_UUID;

/**
 * @author Eduard Tihomirov
 */
@Service
public class EsiaAuthService {
  private static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
  private static final MessageSourceAccessor MESSAGE_SOURCE = MessageBundleUtils.createAccessor(
      "messages/common_errors_messages");
  @Autowired
  private ObjectMapper objectMapper;
  @Autowired
  private EsiaConfig esiaConfig;
  @Autowired
  private UlDataService ulDataService;
  @Autowired
  private JwtTokenService jwtTokenService;
  @Autowired
  private ReplyingKafkaService replyingKafkaService;
  @Autowired
  private OkopfService okopfService;
  @Autowired
  private SecurityHelper securityHelper;
  @Autowired
  private AuditService auditService;
  @Value("${ervu.kafka.org.reply.topic}")
  private String requestReplyTopic;

  @Value("${ervu.kafka.org.request.topic}")
  private String requestTopic;

  public String generateAuthCodeUrl(HttpServletResponse response, HttpServletRequest request) {
    try {
      String clientId = esiaConfig.getClientId();
      DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm:ss xx");
      ZonedDateTime dt = ZonedDateTime.now();
      String timestamp = dt.format(formatter);
      String state = UUID.randomUUID().toString();
      String prnsUUID = UUID.randomUUID().toString();
      Cookie oldPrnsCookie =  WebUtils.getCookie(request, PRNS_UUID);
      if (oldPrnsCookie != null) {
        prnsUUID = oldPrnsCookie.getValue();
      }
      String redirectUrl = esiaConfig.getRedirectUrl();
      String redirectUrlEncoded = redirectUrl.replaceAll(":", "%3A")
          .replaceAll("/", "%2F");
      String scope = esiaConfig.getEsiaScopes();
      String scopeOrg = esiaConfig.getEsiaOrgScopes();

      Map<String, String> parameters = new LinkedHashMap<String, String>();
      parameters.put("client_id", clientId);
      parameters.put("scope", scope);
      parameters.put("scope_org", scopeOrg);
      parameters.put("timestamp", timestamp);
      parameters.put("state", state);
      parameters.put("redirect_uri", esiaConfig.getRedirectUrl());

      String clientSecret = signMap(parameters);
      EsiaAuthInfoStore.addState(prnsUUID, state, esiaConfig.getEsiaStateCookieLifeTime(), esiaConfig.getEsiaLoginAttemptsCount());
      ResponseCookie prnsCookie = securityHelper.createAccessCookie(PRNS_UUID, prnsUUID)
          .maxAge(esiaConfig.getEsiaStateCookieLifeTime())
          .build();
      securityHelper.addResponseCookie(response, prnsCookie);
      String responseType = "code";

      String authUrl = esiaConfig.getEsiaBaseUri() + esiaConfig.getEsiaCodeUrl();

      URL url = new URL(authUrl);
      Map<String, String> params = mapOf("scope", scope,
          "scope_org", scopeOrg.replaceAll(":", "%3A")
              .replaceAll("/", "%2F"),
          "timestamp", urlEncode(timestamp),
          "state", state,
          "client_id", clientId,
          "client_secret", clientSecret,
          "response_type", responseType,
          "redirect_uri", redirectUrlEncoded,
          "obj_type", "B L F A",
          "client_certificate_hash", esiaConfig.getClientCertHash());

      return buildUrl(url, params);
    }
    catch (Exception e) {
      throw new EsiaException(e);
    }
  }

  private static Map<String, String> mapOf(String... params) {
    if (params.length % 2 != 0) {
      throw new IllegalArgumentException("Invalid params count");
    }
    Map<String, String> map = new LinkedHashMap<>(params.length / 2);
    for (int i = 0; i < params.length / 2; i++) {
      map.put(params[i * 2], params[i * 2 + 1]);
    }
    return map;
  }

  private static String urlEncode(String s) {
    return URLEncoder.encode(s, StandardCharsets.UTF_8)
        .replace("+", "%20");
  }

  private static String buildUrl(URL url, Map<String, String> params) {
    StringBuilder uriBuilder = new StringBuilder(url.toString());
    uriBuilder.append('?');
    for (Map.Entry<String, String> node : params.entrySet()) {
      uriBuilder.append(node.getKey()).append('=').append(node.getValue()).append("&");
    }
    uriBuilder.deleteCharAt(uriBuilder.length() - 1);
    return uriBuilder.toString();
  }

  public void authEsiaTokensByCode(String esiaAuthCode, String state, HttpServletResponse response, HttpServletRequest request) {
    String esiaAccessTokenStr = null;
    String prnOid = null;
    Long expiresIn = null;
    boolean hasRole = false;
    long timeSignSecret = 0, timeRequestAccessToken = 0, timeVerifySecret = 0;
    verifyStateFromCookie(request, state, response);
    try {
      String clientId = esiaConfig.getClientId();
      DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm:ss xx");
      ZonedDateTime dt = ZonedDateTime.now();
      String timestamp = dt.format(formatter);
      String newState = UUID.randomUUID().toString();
      String redirectUrl = esiaConfig.getRedirectUrl();
      String scope = esiaConfig.getEsiaScopes();
      String scopeOrg = esiaConfig.getEsiaOrgScopes();

      Map<String, String> parameters = new LinkedHashMap<String, String>();
      parameters.put("client_id", clientId);
      parameters.put("scope", scope);
      parameters.put("scope_org", scopeOrg);
      parameters.put("timestamp", timestamp);
      parameters.put("state", newState);
      parameters.put("redirect_uri", redirectUrl);
      parameters.put("code", esiaAuthCode);

      long startTime = System.currentTimeMillis();
      String clientSecret = signMap(parameters);
      timeSignSecret = System.currentTimeMillis() - startTime;
      String authUrl = esiaConfig.getEsiaBaseUri() + esiaConfig.getEsiaTokenUrl();
      String postBody = new FormUrlencoded()
          .setParameter("client_id", clientId)
          .setParameter("code", esiaAuthCode)
          .setParameter("grant_type", "authorization_code")
          .setParameter("client_secret", clientSecret)
          .setParameter("state", newState)
          .setParameter("redirect_uri", redirectUrl)
          .setParameter("scope", scope)
          .setParameter("scope_org", scopeOrg)
          .setParameter("timestamp", timestamp)
          .setParameter("token_type", "Bearer")
          .setParameter("client_certificate_hash", esiaConfig.getClientCertHash())
          .toFormUrlencodedString();
      startTime = System.currentTimeMillis();
      HttpRequest postReq = HttpRequest.newBuilder(URI.create(authUrl))
          .header(HttpHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded")
          .POST(HttpRequest.BodyPublishers.ofString(postBody))
          .timeout(Duration.ofSeconds(esiaConfig.getRequestTimeout()))
          .build();
      HttpResponse<String> postResp = HttpClient.newBuilder()
          .connectTimeout(Duration.ofSeconds(esiaConfig.getConnectionTimeout()))
          .build()
          .send(postReq, HttpResponse.BodyHandlers.ofString());
      timeRequestAccessToken = System.currentTimeMillis() - startTime;
      String responseString = postResp.body();
      EsiaTokenResponse tokenResponse = objectMapper.readValue(responseString,
          EsiaTokenResponse.class
      );

      if (tokenResponse == null || tokenResponse.getError() != null) {
        String errMsg =
            tokenResponse != null ? tokenResponse.getErrorDescription() : "response is empty";
        throw new IllegalStateException("Esia response error. " + errMsg);
      }
      if (!tokenResponse.getState().equals(newState)) {
        throw new EsiaException("Token invalid. State from request not equals with state from response.");
      }
      esiaAccessTokenStr = tokenResponse.getAccessToken();
      String esiaRefreshTokenStr = tokenResponse.getRefreshToken();
      startTime = System.currentTimeMillis();
      String verifyResult = verifyToken(esiaAccessTokenStr);
      timeVerifySecret = System.currentTimeMillis() - startTime;
      if (verifyResult != null) {
        throw new EsiaException(verifyResult);
      }
      EsiaAccessToken esiaAccessToken = ulDataService.readToken(esiaAccessTokenStr);
      prnOid = esiaAccessToken.getSbjId();
      expiresIn = tokenResponse.getExpiresIn();
      EsiaAuthInfoStore.addAccessToken(prnOid, esiaAccessTokenStr, expiresIn);
      EsiaAuthInfoStore.addRefreshToken(prnOid, esiaRefreshTokenStr, expiresIn);
    }
    catch (Exception e) {
      throw new EsiaException(e);
    }
    finally {
      LOGGER.info("Thread {} - SignSecret: {} ms RequestAccessToken: {} ms VerifySecret: {} ms",
          Thread.currentThread().getId(), timeSignSecret, timeRequestAccessToken, timeVerifySecret);
    }
    OrgInfo orgInfo = null;
    String status = null, ervuId = null;
    try {
      orgInfo = getOrgInfo(esiaAccessTokenStr);
      hasRole = ulDataService.checkRole(esiaAccessTokenStr);
      ervuId = getErvuId(prnOid, orgInfo);
      if (!hasRole) {
        LOGGER.error("The user with id = " + prnOid + " does not have the required role");
        throw new LocalizedException("access_denied", MESSAGE_SOURCE);
      }
      status = AuditConstants.SUCCESS_STATUS_TYPE;
    }
    catch (Exception e) {
      status = AuditConstants.FAILURE_STATUS_TYPE;
      if (e instanceof EsiaException || e instanceof JsonProcessingException) {
        throw new EsiaException(e);
      }
    }
    finally {
      if (orgInfo!= null){
        auditService.processAuthEvent(request, orgInfo, prnOid, status,
            AuditConstants.LOGIN_EVENT_TYPE);
      }
      createTokenAndAddCookie(response, prnOid, ervuId, hasRole , expiresIn);
    }
  }

  public void getEsiaTokensByRefreshToken(HttpServletRequest request, HttpServletResponse response) {
    try {
      String refreshToken = jwtTokenService.getRefreshToken(request);
      String clientId = esiaConfig.getClientId();
      DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm:ss xx");
      ZonedDateTime dt = ZonedDateTime.now();
      String timestamp = dt.format(formatter);
      String state = UUID.randomUUID().toString();
      String redirectUrl = esiaConfig.getRedirectUrl();
      String scope = esiaConfig.getEsiaScopes();
      String scopeOrg = esiaConfig.getEsiaOrgScopes();

      Map<String, String> parameters = new LinkedHashMap<String, String>();
      parameters.put("client_id", clientId);
      parameters.put("scope", scope);
      parameters.put("scope_org", scopeOrg);
      parameters.put("timestamp", timestamp);
      parameters.put("state", state);
      parameters.put("redirect_uri", esiaConfig.getRedirectUrl());
      parameters.put("refresh_token", refreshToken);

      String clientSecret = signMap(parameters);
      String authUrl = esiaConfig.getEsiaBaseUri() + esiaConfig.getEsiaTokenUrl();
      String postBody = new FormUrlencoded()
          .setParameter("client_id", clientId)
          .setParameter("refresh_token", refreshToken)
          .setParameter("grant_type", "refresh_token")
          .setParameter("client_secret", clientSecret)
          .setParameter("state", state)
          .setParameter("redirect_uri", redirectUrl)
          .setParameter("scope", scope)
          .setParameter("scope_org", scopeOrg)
          .setParameter("timestamp", timestamp)
          .setParameter("token_type", "Bearer")
          .setParameter("client_certificate_hash", esiaConfig.getClientCertHash())
          .toFormUrlencodedString();
      HttpRequest postReq = HttpRequest.newBuilder(URI.create(authUrl))
          .header(HttpHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded")
          .POST(HttpRequest.BodyPublishers.ofString(postBody))
          .timeout(Duration.ofSeconds(esiaConfig.getRequestTimeout()))
          .build();
      HttpResponse<String> postResp = HttpClient.newBuilder()
          .connectTimeout(Duration.ofSeconds(esiaConfig.getConnectionTimeout()))
          .build()
          .send(postReq, HttpResponse.BodyHandlers.ofString());
      String responseString = postResp.body();
      EsiaTokenResponse tokenResponse = objectMapper.readValue(responseString, EsiaTokenResponse.class);

      if (tokenResponse == null || tokenResponse.getError() != null) {
        String errMsg =
            tokenResponse != null ? tokenResponse.getErrorDescription() : "response is empty";
        throw new IllegalStateException("Esia response error. " + errMsg);
      }
      if (!tokenResponse.getState().equals(state)) {
        throw new EsiaException("Token invalid. State from request not equals with state from response.");
      }
      String esiaAccessTokenStr = tokenResponse.getAccessToken();
      String esiaNewRefreshToken = tokenResponse.getRefreshToken();
      String verifyResult = verifyToken(esiaAccessTokenStr);
      if (verifyResult != null) {
        throw new EsiaException(verifyResult);
      }
      EsiaAccessToken esiaAccessToken = ulDataService.readToken(esiaAccessTokenStr);
      String prnOid = esiaAccessToken.getSbjId();
      Long expiresIn = tokenResponse.getExpiresIn();
      EsiaAuthInfoStore.addAccessToken(prnOid, esiaAccessTokenStr, expiresIn);
      EsiaAuthInfoStore.addRefreshToken(prnOid, esiaNewRefreshToken, expiresIn);
      OrgInfo orgInfo = getOrgInfo(esiaAccessTokenStr);
      String ervuId = getErvuId(prnOid, orgInfo);
      createTokenAndAddCookie(response, esiaAccessToken.getSbjId(), ervuId, true, expiresIn);
    }
    catch (EsiaException | IOException | InterruptedException e) {
      throw new EsiaException(e);
    }
  }

  private String signMap(Map<String, String> paramsToSign) {
    try {
      StringBuilder toSign = new StringBuilder();
      for (String s : paramsToSign.values()) {
        toSign.append(s);
      }
      String requestBody = toSign.toString();

      HttpRequest request = HttpRequest.newBuilder()
          .uri(URI.create(esiaConfig.getSignUrl()))
          .header("Content-Type", "text/plain")
          .POST(HttpRequest.BodyPublishers.ofString(requestBody, StandardCharsets.UTF_8))
          .timeout(Duration.ofSeconds(esiaConfig.getRequestTimeout()))
          .build();
      HttpResponse<String> response = HttpClient.newBuilder()
          .connectTimeout(Duration.ofSeconds(esiaConfig.getConnectionTimeout()))
          .build()
          .send(request, HttpResponse.BodyHandlers.ofString());
      errorHandler(response);
      return response.body();

    }
    catch (Exception e) {
      throw new EsiaException(e);
    }
  }

  private void errorHandler(HttpResponse<?> httpResponse) {
    if (httpResponse.statusCode() != 200) {
      throw new EsiaException(httpResponse.statusCode() + " " + httpResponse.body());
    }
  }

  public String logout(HttpServletRequest request, HttpServletResponse response) {
    OrgInfo orgInfo = null;
    String userId = null;
    String status = null;
    try {
      userId = jwtTokenService.getUserAccountId(request);
      String accessToken = EsiaAuthInfoStore.getAccessToken(userId);
      orgInfo = getOrgInfo(accessToken);
      securityHelper.clearAccessCookies(response);
      EsiaAuthInfoStore.removeAccessToken(userId);
      EsiaAuthInfoStore.removeRefreshToken(userId);
      String logoutUrl = esiaConfig.getEsiaBaseUri() + esiaConfig.getEsiaLogoutUrl();
      String redirectUrl = esiaConfig.getLogoutRedirectUrl();
      URL url = new URL(logoutUrl);
      Map<String, String> params = mapOf(
          "client_id", esiaConfig.getClientId(),
          "redirect_url", redirectUrl
      );
      status = AuditConstants.SUCCESS_STATUS_TYPE;
      return buildUrl(url, params);
    }
    catch (Exception e) {
      status = AuditConstants.FAILURE_STATUS_TYPE;
      throw new EsiaException(e);
    }
    finally {
      if (orgInfo != null) {
        auditService.processAuthEvent(request, orgInfo, userId, status,
            AuditConstants.LOGOUT_EVENT_TYPE
        );
      }
    }
  }

  public String getErvuId(String prnOid, OrgInfo orgInfo) throws JsonProcessingException {
    orgInfo.setOrgTypeName(okopfService.findTitleByLeg(orgInfo.getOrgTypeLeg()));
    String kafkaResponse = replyingKafkaService.sendMessageAndGetReply(requestTopic,
        requestReplyTopic, objectMapper.writeValueAsString(orgInfo)
    );
    ErvuOrgResponse ervuOrgResponse = objectMapper.readValue(kafkaResponse, ErvuOrgResponse.class);
    String ervuId = ervuOrgResponse.getData().getErvuId();
    if (!StringUtils.hasText(ervuId)) {
      throw new EsiaException("No ervuId for prnOid = " + prnOid);
    }
    return ervuId;
  }

  private OrgInfo getOrgInfo(String accessToken) {
    long startTime = System.currentTimeMillis();
    long timeRequestPersonDataOrg = 0, timeRequestPersonDataEmployee = 0, timeRequestPersonDataChief = 0;
    try {
      OrganizationModel organizationModel = ulDataService.getOrganizationModel(accessToken);
      timeRequestPersonDataOrg = System.currentTimeMillis() - startTime;
      startTime = System.currentTimeMillis();
      EmployeeModel employeeModel = ulDataService.getEmployeeModel(accessToken);
      timeRequestPersonDataEmployee = System.currentTimeMillis() - startTime;
      startTime = System.currentTimeMillis();
      EmployeeModel chiefModel = ulDataService.getChiefEmployeeModel(accessToken);
      timeRequestPersonDataChief = System.currentTimeMillis() - startTime;
      return copyToOrgInfo(organizationModel, employeeModel, chiefModel);
    }
    finally {
      LOGGER.info("Thread {} - RequestPersonDataOrg: {} ms RequestPersonDataEmployee: {} ms RequestPersonDataChief: {} ms",
          Thread.currentThread().getId(), timeRequestPersonDataOrg, timeRequestPersonDataEmployee, timeRequestPersonDataChief
      );
    }
  }

  private OrgInfo copyToOrgInfo(OrganizationModel organizationModel, EmployeeModel employeeModel, EmployeeModel chiefModel ) {
    OrgInfo orgInfo = new OrgInfo();
    if (chiefModel != null) {
      orgInfo.setChiefInfo(copyToEmployee(chiefModel));
    }
    orgInfo.setSenderInfo(copyToEmployee(employeeModel));
    orgInfo.setBrhs(
        Arrays.stream(organizationModel.getBrhs()).map(brhsModel -> {
          Brhs brhs = new Brhs();
          brhs.setName(brhsModel.getName());
          brhs.setBrhOid(brhsModel.getBrhOid());
          brhs.setKpp(brhsModel.getKpp());
          brhs.setLeg(brhsModel.getLeg());
          brhs.setAddresses(brhsModel.getAddresses() != null ? brhsModel.getAddresses().getElements() : null);
          brhs.setContacts(brhsModel.getContacts() != null ? brhsModel.getContacts().getElements() : null);
          return brhs;
        }).toArray(Brhs[]::new));
    orgInfo.setAddresses(organizationModel.getAddresses().getElements());
    orgInfo.setContacts(organizationModel.getContacts().getElements());
    orgInfo.setInn(organizationModel.getInn());
    orgInfo.setOrgFullName(organizationModel.getFullName());
    orgInfo.setOrgTypeLeg(organizationModel.getLeg());
    orgInfo.setKpp(organizationModel.getKpp());
    orgInfo.setLiquidated(organizationModel.getIsLiquidated());
    orgInfo.setOgrn(organizationModel.getOgrn());
    orgInfo.setOrgOid(organizationModel.getOid());
    orgInfo.setOrgShortName(organizationModel.getShortName());
    orgInfo.setStaffCount(organizationModel.getStaffCount());
    orgInfo.setBranchesCount((long) orgInfo.getBrhs().length);
    orgInfo.setOrgType(organizationModel.getType());
    orgInfo.setOrgAgencyTerRange(organizationModel.getAgencyTerRange());
    orgInfo.setOrgAgencyType(organizationModel.getAgencyType());
    return orgInfo;
  }

  private Employee copyToEmployee(EmployeeModel employeeModel) {
    Employee employee = new Employee();
    employee.setBirthDate(employeeModel.getPerson().getBirthDate());
    employee.setFirstName(employeeModel.getPerson().getFirstName());
    employee.setLastName(employeeModel.getPerson().getLastName());
    employee.setMiddleName(employeeModel.getPerson().getMiddleName());
    employee.setBlocked(employeeModel.getBlocked());
    employee.setChief(employeeModel.getChief());
    employee.setCitizenship(employeeModel.getPerson().getCitizenship());
    employee.setEmail(employeeModel.getPerson().getEmail());
    employee.setPhoneNumber(employeeModel.getPerson().getMobileNumber());
    employee.setBirthPlace(employeeModel.getPerson().getBirthPlace());
    employee.setGender(employeeModel.getPerson().getGender());
    employee.setInn(employeeModel.getPerson().getInn());
    employee.setSnils(employeeModel.getPerson().getSnils());
    employee.setPosition(employeeModel.getPosition());
    employee.setPrnOid(employeeModel.getPrnOid());
    employee.setOrgOid(employeeModel.getOrgOid());
    return employee;
  }

  private void createTokenAndAddCookie(HttpServletResponse response, String userId, String ervuId,
      Boolean hasRole, Long expiresIn) {
    Token token = jwtTokenService.createAccessToken(userId, expiresIn, ervuId, hasRole);
    securityHelper.addAccessCookies(response, token.getValue(), expiresIn.intValue());
    UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
        new UsernamePasswordAuthenticationToken(token.getUserAccountId(), null);
    SecurityContext context = SecurityContextHolder.createEmptyContext();
    JwtAuthentication authentication = new JwtAuthentication(usernamePasswordAuthenticationToken,
        userId, token.getValue());
    context.setAuthentication(authentication);
    SecurityContextHolder.setContext(context);
  }

  private String verifyToken(String accessToken) {
    EsiaAccessToken esiaAccessToken = ulDataService.readToken(accessToken);
    EsiaHeader esiaHeader = ulDataService.readHeader(accessToken);
    if (!esiaHeader.getSbt().equals("access")) {
      return "Token invalid. Token sbt: " + esiaHeader.getSbt() + " invalid";
    }
    if (!esiaHeader.getVer().equals(esiaConfig.getEsiaMarkerVer())) {
      return "Token invalid. Token ver: " + esiaHeader.getVer() + " invalid";
    }
    if (!esiaHeader.getTyp().equals("JWT")) {
      return "Token invalid. Token type: " + esiaHeader.getTyp() + " invalid";
    }
    if (!esiaAccessToken.getClientId().equals(esiaConfig.getClientId())) {
      return "Token invalid. Token clientId: " + esiaAccessToken.getClientId() + " invalid";
    }
    if (!esiaAccessToken.getIss().equals(esiaConfig.getEsiaIssuerUrl())) {
      return "Token invalid. Token issuer:" + esiaAccessToken.getIss() + " invalid";
    }
    LocalDateTime iatTime = LocalDateTime.ofInstant(Instant.ofEpochSecond(esiaAccessToken.getIat()),
        ZoneId.systemDefault()
    );
    LocalDateTime expTime = LocalDateTime.ofInstant(Instant.ofEpochSecond(esiaAccessToken.getExp()),
        ZoneId.systemDefault()
    );
    LocalDateTime currentTime = LocalDateTime.now();
    if (currentTime.getNano() > 0) {
      currentTime = currentTime.plusSeconds(1).truncatedTo(ChronoUnit.SECONDS);
    }
    if (currentTime.isBefore(iatTime) || expTime.isBefore(iatTime) || currentTime.isAfter(expTime)) {
      return "Token invalid. Token expired, current: " + currentTime + " iat: " + iatTime + " exp: " + expTime;
    }
    HttpResponse<String> response = signVerify(accessToken);
    if (response.statusCode() != 200) {
      if (response.statusCode() == 401) {
        return "Token invalid. " + response.body();
      }
      return "Error in verify module. Error status " + response.statusCode();
    }
    return null;
  }

  private HttpResponse<String> signVerify(String accessToken) {
    try {
      HttpRequest request = HttpRequest.newBuilder()
          .uri(URI.create(esiaConfig.getSignVerifyUrl()))
          .header("Content-Type", "text/plain")
          .POST(HttpRequest.BodyPublishers.ofString(accessToken, StandardCharsets.UTF_8))
          .timeout(Duration.ofSeconds(esiaConfig.getRequestTimeout()))
          .build();
      return HttpClient.newBuilder()
          .connectTimeout(Duration.ofSeconds(esiaConfig.getConnectionTimeout()))
          .build()
          .send(request, HttpResponse.BodyHandlers.ofString());
    }
    catch (Exception e) {
      throw new EsiaException(e);
    }
  }

  private void verifyStateFromCookie(HttpServletRequest request, String state, HttpServletResponse response) {
    Cookie cookie = WebUtils.getCookie(request, PRNS_UUID);
    if (cookie == null) {
      throw new EsiaException("State invalid. Cookie not found");
    }
    String prnsUUID = cookie.getValue();
    try {
      EsiaAuthInfoStore.validateState(prnsUUID, state);
    }
    finally {
      EsiaAuthInfoStore.removeState(prnsUUID);
      securityHelper.clearAccessCookie(response, PRNS_UUID);
    }
  }
}
