package io.cattle.platform.token.impl;

import io.cattle.platform.archaius.util.ArchaiusUtil;
import io.cattle.platform.token.TokenDecryptionException;
import io.cattle.platform.token.TokenException;
import io.cattle.platform.token.TokenService;

import java.security.interfaces.RSAPublicKey;
import java.text.ParseException;
import java.util.Date;
import java.util.Map;

import javax.inject.Inject;

import org.apache.commons.lang.StringUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

import com.netflix.config.DynamicLongProperty;
import com.netflix.config.DynamicStringProperty;
import com.nimbusds.jose.EncryptionMethod;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JOSEObject;
import com.nimbusds.jose.JWEAlgorithm;
import com.nimbusds.jose.JWEHeader;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSObject;
import com.nimbusds.jose.JWSSigner;
import com.nimbusds.jose.JWSVerifier;
import com.nimbusds.jose.jca.JCASupport;
import com.nimbusds.jose.crypto.RSADecrypter;
import com.nimbusds.jose.crypto.RSAEncrypter;
import com.nimbusds.jose.crypto.RSASSASigner;
import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jwt.EncryptedJWT;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;

public class JwtTokenServiceImpl implements TokenService {

    private static final String KEY_ID = "kid";

    private static final DynamicStringProperty SUBJECT = ArchaiusUtil.getString("jwt.default.subject");
    private static final DynamicStringProperty ISSUER = ArchaiusUtil.getString("jwt.default.issuer");
    private static final DynamicLongProperty EXPIRATION = ArchaiusUtil.getLong("jwt.default.expiration.seconds");

    RSAKeyProvider keyProvider;

    // Delete after Java 8 switch
    static {
        if (!JCASupport.isSupported(EncryptionMethod.A128GCM) &&
            java.security.Security.getProvider("BC") == null) {
            java.security.Security.addProvider(new BouncyCastleProvider());
        }
    }

    @Override
    public String generateToken(Map<String, Object> payload) {
        return generateToken(payload, new Date(), new Date(System.currentTimeMillis() + EXPIRATION.get() * 1000), false);
    }

    @Override
    public String generateToken(Map<String, Object> payload, Date expireDate) {
        return generateToken(payload, new Date(), expireDate, false);
    }

    @Override
    public String generateEncryptedToken(Map<String, Object> payload) {
        return generateToken(payload, new Date(), new Date(System.currentTimeMillis() + EXPIRATION.get() * 1000), true);
    }

    @Override
    public String generateEncryptedToken(Map<String, Object> payload, Date expireDate) {
        return generateToken(payload, new Date(), expireDate, true);
    }

    protected String generateToken(Map<String, Object> payload, Date issueTime, Date expireDate, boolean encrypted) {

        // Prepare JWT with claims set
        JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder();

        if (payload != null) {
            for (Map.Entry<String, Object> entry : payload.entrySet()) {
                builder.claim(entry.getKey(), entry.getValue());
            }
        }

        if (expireDate != null) {
            builder.expirationTime(expireDate);
        }

        builder.subject(SUBJECT.get());
        builder.issueTime(issueTime);
        builder.issuer(ISSUER.get());

        if (encrypted) {
            JWEHeader header = new JWEHeader(JWEAlgorithm.RSA_OAEP, EncryptionMethod.A128GCM);
            EncryptedJWT jwt = new EncryptedJWT(header, builder.build());
            RSAEncrypter encrypter = new RSAEncrypter((RSAPublicKey) keyProvider.getDefaultPublicKey());
            try {
                jwt.encrypt(encrypter);
            } catch (JOSEException e) {
                throw new RuntimeException("Failed to generate encrypted token", e);
            }
            return jwt.serialize();
        } else {
            RSAPrivateKeyHolder privateKey = keyProvider.getPrivateKey();

            builder.claim(KEY_ID, privateKey.getKeyId());
            // Create RSA-signer with the private key
            JWSSigner signer = new RSASSASigner(privateKey.getKey());

            SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.RS256), builder.build());

            // Compute the RSA signature
            try {
                signedJWT.sign(signer);
            } catch (JOSEException e) {
                throw new RuntimeException("Failed to generate token", e);
            }

            return signedJWT.serialize();
        }
    }

    public RSAKeyProvider getKeyProvider() {
        return keyProvider;
    }

    @Override
    public Map<String, Object> getJsonPayload(String token, boolean encrypted) throws TokenException {
        if (StringUtils.isEmpty(token)) {
            throw new TokenException("null or empty token");
        }
        if (encrypted) {
            EncryptedJWT jwt = null;
            try {
                jwt = EncryptedJWT.parse(token);
                RSADecrypter decrypter = new RSADecrypter(keyProvider.getPrivateKey().getKey());
                jwt.decrypt(decrypter);
            } catch (JOSEException | ParseException e) {
                throw new TokenDecryptionException("Invalid token", e);
            }
            return getJSONObject(jwt, encrypted);
        }
        try {
            JWSObject jws = JWSObject.parse(token);
            JWSVerifier verifier = new RSASSAVerifier((RSAPublicKey) keyProvider.getDefaultPublicKey());
            if (!jws.verify(verifier)) {
                throw new TokenException("ERROR: Fradulent token");
            }
            return getJSONObject(jws, encrypted);
        } catch (TokenException | ParseException | JOSEException e) {
            throw new TokenException("Error: Fradulent token, unrecognized signature", e);
        }
    }

    private Map<String, Object> getJSONObject(JOSEObject jose, boolean encrypted) throws TokenException {
        Long exp = (Long) jose.getPayload().toJSONObject().get("exp");
        if (exp != null && exp * 1000 <= System.currentTimeMillis()) {
            throw new TokenException("Expired Token");
        }
        return jose.getPayload().toJSONObject();
    }

    @Inject
    public void setKeyProvider(RSAKeyProvider keyProvider) {
        this.keyProvider = keyProvider;
    }

}