package riomhaire.lti.core.business.commands;


import com.auth0.jwt.JWT;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.jose4j.jwk.PublicJsonWebKey;
import org.jose4j.jwk.RsaJsonWebKey;
import org.jose4j.jws.AlgorithmIdentifiers;
import org.jose4j.jws.JsonWebSignature;
import org.jose4j.keys.RsaKeyUtil;
import org.jose4j.lang.JoseException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.ModelAndView;
import riomhaire.lti.core.model.LTIMessage;
import riomhaire.lti.core.model.interfaces.ApplicationRegistry;
import riomhaire.lti.core.model.interfaces.Command;

import java.io.DataOutputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;

public class StoreDeepLinkInLMSCommand implements Command<ModelAndView, ApplicationRegistry<ModelAndView, LTIMessage>, LTIMessage> {

        public static final String LTI_CLAIM_DEPLOYMENT_ID = "https://purl.imsglobal.org/spec/lti/claim/deployment_id";
        public static final String LTI_CLAIM_MESSAGE_TYPE = "https://purl.imsglobal.org/spec/lti/claim/message_type";
        public static final String LTI_CLAIM_VERSION = "https://purl.imsglobal.org/spec/lti/claim/version";
        public static final String LTI_DL_CLAIM_CONTENT_ITEMS = "https://purl.imsglobal.org/spec/lti-dl/claim/content_items";
        public static final String LTI_CLAIM_TARGET_LINK_URI = "https://purl.imsglobal.org/spec/lti/claim/target_link_uri";
        public static final String LTI_DL_CLAIM_DEEP_LINKING_SETTINGS = "https://purl.imsglobal.org/spec/lti-dl/claim/deep_linking_settings";
        public static final String LTI_DEEP_LINK_RETURN_URL = "deep_link_return_url";
        // Logger
        private Logger log = LoggerFactory.getLogger(this.getClass());

        @Override
public ModelAndView execute(ApplicationRegistry<ModelAndView,LTIMessage> environment, LTIMessage message) {
        var signedDLJwt="";

        // OK steps are
        // 1 - decode originating token
        var token = message.getQueryParams().get("id_token");
        var jwt = JWT.decode(token);
        
        // Lookup client id and issuer
        var issuer = jwt.getClaim("iss").asString();
        var audience = jwt.getClaim("aud").asString();

        // 2 - build response structure
        var deepLinkResponse = new HashMap<String,Object>();
        // Common stuff
        deepLinkResponse.put("aud",issuer);
        deepLinkResponse.put("iss", audience);

        deepLinkResponse.put(LTI_CLAIM_MESSAGE_TYPE,"LtiDeepLinkingRequest");
        deepLinkResponse.put(LTI_CLAIM_DEPLOYMENT_ID,jwt.getClaim(LTI_CLAIM_DEPLOYMENT_ID).asString());
        deepLinkResponse.put(LTI_CLAIM_VERSION,"1.3.0");
        deepLinkResponse.put("iat", Instant.now().getEpochSecond());
        deepLinkResponse.put("exp", Instant.now().plus(1, ChronoUnit.HOURS).getEpochSecond());
        deepLinkResponse.put("nonce", UUID.randomUUID().toString());
        // Add the actual item
        var contentItems = new ArrayList<Map<String,Object>>();
        var contentLink = new HashMap<String,Object>();
        var customParameters = new HashMap<String,Object>();
        customParameters.put("resource_url",message.getQueryParams().get("url"));

        contentLink.put("type","ltiResourceLink");
        contentLink.put("title",message.getQueryParams().get("title"));
        contentLink.put("url",jwt.getClaim(LTI_CLAIM_TARGET_LINK_URI).asString());
        contentLink.put("custom",customParameters);

        contentItems.add(contentLink);
        deepLinkResponse.put(LTI_DL_CLAIM_CONTENT_ITEMS,contentItems);

        // 3 - sign response structure using our private key
        var optionalConfiguration =  environment.toolConfigurationResolver().lookup();
        if( !optionalConfiguration.isEmpty() ) {
                // 4 - post a response to LMS      (or set up for self posting form)
//        var targetUrl =
                try {
                        signedDLJwt = signPayloadUsingOurPrivateKey(deepLinkResponse, optionalConfiguration);
                } catch (Exception  e) {
                        // Todo handle this
                   log.error(e.getMessage(),e);
                }


        }

        // 5 - display 'success' page (maybe inc self post form)
        var lmsDeepLinkClaim = jwt.getClaim(LTI_DL_CLAIM_DEEP_LINKING_SETTINGS);
        var lmsTargetUrl = lmsDeepLinkClaim.asMap().get(LTI_DEEP_LINK_RETURN_URL).toString();
        var state = message.getQueryParams().get("state").toString();
        var mav = new ModelAndView("store-deep-link-in-lms");
        mav.addObject("targetURL",lmsTargetUrl);
        mav.addObject("state",state);
        mav.addObject("JWT",signedDLJwt);
        
        return mav;
   }


        /**
         * This method signs a series of claims using our public key
         * @param deepLinkResponse
         * @param optionalConfiguration
         * @return
         * @throws JsonProcessingException
         * @throws JoseException
         * @throws InvalidKeySpecException
         */
        private String signPayloadUsingOurPrivateKey(HashMap<String, Object> deepLinkResponse, Optional<riomhaire.lti.core.model.configuration.ToolConfiguration> optionalConfiguration) throws JsonProcessingException, JoseException, InvalidKeySpecException, NoSuchAlgorithmException {
                JsonWebSignature jws = new JsonWebSignature();

                // The payload of the JWS is JSON content of the JWT Claims
                ObjectMapper serializer = new ObjectMapper();
                String json = serializer.writerWithDefaultPrettyPrinter().writeValueAsString(deepLinkResponse);



                jws.setPayload(json);

                // The JWT is signed using the private key - we will use the 1st in the list
                var jwkKey = optionalConfiguration.get().getJwkKeys()[0];

                var privatePemString =  new String(Base64.getDecoder().decode(jwkKey.getPrivateKey()));   // default to 1st
                var publicPemString =  new String(Base64.getDecoder().decode(jwkKey.getPublicKey()));   // default to 1st
                var rsaJsonWebKey = (RsaJsonWebKey) PublicJsonWebKey.Factory.newPublicJwk(new RsaKeyUtil().fromPemEncoded(publicPemString));
                // So target will know which to verify against
                rsaJsonWebKey.setKeyId(jwkKey.getKeyId());
                // Add private key  after striping headers
                privatePemString = privatePemString.replace("-----BEGIN RSA PRIVATE KEY-----", "");
                privatePemString = privatePemString.replace("-----END RSA PRIVATE KEY-----", "");
                privatePemString = privatePemString.replaceAll("\\s+","");

                var keySpec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privatePemString));
                var keyFactory = KeyFactory.getInstance("RSA");
                var privateKey = keyFactory.generatePrivate(keySpec); // error?

                jws.setKey(privateKey);
                jws.setKeyIdHeaderValue(jwkKey.getKeyId());
                jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256);

                // Sign the JWS and produce the compact serialization or the complete JWT/JWS
                var signedJWT = jws.getCompactSerialization();
                return signedJWT;
        }



//        private void postToLMS(String lmsTargetUrl, String signedDLJwt, String state) {
//                try {
//                        URL url = new URL(lmsTargetUrl);
//                        URLConnection con = null;
//                        con = url.openConnection();
//                        HttpURLConnection http = (HttpURLConnection)con;
//                        http.setRequestMethod("POST"); // PUT is another valid option
//                        http.setDoOutput(true);
//                        StringBuilder formParams = new StringBuilder();
//                        formParams.append("JWT=");
//                        formParams.append(signedDLJwt);
//                        formParams.append("&state=");
//                        formParams.append(state);
//
//                        byte[] out = formParams.toString().getBytes(StandardCharsets.UTF_8);
//                        int length = formParams.length();
//                        http.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
//                        http.setRequestProperty("Content-Length", Integer.toString(length ));
//                        http.setUseCaches(false);
//                        http.connect();
//                        try(DataOutputStream wr = new DataOutputStream(http.getOutputStream())) {
//                                wr.write( out );
//                        }
//                } catch (Throwable e) {
//                        e.printStackTrace();
//                }
//        }

}
