/*******************************************************************************
 * Copyright 2018 572682
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License.  You may obtain a copy
 * of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
 * License for the specific language governing permissions and limitations under
 * the License.
 ******************************************************************************/
package us.dot.its.jpo.ode.services.asn1;

import java.io.IOException;
import java.text.ParseException;
import java.util.HashMap;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.snmp4j.event.ResponseEvent;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;

import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;

import us.dot.its.jpo.ode.OdeProperties;
import us.dot.its.jpo.ode.context.AppContext;
import us.dot.its.jpo.ode.dds.DdsDepositor;
import us.dot.its.jpo.ode.dds.DdsRequestManager.DdsRequestManagerException;
import us.dot.its.jpo.ode.dds.DdsStatusMessage;
import us.dot.its.jpo.ode.eventlog.EventLogger;
import us.dot.its.jpo.ode.model.Asn1Encoding.EncodingRule;
import us.dot.its.jpo.ode.model.OdeAsdPayload;
import us.dot.its.jpo.ode.model.OdeMsgMetadata;
import us.dot.its.jpo.ode.model.OdeMsgPayload;
import us.dot.its.jpo.ode.plugin.RoadSideUnit.RSU;
import us.dot.its.jpo.ode.plugin.SNMP;
import us.dot.its.jpo.ode.plugin.ServiceRequest;
import us.dot.its.jpo.ode.plugin.SituationDataWarehouse.SDW;
import us.dot.its.jpo.ode.plugin.j2735.DdsAdvisorySituationData;
import us.dot.its.jpo.ode.plugin.j2735.builders.GeoRegionBuilder;
import us.dot.its.jpo.ode.rsu.RsuDepositor;
import us.dot.its.jpo.ode.snmp.SnmpSession;
import us.dot.its.jpo.ode.traveler.TimTransmogrifier;
import us.dot.its.jpo.ode.util.JsonUtils;
import us.dot.its.jpo.ode.util.JsonUtils.JsonUtilsException;
import us.dot.its.jpo.ode.util.XmlUtils;
import us.dot.its.jpo.ode.util.XmlUtils.XmlUtilsException;
import us.dot.its.jpo.ode.wrapper.MessageProducer;

public class Asn1CommandManager {

   public static final String ADVISORY_SITUATION_DATA_STRING = "AdvisorySituationData";
   private static final Logger logger = LoggerFactory.getLogger(Asn1CommandManager.class);

   public static class Asn1CommandManagerException extends Exception {

      private static final long serialVersionUID = 1L;

      public Asn1CommandManagerException(String string) {
         super(string);
      }

      public Asn1CommandManagerException(String msg, Exception e) {
         super(msg, e);
      }

   }

   private String signatureUri;
   private OdeProperties odeProperties;

   private MessageProducer<String, String> stringMessageProducer;

   private String depositTopic;
   private DdsDepositor<DdsStatusMessage> depositor;
   private RsuDepositor rsuDepositor;

   public Asn1CommandManager(OdeProperties odeProperties) {

      this.odeProperties = odeProperties;

      this.signatureUri = odeProperties.getSecuritySvcsSignatureUri();

      try {
         this.depositor = new DdsDepositor<>(odeProperties);
         this.rsuDepositor = new RsuDepositor(odeProperties);
         this.rsuDepositor.start();
         this.stringMessageProducer = MessageProducer.defaultStringMessageProducer(odeProperties.getKafkaBrokers(),
               odeProperties.getKafkaProducerType(), odeProperties.getKafkaTopicsDisabledSet());
         this.setDepositTopic(odeProperties.getKafkaTopicSdwDepositorInput());
      } catch (Exception e) {
         String msg = "Error starting SDW depositor";
         EventLogger.logger.error(msg, e);
         logger.error(msg, e);
      }

   }

   public void depositToSdw(String asdBytes) throws Asn1CommandManagerException {

      if (this.odeProperties.shouldDepositSdwMessagesOverWebsocket()) {
         try {
            depositor.deposit(asdBytes);

            logger.info("Deposited message to SDW directly via websocket");
            logger.debug("Message deposited: {}", asdBytes);
            EventLogger.logger.info("Deposited message to SDW directly via websocket");
            EventLogger.logger.debug("Message deposited: {}", asdBytes);
         } catch (DdsRequestManagerException e) {
            String msg = "Failed to deposit message to SDW";
            throw new Asn1CommandManagerException(msg, e);
         }
      } else {
         stringMessageProducer.send(this.getDepositTopic(), null, asdBytes);

         logger.info("Published message to SDW deposit topic");
         EventLogger.logger.info("Published message to SDW deposit topic");
         logger.debug("Message deposited: {}", asdBytes);
         EventLogger.logger.debug("Message deposited: {}", asdBytes);
      }
   }

   public void sendToRsus(ServiceRequest request, String encodedMsg) {

      rsuDepositor.deposit(request, encodedMsg);
      return;
   }

   public String sendForSignature(String message) {
      HttpHeaders headers = new HttpHeaders();
      headers.setContentType(MediaType.APPLICATION_JSON);

      HttpEntity<String> entity = new HttpEntity<>(JsonUtils.jsonKeyValue("message", message), headers);

      RestTemplate template = new RestTemplate();

      logger.info("Sending data to security services module at {} to be signed", signatureUri);
      logger.debug("Data to be signed: {}", entity);

      ResponseEntity<String> respEntity = template.postForEntity(signatureUri, entity, String.class);

      logger.info("Security services module response: {}", respEntity);

      return respEntity.getBody();
   }

   public String sendForSignature(String message, int sigValidityOverride) {
      HttpHeaders headers = new HttpHeaders();
      headers.setContentType(MediaType.APPLICATION_JSON);
      Map<String, String> map = new HashMap<>();
      map.put("message", message);
      map.put("sigValidityOverride", Integer.toString(sigValidityOverride));

      HttpEntity<Map<String, String>> entity = new HttpEntity<>(map, headers);
      RestTemplate template = new RestTemplate();

      logger.info("Sending data to security services module with validity override at {} to be signed",
            signatureUri);
      logger.debug("Data to be signed: {}", entity);

      ResponseEntity<String> respEntity = template.postForEntity(signatureUri, entity, String.class);

      logger.debug("Security services module response: {}", respEntity);

      return respEntity.getBody();
   }

   public String packageSignedTimIntoAsd(ServiceRequest request, String signedMsg) {

      SDW sdw = request.getSdw();
      SNMP snmp = request.getSnmp();
      DdsAdvisorySituationData asd = null;

      byte sendToRsu = request.getRsus() != null ? DdsAdvisorySituationData.RSU : DdsAdvisorySituationData.NONE;
      byte distroType = (byte) (DdsAdvisorySituationData.IP | sendToRsu);
      //
      String outputXml = null;
      try {
         if (null != snmp) {

            asd = new DdsAdvisorySituationData()
                  .setAsdmDetails(snmp.getDeliverystart(), snmp.getDeliverystop(), distroType, null)
                  .setServiceRegion(GeoRegionBuilder.ddsGeoRegion(sdw.getServiceRegion())).setTimeToLive(sdw.getTtl())
                  .setGroupID(sdw.getGroupID()).setRecordID(sdw.getRecordId());
         } else {
            asd = new DdsAdvisorySituationData()
                  .setAsdmDetails(sdw.getDeliverystart(), sdw.getDeliverystop(), distroType, null)
                  .setServiceRegion(GeoRegionBuilder.ddsGeoRegion(sdw.getServiceRegion())).setTimeToLive(sdw.getTtl())
                  .setGroupID(sdw.getGroupID()).setRecordID(sdw.getRecordId());
         }

         OdeMsgPayload payload = null;

         ObjectNode dataBodyObj = JsonUtils.newNode();
         ObjectNode asdObj = JsonUtils.toObjectNode(asd.toJson());
         ObjectNode admDetailsObj = (ObjectNode) asdObj.findValue("asdmDetails");
         admDetailsObj.remove("advisoryMessage");
         admDetailsObj.put("advisoryMessage", signedMsg);

         dataBodyObj.set(ADVISORY_SITUATION_DATA_STRING, asdObj);

         payload = new OdeAsdPayload(asd);

         ObjectNode payloadObj = JsonUtils.toObjectNode(payload.toJson());
         payloadObj.set(AppContext.DATA_STRING, dataBodyObj);

         OdeMsgMetadata metadata = new OdeMsgMetadata(payload);
         ObjectNode metaObject = JsonUtils.toObjectNode(metadata.toJson());

         ObjectNode requestObj = JsonUtils.toObjectNode(JsonUtils.toJson(request, false));

         requestObj.remove("tim");

         metaObject.set("request", requestObj);

         ArrayNode encodings = buildEncodings();
         ObjectNode enc = XmlUtils.createEmbeddedJsonArrayForXmlConversion(AppContext.ENCODINGS_STRING, encodings);
         metaObject.set(AppContext.ENCODINGS_STRING, enc);

         ObjectNode message = JsonUtils.newNode();
         message.set(AppContext.METADATA_STRING, metaObject);
         message.set(AppContext.PAYLOAD_STRING, payloadObj);

         ObjectNode root = JsonUtils.newNode();
         root.set(AppContext.ODE_ASN1_DATA, message);

         outputXml = XmlUtils.toXmlStatic(root);

         // remove the surrounding <ObjectNode></ObjectNode>
         outputXml = outputXml.replace("<ObjectNode>", "");
         outputXml = outputXml.replace("</ObjectNode>", "");

      } catch (ParseException | JsonUtilsException | XmlUtilsException e) {
         logger.error("Parsing exception thrown while populating ASD structure: {}", e);
      }

      logger.debug("Fully crafted ASD to be encoded: {}", outputXml);

      return outputXml;
   }

   public static ArrayNode buildEncodings() throws JsonUtilsException {
      ArrayNode encodings = JsonUtils.newArrayNode();
      encodings.add(TimTransmogrifier.buildEncodingNode(ADVISORY_SITUATION_DATA_STRING, ADVISORY_SITUATION_DATA_STRING,
            EncodingRule.UPER));
      return encodings;
   }

   public String getDepositTopic() {
      return depositTopic;
   }

   public void setDepositTopic(String depositTopic) {
      this.depositTopic = depositTopic;
   }

}
