/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.apache.camel.component.cm;

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.UUID;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Text;

import org.apache.camel.component.cm.exceptions.CMDirectException;
import org.apache.camel.component.cm.exceptions.XMLConstructionException;
import org.apache.camel.component.cm.exceptions.cmresponse.CMResponseException;
import org.apache.camel.component.cm.exceptions.cmresponse.InsufficientBalanceException;
import org.apache.camel.component.cm.exceptions.cmresponse.InvalidProductTokenException;
import org.apache.camel.component.cm.exceptions.cmresponse.NoAccountFoundForProductTokenException;
import org.apache.camel.component.cm.exceptions.cmresponse.UnknownErrorException;
import org.apache.camel.component.cm.exceptions.cmresponse.UnroutableMessageException;
import org.apache.hc.client5.http.classic.HttpClient;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class CMSenderOneMessageImpl implements CMSender {

    private static final Logger LOG = LoggerFactory.getLogger(CMSenderOneMessageImpl.class);

    private final String url;
    private final UUID productToken;
    private final HttpClient client;

    public CMSenderOneMessageImpl(final HttpClient client, final String url, final UUID productToken) {
        this.url = url;
        this.productToken = productToken;
        this.client = client;
    }

    /**
     * Sends a message to CM endpoints. 1. CMMessage instance is going to be marshalled to xml. 2. Post request xml
     * string to CMEndpoint.
     */
    @Override
    public void send(final CMMessage cmMessage) {

        // See: Check https://dashboard.onlinesmsgateway.com/docs for responses

        // 1.Construct XML. Throws XMLConstructionException
        final String xml = createXml(cmMessage);

        // 2. Try to send to CM SMS Provider ...Throws CMResponseException
        doHttpPost(url, xml);
    }

    private String createXml(final CMMessage message) {
        try {
            final ByteArrayOutputStream xml = new ByteArrayOutputStream();
            final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
            factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
            factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
            factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
            factory.setNamespaceAware(true);

            // Get the DocumentBuilder
            final DocumentBuilder docBuilder = factory.newDocumentBuilder();

            // Create blank DOM Document
            final DOMImplementation impl = docBuilder.getDOMImplementation();
            final Document doc = impl.createDocument(null, "MESSAGES", null);

            // ROOT Element es MESSAGES
            final Element root = doc.getDocumentElement();

            // AUTHENTICATION element
            final Element authenticationElement = doc.createElement("AUTHENTICATION");
            final Element productTokenElement = doc.createElement("PRODUCTTOKEN");
            authenticationElement.appendChild(productTokenElement);
            final Text productTokenValue = doc.createTextNode(productToken.toString());
            productTokenElement.appendChild(productTokenValue);
            root.appendChild(authenticationElement);

            // MSG Element
            final Element msgElement = doc.createElement("MSG");
            root.appendChild(msgElement);

            // <FROM>VALUE</FROM>
            final Element fromElement = doc.createElement("FROM");
            fromElement.appendChild(doc.createTextNode(message.getSender()));
            msgElement.appendChild(fromElement);

            // <BODY>VALUE</BODY>
            final Element bodyElement = doc.createElement("BODY");
            bodyElement.appendChild(doc.createTextNode(message.getMessage()));
            msgElement.appendChild(bodyElement);

            // <TO>VALUE</TO>
            final Element toElement = doc.createElement("TO");
            toElement.appendChild(doc.createTextNode(message.getPhoneNumber()));
            msgElement.appendChild(toElement);

            // <DCS>VALUE</DCS> - if UNICODE - messageOut.isGSM338Enc
            // false
            if (message.isUnicode()) {
                final Element dcsElement = doc.createElement("DCS");
                dcsElement.appendChild(doc.createTextNode("8"));
                msgElement.appendChild(dcsElement);
            }

            // <REFERENCE>VALUE</REFERENCE> -Alfanum
            final String id = message.getIdAsString();
            if (id != null && !id.isEmpty()) {
                final Element refElement = doc.createElement("REFERENCE");
                refElement.appendChild(doc.createTextNode(message.getIdAsString()));
                msgElement.appendChild(refElement);
            }

            // <MINIMUMNUMBEROFMESSAGEPARTS>1</MINIMUMNUMBEROFMESSAGEPARTS>
            // <MAXIMUMNUMBEROFMESSAGEPARTS>8</MAXIMUMNUMBEROFMESSAGEPARTS>
            if (message.isMultipart()) {
                final Element minMessagePartsElement = doc.createElement("MINIMUMNUMBEROFMESSAGEPARTS");
                minMessagePartsElement.appendChild(doc.createTextNode("1"));
                msgElement.appendChild(minMessagePartsElement);

                final Element maxMessagePartsElement = doc.createElement("MAXIMUMNUMBEROFMESSAGEPARTS");
                maxMessagePartsElement.appendChild(doc.createTextNode(Integer.toString(message.getMultiparts())));
                msgElement.appendChild(maxMessagePartsElement);
            }

            // Create XML as String
            TransformerFactory transformerFactory = TransformerFactory.newInstance();
            transformerFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, Boolean.TRUE);
            transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
            transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");
            final Transformer aTransformer = transformerFactory.newTransformer();
            aTransformer.setOutputProperty(OutputKeys.INDENT, "yes");
            final Source src = new DOMSource(doc);
            final Result dest = new StreamResult(xml);
            aTransformer.transform(src, dest);
            return xml.toString();
        } catch (final TransformerException | ParserConfigurationException e) {
            throw new XMLConstructionException(String.format("Cant serialize CMMessage %s", message), e);
        }
    }

    private void doHttpPost(final String urlString, final String requestString) {

        final HttpPost post = new HttpPost(urlString);
        post.setEntity(new StringEntity(requestString, StandardCharsets.UTF_8));

        try {
            client.execute(post,
                    response -> {
                        final int statusCode = response.getCode();

                        LOG.debug("Response Code : {}", statusCode);

                        if (statusCode == 400) {
                            throw new CMDirectException(
                                    "CM Component and CM API show some kind of inconsistency. "
                                                        + "CM is complaining about not using a post method for the request. And this component only uses POST requests. What happens?");
                        }

                        if (statusCode != 200) {
                            throw new CMDirectException(
                                    "CM Component and CM API show some kind of inconsistency. The component expects the status code to be 200 or 400. New api released? ");
                        }

                        // So we have 200 status code...

                        // The response type is 'text/plain' and contains the actual
                        // result of the request processing.

                        // We obtain the result text
                        try (BufferedReader rd = new BufferedReader(new InputStreamReader(response.getEntity().getContent()))) {
                            final StringBuilder result = new StringBuilder();
                            String line;
                            while ((line = rd.readLine()) != null) {
                                result.append(line);
                            }

                            // ... and process it

                            line = result.toString();
                            if (!line.isEmpty()) {

                                // Line is not empty = error
                                LOG.debug("Result of the request processing: FAILED\n{}", line);

                                // The response text contains the error description. We will
                                // throw a custom exception for each.

                                if (line.contains(CMConstants.ERROR_UNKNOWN)) {
                                    throw new UnknownErrorException();
                                } else if (line.contains(CMConstants.ERROR_NO_ACCOUNT)
                                        || line.contains(CMConstants.ERROR_NO_USER)) {
                                    throw new NoAccountFoundForProductTokenException();
                                } else if (line.contains(CMConstants.ERROR_INSUFICIENT_BALANCE)) {
                                    throw new InsufficientBalanceException();
                                } else if (line.contains(CMConstants.ERROR_UNROUTABLE_MESSAGE)) {
                                    throw new UnroutableMessageException();
                                } else if (line.contains(CMConstants.ERROR_INVALID_PRODUCT_TOKEN)) {
                                    throw new InvalidProductTokenException();
                                } else {
                                    throw new CMResponseException(line);
                                }
                            }

                            // Ok. Line is EMPTY - successfully submitted
                            LOG.debug("Result of the request processing: Successfully submitted");
                        }
                        return null;
                    });

        } catch (final IOException io) {
            throw new CMDirectException(io);
        } catch (Exception t) {
            Exception exception = t;
            if (!(exception instanceof CMDirectException)) {
                // Chain it
                exception = new CMDirectException(exception);
            }
            throw (CMDirectException) exception;
        }
    }
}
