package com.airlock.iam.oauth2.domain.model.token;

import com.airlock.iam.base.api.domain.model.exception.ViolationException;
import com.airlock.iam.base.api.domain.model.user.UserId;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.core.misc.persistency.token.TokenAssignment;
import com.airlock.iam.core.misc.persistency.token.TokenBean;
import com.airlock.iam.core.misc.token.AbstractToken;
import com.airlock.iam.core.misc.util.DateRange;
import com.airlock.iam.core.misc.util.ticket.KeyMultiValue;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.elog.api.LogTypes;
import com.airlock.iam.oauth2.api.domain.model.AuthorizationServerId;
import com.airlock.iam.oauth2.api.domain.model.OAuth2ClientId;
import com.airlock.iam.oauth2.api.domain.model.OAuth2Scope;
import com.airlock.iam.oauth2.api.domain.model.OAuth2Scopes;
import com.airlock.iam.oauth2.domain.model.session.OAuth2SessionId;
import java.text.ParseException;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.Optional;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateUtils;

public abstract class AbstractTokenTableOAuth2Token extends AbstractToken implements OAuth2Token {
   private static final ELogger LOG = ELoggerFactory.getLogger(AbstractTokenTableOAuth2Token.class);
   private static final int TOKEN_VALIDITY_SKEW_SECONDS = 5;

   public AbstractTokenTableOAuth2Token(TokenBean delegate) {
      super(delegate);
   }

   protected void initialize(OAuth2Token.OAuth2TokenType type, OAuth2TokenKey key, SessionInfo session, Set<OAuth2Scope> scopes, int expiresIn) {
      this.setTokenKey(key);
      this.setTokenSubtype(type);
      this.setScope(scopes);
      this.setExpiresIn(expiresIn);
      this.setClientId(session.getClientId());
      this.setAuthorizationServerId(session.getAuthorizationServerId());
      this.setUserAuthenticationDate(session.getUserAuthenticationDate());
      this.setSessionId(session.getSessionId());
   }

   private void setTokenKey(OAuth2TokenKey tokenKey) {
      this.getDelegate().setSerialId(tokenKey.getValue());
   }

   @Override
   public boolean isValid(Optional<OAuth2ClientId> clientId, AuthorizationServerId authorizationServerId) {
      if (!authorizationServerId.equals(this.getAuthorizationServerId())) {
         LOG.info(
            "OAuth 2.0 "
               + this.getOAuth2TokenSubtype()
               + " belongs to a different authorization server. Expected: "
               + LogUtil.quote(authorizationServerId)
               + ", actual: "
               + LogUtil.quote(this.getAuthorizationServerId())
         );
         return false;
      } else if (clientId.isPresent() && !clientId.get().equals(this.getClientId())) {
         LOG.info(
            "Client ID of OAuth 2.0 "
               + this.getOAuth2TokenSubtype()
               + " ("
               + LogUtil.quote(this.getClientId())
               + ") does not match expected Client ID "
               + LogUtil.quote(clientId.get())
         );
         return false;
      } else if (!this.isActive()) {
         LOG.info("OAuth 2.0 " + this.getOAuth2TokenSubtype() + " is not active.");
         return false;
      } else if (!this.getValidityRange().isInInclusiveRange(new Date())) {
         LOG.info("OAuth 2.0 " + this.getOAuth2TokenSubtype() + " is expired.");
         return false;
      } else {
         return true;
      }
   }

   @Override
   public Instant getGenerationInstant() {
      return this.getGenerationDate().toInstant();
   }

   @Override
   public OAuth2Scopes getScopes() {
      String data = this.getGenericDataElement3();
      if (StringUtils.isNotBlank(data)) {
         try {
            KeyMultiValue scopes = KeyMultiValue.parseFromString(this.getGenericDataElement3(), "UTF-8");
            return OAuth2Scopes.scopesFromStrings(Arrays.asList(scopes.getValues()));
         } catch (ParseException e) {
            LOG.warn(
               LogTypes.RESOURCE_NOT_FOUND, "Could not parse scopes %s from generic token. Proceeded with empty scope.".formatted(LogUtil.logSafeQuote(data))
            );
         }
      }

      return OAuth2Scopes.emptyScopes();
   }

   public final void setScope(Set<OAuth2Scope> scope) {
      this.setGenericDataElement3(new KeyMultiValue("scope", OAuth2Scope.convertToScopeNames(scope).toArray(String[]::new)).getEncoded("UTF-8"));
   }

   @Override
   public Optional<Long> getExpiresIn() {
      return this.getExpiry().map(exp -> Math.max(exp.getEpochSecond() - Instant.now().getEpochSecond(), 0L));
   }

   @Override
   public Optional<Instant> getExpiry() {
      return Optional.ofNullable(this.getValidityRange().getToInstant());
   }

   private void setExpiresIn(int expiresIn) {
      if (expiresIn < 0) {
         throw new ViolationException("Invalid expires_in value: " + expiresIn);
      }

      this.setValidityRange(new DateRange(DateUtils.addSeconds(this.getGenerationDate(), -5), DateUtils.addSeconds(this.getGenerationDate(), expiresIn)));
   }

   @Override
   public OAuth2SessionId getSessionId() {
      return OAuth2SessionId.oAuth2SessionId(this.getGenericDataElement6());
   }

   private void setSessionId(OAuth2SessionId sessionId) {
      this.setGenericDataElement6(sessionId.getId());
   }

   @Override
   public OAuth2Token.OAuth2TokenType getOAuth2TokenSubtype() {
      return getTokenTypeForDbIdentifier(this.getGenericDataElement1());
   }

   private void setTokenSubtype(OAuth2Token.OAuth2TokenType type) {
      this.setGenericDataElement1(getDbIdentifierForTokenType(type));
   }

   @Override
   public OAuth2ClientId getClientId() {
      return OAuth2ClientId.oAuth2ClientId(this.getGenericDataElement7());
   }

   public final void setClientId(OAuth2ClientId clientId) {
      this.setGenericDataElement7(clientId.id());
   }

   @Override
   public AuthorizationServerId getAuthorizationServerId() {
      if (this.getGenericDataElement12() == null) {
         throw new ViolationException("Stored token is an unsupported client-centric OAuth 2.0 token");
      } else {
         return AuthorizationServerId.authorizationServerId(this.getGenericDataElement12());
      }
   }

   public final void setAuthorizationServerId(AuthorizationServerId authorizationServerId) {
      this.setGenericDataElement12(authorizationServerId.id());
   }

   @Override
   public Instant getUserAuthenticationInstant() {
      return Instant.ofEpochMilli(Long.parseLong(this.getGenericDataElement9()));
   }

   private void setUserAuthenticationDate(Date date) {
      this.setGenericDataElement9(Long.toString(date.getTime()));
   }

   @Override
   public int getTotalUsages() {
      return this.getDelegate().getTokenUsage().getTotalUsages();
   }

   public void increaseUsage() {
      this.getDelegate().handleLatestUsage(new Date());
   }

   private TokenAssignment getTokenAssignment() {
      Collection<TokenAssignment> assignments = super.getAssignments();
      if (assignments.isEmpty()) {
         throw new IllegalStateException("OAuth 2.0 Token not assigned to a user!");
      } else if (assignments.size() > 1) {
         throw new IllegalStateException("OAuth 2.0 Token assigned to multiple users!");
      } else {
         return assignments.iterator().next();
      }
   }

   @Override
   public UserId getUserId() {
      return UserId.userId(this.getTokenAssignment().getUser());
   }

   public static AbstractTokenTableOAuth2Token asTokenTableBasedOAuth2Token(OAuth2Token token) {
      if (token instanceof AbstractTokenTableOAuth2Token t) {
         return t;
      } else {
         throw new ViolationException("Cannot cast " + token.getClass().getSimpleName() + " to " + AbstractTokenTableOAuth2Token.class.getSimpleName());
      }
   }

   public static String getDbIdentifierForTokenType(OAuth2Token.OAuth2TokenType tokenSubtype) {
      return switch (tokenSubtype) {
         case AUTHORIZATION_CODE -> "authorization_code";
         case ACCESS_TOKEN -> "access_token";
         case REFRESH_TOKEN -> "refresh_token";
         case OPENID_CONNECT_ID_TOKEN -> "id_token";
      };
   }

   public static OAuth2Token.OAuth2TokenType getTokenTypeForDbIdentifier(String identifier) {
      return switch (identifier) {
         case "authorization_code" -> OAuth2Token.OAuth2TokenType.AUTHORIZATION_CODE;
         case "access_token" -> OAuth2Token.OAuth2TokenType.ACCESS_TOKEN;
         case "refresh_token" -> OAuth2Token.OAuth2TokenType.REFRESH_TOKEN;
         case "id_token" -> OAuth2Token.OAuth2TokenType.OPENID_CONNECT_ID_TOKEN;
         default -> throw new IllegalStateException("Unknown token subtype on db: " + LogUtil.logSafeQuote(identifier));
      };
   }
}
