package com.airlock.iam.common.domain.service.sms;

import com.airlock.iam.base.api.domain.model.exception.FailureException;
import com.airlock.iam.common.api.domain.model.exception.SmsGatewayUnavailableException;
import com.airlock.iam.common.domain.model.sms.SmppSmsGatewayConfiguration;
import com.airlock.iam.core.domain.model.sms.SmppTon;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.elog.api.LogTypes;
import com.cloudhopper.commons.charset.CharsetUtil;
import com.cloudhopper.smpp.SmppSession;
import com.cloudhopper.smpp.impl.DefaultSmppClient;
import com.cloudhopper.smpp.pdu.SubmitSm;
import com.cloudhopper.smpp.pdu.SubmitSmResp;
import com.cloudhopper.smpp.tlv.Tlv;
import com.cloudhopper.smpp.type.Address;
import com.cloudhopper.smpp.type.RecoverablePduException;
import com.cloudhopper.smpp.type.SmppChannelException;
import com.cloudhopper.smpp.type.SmppInvalidArgumentException;
import com.cloudhopper.smpp.type.SmppTimeoutException;
import com.cloudhopper.smpp.type.UnrecoverablePduException;
import com.google.common.annotations.VisibleForTesting;
import java.io.Closeable;
import java.util.Objects;
import java.util.concurrent.Semaphore;
import org.apache.commons.lang3.Strings;

public final class SmppSmsGateway implements Closeable {
   private static final ELogger LOG = ELoggerFactory.getLogger(SmppSmsGateway.class);
   private final DefaultSmppClient defaultSmppClient;
   private final SmppSmsGatewayConfiguration config;
   private final Semaphore availableSessions;

   public SmppSmsGateway(SmppSmsGatewayConfiguration config) {
      this.defaultSmppClient = new DefaultSmppClient();
      this.config = config;
      this.availableSessions = new Semaphore(config.getMaxNumberOfSessions(), true);
   }

   @Override
   public void close() {
      this.defaultSmppClient.destroy();
      LOG.debug("Closed SmppSmsGateway instance.");
   }

   private SmppSession bindSmppSession() throws SmppTimeoutException, UnrecoverablePduException, SmppChannelException, InterruptedException {
      SmppSession smppSession = this.defaultSmppClient.bind(this.config.getSmppSessionConfiguration());
      LOG.debug("SMPP session was bound with state: \"" + smppSession.getStateName() + "\"");
      return smppSession;
   }

   private void releaseSmppSession(SmppSession smppSession) {
      if (smppSession != null) {
         smppSession.unbind(5000L);
         smppSession.close();
         smppSession.destroy();
      }
   }

   public String sendSms(String originator, String phoneNr, String message, boolean isFlashMsg) {
      try {
         this.availableSessions.acquire();
         return this.sendSmsInternal(originator, phoneNr, message, isFlashMsg);
      } catch (InterruptedException e) {
         throw new FailureException("error while acquiring a permit for a session to send SMS", e);
      } finally {
         this.availableSessions.release();
      }
   }

   private String sendSmsInternal(String originator, String phoneNr, String message, boolean isFlashMsg) {
      SmppSession smppSession = null;

      try {
         smppSession = this.bindSmppSession();
         SubmitSm submitMsg = this.prepareMessage(originator, phoneNr, message, isFlashMsg);
         SubmitSmResp submitResp = smppSession.submit(submitMsg, this.config.getSendTimeout());
         if (submitResp.getCommandStatus() != 0) {
            LOG.warn(
               LogTypes.SMS_GATEWAY,
               "The SMS could not be sent. SMPP session name is: \""
                  + this.config.getSessionName()
                  + "\" command_status is: \""
                  + submitResp.getCommandStatus()
                  + "\" command_status description is: \""
                  + Objects.toString(submitResp.getResultMessage(), "<empty>")
                  + "\""
            );
            throw new FailureException("Error while sending SMS: Code " + submitResp.getCommandStatus() + ": " + submitResp.getResultMessage());
         } else {
            LOG.debug(
               "SMPP session name is: \""
                  + this.config.getSessionName()
                  + "\" command_status is: \""
                  + submitResp.getCommandStatus()
                  + "\" command_status description is: \""
                  + Objects.toString(submitResp.getResultMessage(), "<empty>")
                  + "\""
            );
            return submitResp.getMessageId();
         }
      } catch (RecoverablePduException | UnrecoverablePduException | SmppTimeoutException | SmppChannelException | InterruptedException e) {
         throw new SmsGatewayUnavailableException("Error while sending SMS to SMPP Gateway:" + e.getMessage(), this.config.getServiceUri(), e);
      } finally {
         this.releaseSmppSession(smppSession);
      }
   }

   private SubmitSm prepareMessage(String originator, String phoneNr, String message, boolean isFlashMsg) {
      try {
         SubmitSm submitMsg = new SubmitSm();
         submitMsg.setSourceAddress(
            new Address(this.config.getSourceTon(), this.config.getSourceNpi(), normalizeNumber(originator, this.config.getSourceTon()))
         );
         submitMsg.setDestAddress(new Address(this.config.getDestTon(), this.config.getDestNpi(), normalizeNumber(phoneNr, this.config.getDestTon())));
         submitMsg.setShortMessage(CharsetUtil.encode(message, this.config.getCharset()));
         byte dataCodingScheme = this.config.getDataCodingScheme().getValue();
         if (isFlashMsg) {
            switch (this.config.getFlashMode()) {
               case DCS_CODE_F0:
                  dataCodingScheme = (byte)(dataCodingScheme | -16);
                  break;
               case DEST_ADDR_SUBUNIT:
                  submitMsg.setOptionalParameter(new Tlv((short)5, new byte[]{1}, "dest_addr_subunit"));
                  break;
               case DCS_CODE_10:
                  dataCodingScheme = (byte)(dataCodingScheme | 16);
               case IGNORE_FLASH:
                  break;
               default:
                  throw new FailureException("Unknown flash mode: " + this.config.getFlashMode());
            }
         }

         submitMsg.setDataCoding(dataCodingScheme);
         return submitMsg;
      } catch (SmppInvalidArgumentException e) {
         throw new FailureException("Invalid message for SMPP gateway: " + e.getMessage(), e);
      }
   }

   private static String normalizeNumber(String number, byte ton) {
      if (ton == SmppTon.INTERNATIONAL.getValue()) {
         return number.startsWith("+") ? Strings.CS.removeStart(number, "+") : Strings.CS.removeStart(number, "00");
      } else {
         return number;
      }
   }

   @VisibleForTesting
   SmppSmsGateway(DefaultSmppClient smppClient, SmppSmsGatewayConfiguration config, Semaphore semaphore) {
      this.defaultSmppClient = smppClient;
      this.config = config;
      this.availableSessions = semaphore;
   }
}
