public interface SmsNotification {

    public void notifyUser(String type);

    public String getBalance();

    public String getStatus(String msgId);

}

//TODO add telephone number field to entity and web form
/**
 * Service class which gives opportunity send sms notifications to user's mobile phone,
 * get balance on service account, get status of notification (in this partial case vendor is)
 */
@Service
public class SmsNotificationTurboSmsImpl implements SmsNotification {

    private final static Logger logger = LoggerFactory.getLogger(SmsNotificationTurboSmsImpl.class);

    private final static String NEW_MESSAGE_TYPE = "message";
    private final static String NEW_VIDEO_TYPE = "video";
    private final static String NEW_ADVERTISEMENT_TYPE = "advertisement";

    @Inject
    private UserRepository userRepository;

    @Inject
    private static MessageSource messageSource;

    @Inject
    private RequestBuilder requestBuilder;

    private boolean serviceEnabled;

    private final static JHipsterProperties jHipsterProperties = new JHipsterProperties();
    private final static String serviceLogin = jHipsterProperties.getSmsNotification().getLogin();
    private final static String servicePassword = jHipsterProperties.getSmsNotification().getPassword();
    private final static String applicationName = jHipsterProperties.getSmsNotification().getApplicationName();

    private final static String WRONG_MESSAGE_TYPE = "Wrong message type";

    /*
    * Top level method to notify user by sending him sms
    * Invoking service classes to get xml query, establish connection,
    * send request and process response in well-read form
    * Result should be logged for further control by administrator
    * Current user getting from security utils class
    * @param type - type of message(new message or video etc)
    *
     */
    @Override
    public void notifyUser(String type) {
        if(serviceEnabled = false){
            return;
            }
        String login = SecurityUtils.getCurrentUserLogin();
        User user = userRepository.findOneByLogin(login).get();
        logger.debug("Invoke notifyUser method with user {}, message type {}", user.getLogin(), type);
        Locale locale = Locale.forLanguageTag(user.getLangKey());
        String telNumber = user.getEmail();                     //TODO add new field to user entity!
        String request = getRequestStringToSendSms(type, locale, telNumber);
        logger.info("Sending sms notification request was sent to service, message recipient is user {}, message type is {}", user.getLogin(), type);
        String rawResponse = requestBuilder.doXMLQuery(request);
        String parsedResponse = SmsNotificationUtils.parseDeliveryOrBalanceReport(rawResponse);
        logger.info("Notification service response after sending request to notify user {} with message type {} is: {}", user.getLogin(), type, parsedResponse);
    }

    /*
    * Top-level method for getting current account balance of sms service
    * @return parsed response - response in well-read form
     */
    @Override
    public String getBalance() {
        String request = getRequestStringToGetBalance();
        String rawResponse = requestBuilder.doXMLQuery(request);
        String parsedResponse = SmsNotificationUtils.parseDeliveryOrBalanceReport(rawResponse);
        return parsedResponse;
    }

    /*
    * Top-level method for getting status of particular sms
    * @param msgId - sms id
    * @return response in well-read form
     */
    @Override
    public String getStatus(String msgId) {
        String request = getRequestStringToGetSmsStatus(msgId);
        String rawResponse = requestBuilder.doXMLQuery(request);
        String parsedResponse = SmsNotificationUtils.parseMessageStatusResponse(rawResponse);
        return parsedResponse;
    }

    /*
    * Create a string with XML query for sending sms
    * @param type - type of message
    * @param locale - user's locale
    * @param telNumber
    * @return string with query
     */
    private static String getRequestStringToSendSms(String type, Locale locale,
                                                    String telNumber){
        String text = null;
        switch (type){
            case NEW_ADVERTISEMENT_TYPE : text = messageSource.getMessage("sms.notification.newAdvertisement", null, locale);
            case NEW_MESSAGE_TYPE : text = messageSource.getMessage("sms.notification.newMessage", null, locale);
            case NEW_VIDEO_TYPE : text = messageSource.getMessage("sms.notification.newVideo", null, locale);
        }
        if(text.equals(null)){
            throw new RuntimeException(WRONG_MESSAGE_TYPE);
        }
        StringBuilder sb = new StringBuilder();
            sb.append("<SMS>");
            sb.append("<operations>");
            sb.append("<operation>SEND</operation>");
            sb.append("</operations>");
            sb.append("<authentification>");
            sb.append("<username>" + serviceLogin + "</username>");
            sb.append("<password>" + servicePassword + "</password>");
            sb.append("</authentification>");
            sb.append("<message>");
            sb.append("<sender>" + applicationName + "</sender>");
            sb.append("<text>" + text + "</text>");
            sb.append("</message>");
            sb.append("<numbers>");
            sb.append(telNumber);
            sb.append("</numbers>");
            sb.append("</SMS>");
        return sb.toString();
    }
    /*
    * Creates a string with XML query for getting account balance
     */
    private static String getRequestStringToGetBalance(){
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
        stringBuilder.append("<SMS>");
        stringBuilder.append("<operations>");
        stringBuilder.append("<operation>BALANCE</operation>");
        stringBuilder.append("</operations>");
        stringBuilder.append("<authentification>");
        stringBuilder.append("<username>" + serviceLogin + "</username>");
        stringBuilder.append("<password>" + servicePassword + "</password>");
        stringBuilder.append("</authentification> ");
        stringBuilder.append("</SMS>");
        return stringBuilder.toString();
    }

    /*
    * Creates a string with XML query for getting message status details
    * @param msgId - sms id
    * @return string with query
     */
    private static String getRequestStringToGetSmsStatus(String msgId){
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
        stringBuilder.append("<SMS><operations><operation>GETSTATUS</operation></operations>");
        stringBuilder.append("<authentification>");
        stringBuilder.append("<username>" + serviceLogin + "</username>");
        stringBuilder.append("<password>" + servicePassword + "</password>");
        stringBuilder.append("</authentification>");
        stringBuilder.append("<statistics>");
        stringBuilder.append("<messageid>" + msgId + "</messageid>");
        stringBuilder.append("</statistics>");
        stringBuilder.append("</SMS>");
        return stringBuilder.toString();
    }

    public SmsNotificationTurboSmsImpl() {
    }

    public UserRepository getUserRepository() {
        return userRepository;
    }

    public void setUserRepository(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public static MessageSource getMessageSource() {
        return messageSource;
    }

    public static void setMessageSource(MessageSource messageSource) {
        SmsNotificationTurboSmsImpl.messageSource = messageSource;
    }

    public RequestBuilder getRequestBuilder() {
        return requestBuilder;
    }

    public void setRequestBuilder(RequestBuilder requestBuilder) {
        this.requestBuilder = requestBuilder;
    }

    public boolean isServiceEnabled() {
        return serviceEnabled;
    }

    public void setServiceEnabled(boolean serviceEnabled) {
        this.serviceEnabled = serviceEnabled;
    }

    public static JHipsterProperties getjHipsterProperties() {
        return jHipsterProperties;
    }
}




/**
 * Connection with sms service API
 */
public class TurboSmsVendorConnection {

    private final static Logger logger = LoggerFactory.getLogger(TurboSmsVendorConnection.class);

    private final static Boolean USE_CACHES = false;
    private final static Boolean DO_INPUT = true;
    private final static Boolean DO_OUTPUT = true;
    private final static String CONNECTION_ERROR = "Connection is not established!";
    private final static String ENCODING = "UTF-8";
    private final static String PARAM_SEPARATOR = "&";
    private final static String PARAM_EQUAL = "=";

    private static HttpURLConnection httpURLConnection;

    /*
    * Makes HTTP request using POST method to specified URL
    * @param requestURL - message service URL
    * @param params - map contains POST data in pairs key-value
    * @throws IOException
    * @return httpURLConnection instance
     */
    public static HttpURLConnection sendPostRequest(String requestUrl, Map<String, String> params) throws IOException {
        logger.debug("Invoke sendPostRequest method");
        URL url = new URL(requestUrl);
        httpURLConnection = (HttpURLConnection) url.openConnection();
        httpURLConnection.setUseCaches(USE_CACHES);
        httpURLConnection.setDoInput(DO_INPUT);
        StringBuffer requestParams = new StringBuffer();

        if (params != null && params.size() > 0) {
            httpURLConnection.setDoOutput(DO_OUTPUT); // true indicates POST request
            Iterator<String> paramIterator = params.keySet().iterator();

            boolean isFirst=true;
            while (paramIterator.hasNext()) {
                String key = paramIterator.next();
                String value = params.get(key);
                if(!isFirst){  requestParams.append(PARAM_SEPARATOR);}
                requestParams.append(key);
                requestParams.append(PARAM_EQUAL).append(URLEncoder.encode(value, ENCODING));
                isFirst=false;
            }
            // sends POST request
            OutputStreamWriter writer = new OutputStreamWriter(httpURLConnection.getOutputStream());
            URLEncoder.encode(requestParams.toString(), ENCODING);
            writer.write(requestParams.toString());
            writer.flush();
        }
        return httpURLConnection;
    }

    /*
    * Pulls string with response from httpURLConnection
    * @throws IOException
     */
    public static String readSingleLine() throws IOException {
        logger.debug("Invoke readSingleLine method.");
        InputStream inputStream = null;
        if(httpURLConnection != null){
            inputStream = httpURLConnection.getInputStream();
        }else {
            logger.error("HttpURLConnection not established.");
            throw new IOException(CONNECTION_ERROR);
        }
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
        String response = bufferedReader.readLine();
        bufferedReader.close();
        return response;
    }

    /*
    * Pulls multiple strings from httpURLConnection
    * @return divided by lines response in a string array
    * @throws IOException
     */
    public static String[] readMultipleLinesResponse() throws IOException {
        logger.debug("Invoke readMultipleLinesResponse method.");
        InputStream inputStream = null;
        if (httpURLConnection != null) {
            inputStream = httpURLConnection.getInputStream();
        } else {
            logger.error("HttpURLConnection is not established.");
            throw new IOException(CONNECTION_ERROR);
        }

        BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
        List<String> response = new ArrayList<String>();

        String line = "";
        while ((line = reader.readLine()) != null) {
            response.add(line);
        }
        reader.close();

        return (String[]) response.toArray(new String[0]);
    }

    /*
    * Disconnect method
     */
    public static void disconnect() {
        if(httpURLConnection != null){
            httpURLConnection.disconnect();
        }
    }

    public static HttpURLConnection getHttpURLConnection() {
        return httpURLConnection;
    }

    public static void setHttpURLConnection(HttpURLConnection httpURLConnection) {
        TurboSmsVendorConnection.httpURLConnection = httpURLConnection;
    }
}


/**
 * Wrapper class to perform XML request and getting response
 */
@Component
public class RequestBuilder {

    private final static Logger logger = LoggerFactory.getLogger(RequestBuilder.class);

    private final static String CONNECTION_PARAM = "XML";

    @Inject
    private JHipsterProperties jHipsterProperties;

    /*
    * This method getting string with string query in XML format,
    * invoking connection and returning result string
    * @throws IOException in case appearing any IO exception
    * @param xml - string with query in XML format
    * @return string with service response
     */
    public String doXMLQuery(String xml) {

        logger.debug("Invoke doXMLQuery method.");

        StringBuilder responseString = new StringBuilder();

        Map<String, String> params = new HashMap();
        params.put(CONNECTION_PARAM, xml);
            try {
                TurboSmsVendorConnection.sendPostRequest(jHipsterProperties.getSmsNotification().getURL(), params);
                String[] response = TurboSmsVendorConnection.readMultipleLinesResponse();
                for (String line : response) {
                    responseString.append(line);
                }
            } catch (IOException e) {
                logger.error("IOException in doXMLQueryMethod.", e);
            }
        TurboSmsVendorConnection.disconnect();
        return responseString.toString();
    }

    public JHipsterProperties getjHipsterProperties() {
        return jHipsterProperties;
    }

    public void setjHipsterProperties(JHipsterProperties jHipsterProperties) {
        this.jHipsterProperties = jHipsterProperties;
    }
}


/**
 * Utilize class for processing request "raw" string to well-read form
 */
public final class SmsNotificationUtils {

    private final static String STATUS_PREFIX = "<status>";
    private final static String STATUS_SUFFIX = "</status>";

    private final static String CREDITS_PREFIX = "<credits>";
    private final static String CREDITS_SUFFIX = "</credits>";

    private final static String SENT_DATE = "sentdate=";
    private final static String DONE_DATE = "donedate=";
    private final static String STATUS = "status=";

    private static final Map<String, String> deliveryMessageStatusCodes = new HashMap<>();
    static {
        deliveryMessageStatusCodes.put("-1", "Authentication failed.");
        deliveryMessageStatusCodes.put("-2", "Wrong XML syntax.");
        deliveryMessageStatusCodes.put("-3", "Not enough credits on account balance.");
        deliveryMessageStatusCodes.put("1", "Message was delivered successfully.");
        deliveryMessageStatusCodes.put("0", "Request balance status is OK");
    }

    private static final Map<String, String> messageStatuses = new HashMap<>();
    {
        messageStatuses.put("SENT", "Message was sent.");
        messageStatuses.put("NOT_DELIVERED", "Message wasn't delivered.");
        messageStatuses.put("DELIVERED", "Message was delivered");
        messageStatuses.put("INVALID_DESTINATION_ADDRESS", "Destination address invalid.");
        messageStatuses.put("INVALID_SOURCE_ADDRESS", "Source address invalid");
        messageStatuses.put("NOT_ALLOWED", "Recipient mobile operator is out of processing.");
        messageStatuses.put("NOT_ENOUGH_CREDITS", "Not enough credits on account balance.");
    }

    private final static String DELIVERY_STATUS = "Delivery status is: ";
    private final static String AVAILABLE_CREDITS = "Available credits: ";

    private final static String SEND_DATE_TEXT = "Message send date: ";
    private final static String DONE_DATE_TEXT = "Message delivered at: ";
    private final static String STATUS_TEXT = "Delivery status is: ";

    /*
     * Convert raw response with number codes after string after message sending to well-read form
     * @param serverResponse - raw string from server
     * @return string in well-read form
     */
    public static String parseDeliveryOrBalanceReport(String serverResponse){
        //typical service response is: <?xml version="1.0" encoding="UTF-8"?><RESPONSE><status>2</status><credits>0.682</credits></RESPONSE>
        //AUTH_FAILED -1 Wrong login or password
        //XML_ERROR -2 Wrong XML syntax
        //NOT_ENOUGH_CREDITS -3 Not enough credits on account balance
        //NO_RECIPIENTS -4 Missing correct recipient number
        //SEND_OK  >0 count of received messages, in this case should always be 1
        //SEND_OK 0 request to get current credit balance on service account

        String statusCode = serverResponse.substring(serverResponse.indexOf(STATUS_PREFIX) + STATUS_PREFIX.length(),
                                                                            serverResponse.indexOf(STATUS_SUFFIX));
        String availableCredits = serverResponse.substring(serverResponse.indexOf(CREDITS_PREFIX) + CREDITS_PREFIX.length(),
                                                                            serverResponse.indexOf(CREDITS_SUFFIX));
        String convertedStatusForm = deliveryMessageStatusCodes.get(statusCode);
        return DELIVERY_STATUS + convertedStatusForm + AVAILABLE_CREDITS + availableCredits;
    }

    /*
    * Parses string with "raw" response to well-read form
    * @param messageStatusResponse - response string
     */
    public static String parseMessageStatusResponse(String messageStatusResponse){
        //typical service response is: <?xml version="1.0" encoding="UTF-8"?><deliveryreport><message id="1299" sentdate="0000-00-00 00:00:00" donedate="0000-00-00 00:00:00" status="0" /></deliveryreport>

        //SENT Was sent
        //NOT_DELIVERED Wasn't delivered
        //DELIVERED Delivered
        //NOT_ALLOWED Operator is not allowed
        //INVALID_DESTINATION_ADDRESS Wrong destination address
        //INVALID_SOURCE_ADDRESS Wrong source name
        //NOT_ENOUGH_CREDITS Not enough credits
        String send_date = messageStatusResponse.substring(messageStatusResponse.indexOf(SENT_DATE) + SENT_DATE.length(),
                                                            messageStatusResponse.indexOf(SENT_DATE) + SENT_DATE.length() + 21);
        String done_date = messageStatusResponse.substring(messageStatusResponse.indexOf(DONE_DATE) + DONE_DATE.length(),
                                                            messageStatusResponse.indexOf(DONE_DATE) + DONE_DATE.length() + 21);
        String status_code = messageStatusResponse.substring(messageStatusResponse.indexOf(STATUS) + STATUS.length(),
                                                            messageStatusResponse.lastIndexOf("\""));

        String status = messageStatuses.get(status_code);
        return SEND_DATE_TEXT + send_date + DONE_DATE_TEXT + done_date + STATUS_TEXT + status;
    }
}


public class SmsNotificationTest {

    @Mock
    private RequestBuilder requestBuilder;

    @Inject
    TurboSmsVendorConnection turboSmsVendorConnection;

    SmsNotificationTurboSmsImpl turboSms;

    @Before
    public void setup(){
        MockitoAnnotations.initMocks(this);

        SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
        securityContext.setAuthentication(new UsernamePasswordAuthenticationToken("admin", "admin"));
        SecurityContextHolder.setContext(securityContext);

        turboSms = new SmsNotificationTurboSmsImpl();
        when(requestBuilder.doXMLQuery(anyString())).thenReturn("<?xml version=\"1.0\" encoding=\"UTF-8\"?><RESPONSE><status>1</status><credits>0.682</credits></RESPONSE>");

        turboSms.setServiceEnabled(true);

        ReflectionTestUtils.setField(turboSms, "requestBuilder", requestBuilder);
    }

    @Test
    public void testDeliveryOrBalanceReport(){
        assertThat(turboSms.notifyUser("message")).isEqualTo("Delivery status is: Message was delivered successfully.Available credits: 0.628");
    }

}
