package com.airlock.iam.core.misc.util.ticket.codec.esp;

import com.airlock.iam.base.domain.service.uri.UrlUtils;
import com.airlock.iam.core.misc.util.ticket.KeyMultiValue;
import com.airlock.iam.core.misc.util.ticket.Ticket;
import com.airlock.iam.core.misc.util.ticket.TicketEncoder;
import com.airlock.iam.core.misc.util.ticket.TicketEncodingException;
import com.airlock.iam.core.misc.util.ticket.codec.esp.EspSignTicketEncoder.EspSignTicketEncoderBuilder;
import com.airlock.iam.plugin.api.annotation.config.Plugin;
import com.airlock.iam.plugin.api.annotation.lifecycle.AfterConfigActivation;
import com.airlock.iam.plugin.api.exception.InitializeException;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.Signature;
import java.security.SignatureException;
import java.security.interfaces.RSAPrivateKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.RSAPrivateKeySpec;
import java.util.Optional;
import java.util.Map.Entry;
import org.apache.commons.codec.binary.Base64;

@Plugin(
   description = "Ticket encoder digitally signing tickets with a fix 512-bit RSA private key located in the ticket encoder code.<p><b>Note:</b>In order for this encoder to be compliant with legacy code, the encoding of the ticket information is not straight forward and not like in the other ticket encoders.</p><p>Use the ticket decoder <class>EspSignTicketDecoder</class> to decode tickets encoded with this encoder.</p>"
)
public class EspSignTicketEncoder implements TicketEncoder {
   public static final long DEFAULT_VALIDITY_PERIOD = 28800000L;
   private static final byte[] PRIVATE_EXPONENT = new byte[]{
      72,
      121,
      -73,
      -40,
      -14,
      -37,
      -28,
      -22,
      -102,
      -35,
      13,
      -36,
      34,
      -56,
      -104,
      -92,
      -66,
      66,
      103,
      95,
      34,
      -39,
      -44,
      98,
      -25,
      -67,
      39,
      85,
      40,
      -22,
      108,
      102,
      -24,
      -36,
      41,
      127,
      49,
      -85,
      -95,
      124,
      -123,
      -23,
      -84,
      -125,
      -109,
      19,
      37,
      -62,
      78,
      107,
      -18,
      -102,
      -104,
      4,
      35,
      6,
      -34,
      -108,
      -118,
      -49,
      -49,
      -90,
      92,
      -79
   };
   private final Object signerLock = new Object();
   private Signature signer;

   public String encode(Ticket ticket, String urlEncodingScheme) throws TicketEncodingException {
      String username = (String)ticket.getFirstValueForName("username").get();
      Optional<String> gsid = ticket.getFirstValueForName("EAAS_GSID");
      Optional<String> dn = ticket.getFirstValueForName("DISTINGUISHED_NAME");
      Optional<String> authMethod = ticket.getFirstValueForName("AUTH_METHOD");
      KeyMultiValue rolesInTicket = ticket.getValueForName("roles");
      String[] roles = rolesInTicket == null ? null : rolesInTicket.getValues();
      StringBuilder cs = new StringBuilder();
      cs.append("userids:");
      cs.append(urlEncode("eServiceUser"));
      cs.append('=');
      cs.append(urlEncode(username));
      if (gsid.isPresent()) {
         cs.append(",");
         cs.append(urlEncode("EAAS_GSID"));
         cs.append('=');
         cs.append(urlEncode(gsid.get()));
      }

      if (authMethod.isPresent()) {
         cs.append(",");
         cs.append(urlEncode("AUTH_METHOD"));
         cs.append('=');
         cs.append(urlEncode(authMethod.get()));
      }

      if (dn.isPresent()) {
         cs.append(",");
         cs.append(urlEncode("DISTINGUISHED_NAME"));
         cs.append('=');
         cs.append(urlEncode(dn.get()));
      }

      cs.append(';');
      cs.append("roles:");
      if (roles != null) {
         for (int i = 0; i < roles.length; i++) {
            cs.append(urlEncode(roles[i]));
            if (i + 1 < roles.length) {
               cs.append(",");
            }
         }
      }

      cs.append(';');

      for (Entry<String, KeyMultiValue> entry : ticket.entrySet()) {
         String key = entry.getKey();
         if (!key.equals("username") && !key.equals("roles") && !key.equals("EAAS_GSID") && !key.equals("DISTINGUISHED_NAME") && !key.equals("AUTH_METHOD")) {
            KeyMultiValue kmv = entry.getValue();
            cs.append(urlEncode(key));
            cs.append('=');
            String[] values = kmv.getValues();

            for (int j = 0; j < values.length; j++) {
               cs.append(urlEncode(values[j]));
               if (j + 1 < values.length) {
                  cs.append(",");
               }
            }

            cs.append(';');
         }
      }

      byte[] stringEncoded = cs.toString().getBytes(StandardCharsets.ISO_8859_1);
      long expiryDate = System.currentTimeMillis() + 28800000L;
      byte[] bodyBytes = concatenate(stringEncoded, longToBytes(expiryDate));

      byte[] signature;
      try {
         signature = this.sign(bodyBytes);
      } catch (SignatureException e) {
         throw new TicketEncodingException("Error while signing authentication ticket (" + e.getMessage() + ")", e);
      }

      byte[] securedBytes = concatenate(signature, bodyBytes);
      byte[] result = new byte[securedBytes.length + 1];
      result[0] = 1;
      System.arraycopy(securedBytes, 0, result, 1, securedBytes.length);
      return Base64.encodeBase64String(result);
   }

   @AfterConfigActivation
   void preprocessConfiguration() throws InitializeException {
      this.initKeys();
   }

   private void initKeys() throws InitializeException {
      RSAPrivateKeySpec privKeySpec = new RSAPrivateKeySpec(new BigInteger(EspSignTicketDecoder.MODULUS), new BigInteger(PRIVATE_EXPONENT));

      RSAPrivateKey privateKey;
      try {
         privateKey = (RSAPrivateKey)KeyFactory.getInstance("RSA").generatePrivate(privKeySpec);
      } catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
         throw new InitializeException("Error initializing ticket encoder RSA key (" + e.getMessage() + ")", e);
      }

      try {
         synchronized (this.signerLock) {
            this.signer = Signature.getInstance("SHA1withRSA");
            this.signer.initSign(privateKey);
         }
      } catch (NoSuchAlgorithmException | InvalidKeyException e) {
         throw new InitializeException("Error initializing ticket encoder signer object (" + e.getMessage() + ")", e);
      }
   }

   private static String urlEncode(String s) {
      if (s == null) {
         throw new IllegalArgumentException("s is null");
      } else {
         return UrlUtils.urlEncodeCookie(s, EspSignTicketDecoder.URL_ENCODING);
      }
   }

   public static byte[] longToBytes(long value) {
      int bitsInAByte = 8;
      return new byte[]{
         (byte)(value >>> 56),
         (byte)(value >>> 48),
         (byte)(value >>> 40),
         (byte)(value >>> 32),
         (byte)(value >>> 24),
         (byte)(value >>> 16),
         (byte)(value >>> 8),
         (byte)value
      };
   }

   public static byte[] concatenate(byte[] ba1, byte[] ba2) {
      if (ba1 == null) {
         throw new IllegalArgumentException("byte array 1 is null");
      }

      if (ba2 == null) {
         throw new IllegalArgumentException("byte array 2 is null");
      }

      byte[] res = new byte[ba1.length + ba2.length];
      System.arraycopy(ba1, 0, res, 0, ba1.length);
      System.arraycopy(ba2, 0, res, ba1.length, ba2.length);
      return res;
   }

   private byte[] sign(byte[] input) throws SignatureException {
      if (input == null) {
         throw new IllegalArgumentException("input is null");
      }

      synchronized (this.signerLock) {
         this.signer.update(input);
         return this.signer.sign();
      }
   }

   @SuppressFBWarnings(justification = "generated code")
   public static EspSignTicketEncoderBuilder builder() {
      return new EspSignTicketEncoderBuilder();
   }
}
