package ru.micord.ervu.security.esia;

import java.lang.invoke.MethodHandles;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.support.MessageSourceAccessor;
import ru.micord.ervu.security.esia.exception.EsiaException;
import ru.micord.ervu.security.esia.model.ExpiringState;
import ru.micord.ervu.security.esia.model.ExpiringToken;

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

/**
 * @author Eduard Tihomirov
 */
public class EsiaAuthInfoStore {
  private static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
  private static final Map<String, ExpiringToken> ACCESS_TOKENS_MAP = new ConcurrentHashMap<>();
  private static final Map<String, ExpiringToken> REFRESH_TOKENS_MAP = new ConcurrentHashMap<>();
  private static final Map<String, List<ExpiringState>> PRNS_UUID_STATE_MAP = new ConcurrentHashMap<>();
  private static final MessageSourceAccessor MESSAGE_SOURCE = MessageBundleUtils.createAccessor(
      "messages/common_errors_messages");

  public static void addAccessToken(String prnOid, String token, long expiresIn) {
    if (token != null) {
      long expiryTime = System.currentTimeMillis() + 1000L * expiresIn;
      ACCESS_TOKENS_MAP.put(prnOid, new ExpiringToken(token, expiryTime));
    }
  }

  public static String getAccessToken(String prnOid) {
    return ACCESS_TOKENS_MAP.get(prnOid).getAccessToken();
  }

  public static boolean validateAccessToken(String prnOid) {
    ExpiringToken token = ACCESS_TOKENS_MAP.get(prnOid);
    if (token == null || token.getAccessToken() == null) {
      LOGGER.error("No ESIA access token for prnOid: " + prnOid);
      return false;
    }
    else if (token.isExpired()) {
      LOGGER.error("ESIA access token expired for prnOid: " + prnOid);
      return false;
    }
    return true;
  }

  public static void removeExpiredAccessToken() {
    for (String key : ACCESS_TOKENS_MAP.keySet()) {
      ExpiringToken token = ACCESS_TOKENS_MAP.get(key);
      if (token != null && token.isExpired()) {
        ACCESS_TOKENS_MAP.remove(key);
      }
    }
  }

  public static void removeExpiredRefreshToken() {
    for (String key : REFRESH_TOKENS_MAP.keySet()) {
      ExpiringToken token = REFRESH_TOKENS_MAP.get(key);
      if (token != null && token.isExpired()) {
        REFRESH_TOKENS_MAP.remove(key);
      }
    }
  }

  public static void removeAccessToken(String prnOid) {
    ACCESS_TOKENS_MAP.remove(prnOid);
  }

  public static void addRefreshToken(String prnOid, String token, long expiresIn) {
    if (token != null) {
      long expiryTime = System.currentTimeMillis() + 1000L * expiresIn;
      REFRESH_TOKENS_MAP.put(prnOid, new ExpiringToken(token, expiryTime));
    }
  }

  public static String getRefreshToken(String prnOid) {
    return REFRESH_TOKENS_MAP.get(prnOid).getAccessToken();
  }

  public static void removeRefreshToken(String prnOid) {
    REFRESH_TOKENS_MAP.remove(prnOid);
  }

  public static void addState(String prnsUUID, String state, long expiresIn, long attemptsCount) {
    long expiryTime = System.currentTimeMillis() + expiresIn * 1000L;
    ExpiringState newState = new ExpiringState(state, expiryTime);
    PRNS_UUID_STATE_MAP.compute(prnsUUID, (key, states) -> {
      if (states == null) {
        states = new CopyOnWriteArrayList<>();
      }
      else {
        states.removeIf(ExpiringState::isExpired);
      }
      if (states.size() >= attemptsCount) {
        throw new LocalizedException("login_attempts_exceeded", MESSAGE_SOURCE);
      }
      states.add(newState);
      return states;
    });
  }

  public static void validateState(String prnsUUID, String state) {
    List<ExpiringState> states = PRNS_UUID_STATE_MAP.get(prnsUUID);
    if (states == null) {
      throw new EsiaException("State invalid. No state found");
    }
    long currentTime = System.currentTimeMillis();

    StringBuilder statesStringBuilder = new StringBuilder();
    for (ExpiringState expiringState : states) {
      if (expiringState.getState().equals(state)) {
        if (expiringState.getExpiryTime() < currentTime) {
          throw new EsiaException("State invalid. State : " + state + " expired at : " + expiringState.getExpiryTime());
        }
        return;
      }
      statesStringBuilder.append(expiringState.getState(), 0, 8).append(", ");
    }
    throw new EsiaException("State invalid. Backend states :" + statesStringBuilder + " cookie state :" + state);
  }

  public static void removeState(String prnsUUID) {
    PRNS_UUID_STATE_MAP.remove(prnsUUID);
  }

  public static void removeExpiredState() {
    for (String key : PRNS_UUID_STATE_MAP.keySet()) {
      PRNS_UUID_STATE_MAP.computeIfPresent(key, (k, states) -> {
        states.removeIf(ExpiringState::isExpired);
        return states.isEmpty() ? null : states;
      });
    }
  }
}
