#include <asio.hpp>
#include <botan_all.h>
#include <botan_asio/asio_stream.h>
#include <spdlog/spdlog.h>
#include "server_internal.hxx"

struct SocketData {
    asio::streambuf socket_buf;
    std::istream socket_istream{&socket_buf};
    std::string last_send;
};

struct Exception : public std::exception {
    explicit Exception(std::string msg) : msg(std::move(msg)) {}
    [[nodiscard]] const char* what() const noexcept override { return msg.c_str(); }
    std::string msg;
};

template<typename Socket>
void expect_code(SocketData &data, Socket &s, const std::string& code) {
    std::string line;
    do {
        std::string buf;
        asio::read_until(s, data.socket_buf, "\n");
        std::getline(data.socket_istream, line, '\n');
    } while (line[3] == '-');
    if (std::string_view{line}.substr(0, 3) != code)
        throw Exception{"Request: '" + data.last_send + "' Expected: " + code + " got: " + line};
}

template<typename Socket>
void send(SocketData &data, Socket &s, std::string l) {
    data.last_send = l.substr(0, l.find_first_of('\r'));
    l += "\r\n";
    asio::write(s, asio::buffer(l, l.size()));
}

std::string get_date() {
    char buf[80];
    auto t = std::time(nullptr);
    auto ti = localtime (&t);
    std::strftime(buf, sizeof(buf), "%a, %d %B %Y %T %z", ti);//Format time as string
    return std::string{buf};
}

std::string get_hostname() {
    try {
        return asio::ip::host_name();
    } catch (std::exception &_) {
        return "";
    }
}

struct CredMan : public Botan::Credentials_Manager {
    Botan::System_Certificate_Store str;
    CredMan() = default;
    std::vector<Botan::Certificate_Store*> trusted_certificate_authorities(const std::string&, const std::string&) override {
        return {&str};
    }
};

struct Policy : public Botan::TLS::Strict_Policy {
    [[nodiscard]] bool require_cert_revocation_info() const override { return false; }
};

void Server::send_mail(const std::string &email, const std::string &title, const std::string &body) {
    static std::string host_name = get_hostname();

    try {
        std::string msg = fmt::format(
            "Date: {}\r\n"
            "To: <{}>\r\n"
            "From: MFileserver <{}>\r\n"
            "Subject: {}\r\n"
            "\r\n"
            "{}\r\n.",
            get_date(), email, config.smtp_from, title, body
        );

        asio::io_service ctx;
        auto ssl_ctx = std::make_shared<Botan::TLS::Context>(
            std::make_shared<CredMan>(),
            std::make_shared<Botan::AutoSeeded_RNG>(),
            std::make_shared<Botan::TLS::Session_Manager_Noop>(),
            std::make_shared<Policy>()
        );

        asio::ip::tcp::socket s{ctx};
        asio::ip::tcp::resolver res{ctx};

        SocketData data;

        asio::connect(s, res.resolve(config.smtp_host, std::to_string(config.smtp_port)));
        expect_code(data, s, "220");

        send(data, s, "EHLO " + host_name);
        expect_code(data, s, "250");

        send(data, s, "STARTTLS");
        expect_code(data, s, "220");

        // switch_to_ssl
        Botan::TLS::Stream<asio::ip::tcp::socket> ss(std::move(s), ssl_ctx);
        ss.handshake(Botan::TLS::Connection_Side::Client);

        send(data, ss, "EHLO " + host_name);
        expect_code(data, ss, "250");

        send(data, ss, "AUTH LOGIN");
        expect_code(data, ss, "334");

        send(data, ss, Botan::base64_encode((std::uint8_t*)config.smtp_user.data(), config.smtp_user.size()));
        expect_code(data, ss, "334");

        send(data, ss, Botan::base64_encode((std::uint8_t*)config.smtp_pass.data(), config.smtp_pass.size()));
        expect_code(data, ss, "235");

        send(data, ss, "MAIL FROM:<" + config.smtp_from + ">");
        expect_code(data, ss, "250");

        send(data, ss, "RCPT TO:<" + email + ">");
        expect_code(data, ss, "250");

        send(data, ss, "DATA");
        expect_code(data, ss, "354");

        send(data, ss, msg);
        expect_code(data, ss, "250");

        send(data, ss, "QUIT");
        expect_code(data, ss, "221");
    } catch (const std::exception &e) {
        spdlog::error("Failed to send mail");
        spdlog::error(e.what());
    }
}
