/*******************************************************************************
 * Copyright (c) 2024, Jan Koester jan.koester@gmx.net
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * Redistributions of source code must retain the above copyright
 *      notice, this list of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright
 *      notice, this list of conditions and the following disclaimer in the
 *      documentation and/or other materials provided with the distribution.
 * Neither the name of the <organization> nor the
 *      names of its contributors may be used to endorse or promote products
 *      derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *******************************************************************************/

#include <iostream>

#include <dbpp/database.h>

#include <httppp/http.h>
#include <json-c/json.h>

#include <smtpclient/opportunisticsecuresmtpclient.h>
#include <smtpclient/plaintextmessage.h>
#include <smtpclient/htmlmessage.h>
#include <smtpclient/attachment.h>

#include "mail.h"
#include "blogi.h"

// ---- MailQueue implementation ----

blogi::MailQueue::MailQueue() : _thread(&MailQueue::_worker, this) {}

blogi::MailQueue::~MailQueue(){
    {
        std::lock_guard<std::mutex> lock(_mutex);
        _stop.store(true, std::memory_order_release);
    }
    _cv.notify_one();
    if(_thread.joinable())
        _thread.join();
}

bool blogi::MailQueue::sendMail(SmtpSettings *settings, const char *receiver,
                                const char *subject, const std::string &message){
    std::lock_guard<std::mutex> lock(_mutex);
    if(_queue.size() >= MAX_QUEUE_SIZE){
        std::cerr << "MailQueue: queue full, dropping mail to " << receiver << std::endl;
        return false;
    }
    _queue.push(PlainMail{
        settings->_SHost, settings->_SUser, settings->_SPass, settings->_Sender,
        settings->_SPort, receiver, subject, message
    });
    _cv.notify_one();
    return true;
}

bool blogi::MailQueue::sendHtmlMail(SmtpSettings *settings, const char *receiver,
                                    const char *subject, const std::string &htmlBody,
                                    const std::vector<MailAttachment> &attachments){
    std::lock_guard<std::mutex> lock(_mutex);
    if(_queue.size() >= MAX_QUEUE_SIZE){
        std::cerr << "MailQueue: queue full, dropping HTML mail to " << receiver << std::endl;
        return false;
    }
    _queue.push(HtmlMail{
        settings->_SHost, settings->_SUser, settings->_SPass, settings->_Sender,
        settings->_SPort, receiver, subject, htmlBody, attachments
    });
    _cv.notify_one();
    return true;
}

void blogi::MailQueue::_worker(){
    while(true){
        MailJob job;
        {
            std::unique_lock<std::mutex> lock(_mutex);
            _cv.wait(lock, [this]{ return _stop.load(std::memory_order_acquire) || !_queue.empty(); });
            if(_stop.load(std::memory_order_acquire) && _queue.empty())
                return;
            job = std::move(_queue.front());
            _queue.pop();
        }
        std::visit([](auto &&j){
            using T = std::decay_t<decltype(j)>;
            if constexpr (std::is_same_v<T, PlainMail>)
                _doSendPlain(j);
            else
                _doSendHtml(j);
        }, job);
    }
}

void blogi::MailQueue::_doSendPlain(const PlainMail &job){
    jed_utils::OpportunisticSecureSMTPClient client(job.host.c_str(), job.port);
    client.setCredentials(jed_utils::Credential(job.user.c_str(), job.pass.c_str()));
    try {
        jed_utils::PlaintextMessage msg(
            jed_utils::MessageAddress(job.sender.c_str(), "Blogi Contact Form"),
            { jed_utils::MessageAddress(job.receiver.c_str()) },
            job.subject.c_str(),
            job.message.c_str());

        int err_no = client.sendMail(msg);
        if (err_no != 0) {
            std::cerr << client.getCommunicationLog() << '\n';
            std::cerr << "MailQueue: send failed: " << client.getErrorMessage(err_no)
                      << " (error no: " << err_no << ")" << '\n';
        }
    } catch (std::invalid_argument &err) {
        std::cerr << "MailQueue: " << err.what() << std::endl;
    }
}

void blogi::MailQueue::_doSendHtml(const HtmlMail &job){
    jed_utils::OpportunisticSecureSMTPClient client(job.host.c_str(), job.port);
    client.setCredentials(jed_utils::Credential(job.user.c_str(), job.pass.c_str()));
    try {
        std::vector<jed_utils::Attachment> atts;
        atts.reserve(job.attachments.size());
        for(const auto &a : job.attachments)
            atts.emplace_back(a.filePath.c_str(), a.displayName.c_str(), a.contentId.c_str());

        jed_utils::HTMLMessage msg(
            jed_utils::MessageAddress(job.sender.c_str(), "Blogi"),
            jed_utils::MessageAddress(job.receiver.c_str()),
            job.subject.c_str(),
            job.htmlBody.c_str(),
            nullptr, nullptr,
            atts.empty() ? nullptr : atts.data(),
            atts.size());

        int err_no = client.sendMail(msg);
        if (err_no != 0) {
            std::cerr << client.getCommunicationLog() << '\n';
            std::cerr << "MailQueue: send failed: " << client.getErrorMessage(err_no)
                      << " (error no: " << err_no << ")" << '\n';
        }
    } catch (std::invalid_argument &err) {
        std::cerr << "MailQueue: " << err.what() << std::endl;
    }
}

json_object* blogi::SettingsMail(dbpp::Database &db,libhttppp::HttpRequest &req,const std::string &sessionid){
    std::string host,port,user,pass,sender;
    bool saved = false;

    libhttppp::HttpForm smform;
    smform.parse(req);

    for(const auto &cur : smform.urlData()){
            if(cur.key=="smtphost"){
                setConfig(db,"SMTPHOST",cur.value);
                saved = true;
            }else if(cur.key=="smtpport"){
                setConfig(db,"SMTPPORT",cur.value);
            }else if(cur.key=="smtpuser"){
                setConfig(db,"SMTPUSER",cur.value);
            }else if(cur.key=="smtppass"){
                setConfig(db,"SMTPPASSWORD",cur.value);
            }else if(cur.key=="smtpsender"){
                setConfig(db,"SMTPSENDER",cur.value);
            }
    }

    try { getConfig(db,"SMTPHOST",host); } catch(...) {}
    try { getConfig(db,"SMTPPORT",port); } catch(...) {}
    try { getConfig(db,"SMTPUSER",user); } catch(...) {}
    try { getConfig(db,"SMTPPASSWORD",pass); } catch(...) {}
    try { getConfig(db,"SMTPSENDER",sender); } catch(...) {}

    json_object *jroot = json_object_new_object();
    json_object_object_add(jroot, "title", json_object_new_string("Smtp Settings"));

    if(saved){
        json_object_object_add(jroot, "message", json_object_new_string("Saved!"));
        json_object_object_add(jroot, "message_type", json_object_new_string("success"));
    }

    // Form
    json_object *jform = json_object_new_object();
    json_object_object_add(jform, "method", json_object_new_string("POST"));
    json_object_object_add(jform, "action", json_object_new_string(req.getRequestURL().c_str()));

    json_object *jfields = json_object_new_array();

    auto addField = [&](const char *name, const char *label, const char *type, const std::string &value){
        json_object *jf = json_object_new_object();
        json_object_object_add(jf, "name", json_object_new_string(name));
        json_object_object_add(jf, "label", json_object_new_string(label));
        json_object_object_add(jf, "type", json_object_new_string(type));
        json_object_object_add(jf, "value", json_object_new_string(value.c_str()));
        json_object_array_add(jfields, jf);
    };

    addField("smtphost", "Host", "text", host);
    addField("smtpport", "Port", "text", port);
    addField("smtpuser", "User", "text", user);
    addField("smtppass", "Password", "password", pass);
    addField("smtpsender", "Sender address", "text", sender);

    json_object_object_add(jform, "fields", jfields);
    json_object_object_add(jform, "submit", json_object_new_string("save"));
    json_object_object_add(jroot, "form", jform);

    return jroot;
}

// Global mail queue singleton
static std::unique_ptr<blogi::MailQueue> g_mailQueue;

static blogi::MailQueue& getMailQueue(){
    if(!g_mailQueue)
        g_mailQueue = std::make_unique<blogi::MailQueue>();
    return *g_mailQueue;
}

void blogi::sendMail(SmtpSettings *stmpsettings,const char *reciver,const char *subject,const std::string &mesg){
    getMailQueue().sendMail(stmpsettings, reciver, subject, mesg);
}

void blogi::sendHtmlMail(SmtpSettings *stmpsettings,const char *reciver,const char *subject,
                         const std::string &htmlBody,const std::vector<MailAttachment> &attachments){
    getMailQueue().sendHtmlMail(stmpsettings, reciver, subject, htmlBody, attachments);
}
