#include "mailservice.h"
#include <QFuture>
#include <QFutureWatcher>
#include <QtConcurrent/QtConcurrent>
#include <QDebug>
#include <QFile>
#include <QFileInfo>
#include <QMimeDatabase>
#include <QRegularExpression>
#include <QDateTime>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QEventLoop>
#include <QUrl>
#include "services/mimestorage.h"

namespace {

QByteArray smtpHeader(const QString &name, const QString &value)
{
    const QByteArray utf8 = value.toUtf8();
    bool ascii = true;
    for (char c : utf8) {
        if (static_cast<unsigned char>(c) < 32 || static_cast<unsigned char>(c) > 126) {
            ascii = false;
            break;
        }
    }
    const QByteArray encoded = ascii
        ? utf8
        : QByteArray("=?UTF-8?B?") + utf8.toBase64() + QByteArray("?=");
    return name.toUtf8() + QByteArray(": ") + encoded + QByteArray("\r\n");
}

QByteArray base64Lines(const QByteArray &data)
{
    const QByteArray encoded = data.toBase64();
    QByteArray wrapped;
    for (int i = 0; i < encoded.size(); i += 76)
        wrapped.append(encoded.mid(i, 76)).append("\r\n");
    return wrapped;
}

QStringList smtpRecipients(const QString &value)
{
    QStringList result;
    for (QString part : value.split(QRegularExpression(QStringLiteral("[,;]")), Qt::SkipEmptyParts)) {
        const QRegularExpressionMatch match = QRegularExpression(QStringLiteral("<([^>]+)>")).match(part);
        if (match.hasMatch()) part = match.captured(1);
        part = part.trimmed();
        if (!part.isEmpty()) result.append(part);
    }
    return result;
}

bool smtpResponse(QSslSocket &socket, int expectedCode)
{
    QByteArray line;
    while (true) {
        if (!socket.canReadLine() && !socket.waitForReadyRead(15000)) return false;
        line = socket.readLine();
        if (line.size() < 3) continue;
        bool ok = false;
        const int code = line.left(3).toInt(&ok);
        if (!ok) continue;
        // A space marks the final line of a multiline SMTP response.
        if (line.size() > 3 && line.at(3) == '-') continue;
        return code == expectedCode;
    }
}

QByteArray composeMimeMessage(const MailItem &mail, const Account &account,
                              const QStringList &attachmentPaths, QString &error)
{
    QStringList recipients = smtpRecipients(mail.to());
    if (recipients.isEmpty()) recipients = smtpRecipients(mail.recipient());
    recipients.append(smtpRecipients(mail.cc()));
    recipients.append(smtpRecipients(mail.bcc()));
    if (recipients.isEmpty()) {
        error = QObject::tr("No recipients specified");
        return {};
    }
    recipients.removeDuplicates();

    QByteArray message;
    const QString from = mail.sender().isEmpty() ? account.email() : mail.sender();
    message.append(smtpHeader(QStringLiteral("From"), from));
    message.append(smtpHeader(QStringLiteral("To"), mail.to().isEmpty() ? mail.recipient() : mail.to()));
    if (!mail.cc().isEmpty()) message.append(smtpHeader(QStringLiteral("Cc"), mail.cc()));
    message.append(smtpHeader(QStringLiteral("Subject"), mail.subject()));
    message.append("Date: ").append((mail.date().isValid() ? mail.date() : QDateTime::currentDateTimeUtc()).toString(Qt::RFC2822Date).toUtf8()).append("\r\n");
    message.append("MIME-Version: 1.0\r\n");

    QByteArray body = mail.bodyHtml().toUtf8();
    body.replace("\r\n", "\n");
    body.replace('\r', '\n');
    body.replace('\n', "\r\n");
    if (attachmentPaths.isEmpty()) {
        message.append("Content-Type: text/html; charset=UTF-8\r\n");
        message.append("Content-Transfer-Encoding: 8bit\r\n\r\n");
        message.append(body).append("\r\n");
        return message;
    }

    const QByteArray boundary = QByteArray("----WinoMail-")
        + QByteArray::number(QDateTime::currentMSecsSinceEpoch());
    message.append("Content-Type: multipart/mixed; boundary=\"").append(boundary).append("\"\r\n\r\n");
    message.append("--").append(boundary).append("\r\n");
    message.append("Content-Type: text/html; charset=UTF-8\r\n");
    message.append("Content-Transfer-Encoding: 8bit\r\n\r\n").append(body).append("\r\n");

    QMimeDatabase mimeDatabase;
    for (const QString &path : attachmentPaths) {
        QFile file(path);
        if (!file.open(QIODevice::ReadOnly)) {
            error = QObject::tr("Could not read attachment: %1").arg(path);
            return {};
        }
        const QFileInfo info(path);
        const QString fileName = info.fileName().replace('"', '_');
        const QString mimeType = mimeDatabase.mimeTypeForFile(info).name();
        message.append("--").append(boundary).append("\r\n");
        message.append("Content-Type: ").append(mimeType.toUtf8()).append("; name=\"").append(fileName.toUtf8()).append("\"\r\n");
        message.append("Content-Transfer-Encoding: base64\r\n");
        message.append("Content-Disposition: attachment; filename=\"").append(fileName.toUtf8()).append("\"\r\n\r\n");
        message.append(base64Lines(file.readAll()));
    }
    message.append("--").append(boundary).append("--\r\n");
    return message;
}

bool sendSmtp(const MailItem &mail, const Account &account,
              const QStringList &attachmentPaths, QString &error)
{
    const Account::ConnectionSettings settings = account.connectionSettings();
    if (settings.outgoingHost.isEmpty() || settings.outgoingPort == 0) {
        error = QObject::tr("SMTP server is not configured");
        return false;
    }
    const QByteArray mime = composeMimeMessage(mail, account, attachmentPaths, error);
    if (mime.isEmpty()) return false;
    QSslSocket socket;
    if (settings.outgoingSsl && settings.outgoingPort == 465)
        socket.connectToHostEncrypted(settings.outgoingHost, settings.outgoingPort);
    else
        socket.connectToHost(settings.outgoingHost, settings.outgoingPort);
    if (!socket.waitForConnected(15000)) { error = socket.errorString(); return false; }
    if (settings.outgoingSsl && settings.outgoingPort == 465) {
        if (!socket.waitForEncrypted(15000)) { error = socket.errorString(); return false; }
    }
    if (!smtpResponse(socket, 220)) { error = QObject::tr("SMTP greeting failed"); return false; }

    auto command = [&](const QByteArray &value, int code) {
        socket.write(value + QByteArray("\r\n"));
        socket.flush();
        return smtpResponse(socket, code);
    };
    if (!command("EHLO localhost", 250)) { error = QObject::tr("SMTP EHLO failed"); return false; }
    if (settings.outgoingSsl && settings.outgoingPort != 465) {
        if (!command("STARTTLS", 220)) { error = QObject::tr("SMTP STARTTLS failed"); return false; }
        socket.startClientEncryption();
        if (!socket.waitForEncrypted(15000)) { error = socket.errorString(); return false; }
        if (!command("EHLO localhost", 250)) { error = QObject::tr("SMTP EHLO after TLS failed"); return false; }
    }
    const QString username = settings.username.isEmpty() ? account.email() : settings.username;
    if (!username.isEmpty()) {
        if (!command("AUTH LOGIN", 334)
            || !command(username.toUtf8().toBase64(), 334)
            || !command(settings.password.toUtf8().toBase64(), 235)) {
            error = QObject::tr("SMTP authentication failed");
            return false;
        }
    }
    const QString from = mail.sender().isEmpty() ? account.email() : mail.sender();
    if (!command("MAIL FROM:<" + smtpRecipients(from).value(0).toUtf8() + ">", 250)) { error = QObject::tr("SMTP MAIL FROM failed"); return false; }
    const QStringList recipients = smtpRecipients(mail.to()) + smtpRecipients(mail.recipient())
        + smtpRecipients(mail.cc()) + smtpRecipients(mail.bcc());
    QStringList uniqueRecipients = recipients;
    uniqueRecipients.removeDuplicates();
    for (const QString &recipient : uniqueRecipients) {
        if (!command("RCPT TO:<" + recipient.toUtf8() + ">", 250)) { error = QObject::tr("SMTP recipient rejected: %1").arg(recipient); return false; }
    }
    if (!command("DATA", 354)) { error = QObject::tr("SMTP DATA failed"); return false; }
    QByteArray stuffed;
    const QList<QByteArray> lines = mime.split('\n');
    for (QByteArray line : lines) {
        if (line.startsWith('.')) stuffed.append('.');
        stuffed.append(line);
        if (!line.endsWith('\r')) stuffed.append('\r');
        stuffed.append('\n');
    }
    socket.write(stuffed);
    socket.write(".\r\n");
    socket.flush();
    if (!smtpResponse(socket, 250)) { error = QObject::tr("SMTP message rejected"); return false; }
    command("QUIT", 221);
    return true;
}

QByteArray base64Url(const QByteArray &data)
{
    QByteArray result = data.toBase64();
    result.replace('+', '-');
    result.replace('/', '_');
    while (result.endsWith('=')) result.chop(1);
    return result;
}

bool postJson(const QUrl &url, const QByteArray &token, const QJsonObject &payload,
              int expectedStatus, QString &error)
{
    QNetworkAccessManager network;
    QNetworkRequest request(url);
    request.setRawHeader("Authorization", QByteArrayLiteral("Bearer ") + token);
    request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
    QNetworkReply *reply = network.post(request, QJsonDocument(payload).toJson(QJsonDocument::Compact));
    QEventLoop loop;
    QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
    loop.exec();
    const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
    const bool ok = reply->error() == QNetworkReply::NoError && status == expectedStatus;
    if (!ok) error = reply->error() == QNetworkReply::NoError
        ? QObject::tr("Server returned HTTP %1").arg(status) : reply->errorString();
    reply->deleteLater();
    return ok;
}

bool sendGmailApi(const MailItem &mail, const Account &account,
                  const QStringList &attachmentPaths, QString &error)
{
    if (!account.isTokenValid()) {
        error = QObject::tr("Gmail access token is missing or expired");
        return false;
    }
    const QByteArray mime = composeMimeMessage(mail, account, attachmentPaths, error);
    if (mime.isEmpty()) return false;
    QJsonObject payload;
    payload.insert(QStringLiteral("raw"), QString::fromLatin1(base64Url(mime)));
    const QString user = QString::fromUtf8(QUrl::toPercentEncoding(account.email()));
    const QUrl url(QStringLiteral("https://gmail.googleapis.com/gmail/v1/users/%1/messages/send").arg(user));
    return postJson(url, account.accessToken().toUtf8(), payload, 200, error);
}

QJsonArray graphRecipients(const QString &value)
{
    QJsonArray result;
    for (const QString &address : smtpRecipients(value)) {
        QJsonObject recipient;
        QJsonObject email;
        email.insert(QStringLiteral("address"), address);
        recipient.insert(QStringLiteral("emailAddress"), email);
        result.append(recipient);
    }
    return result;
}

bool sendOutlookApi(const MailItem &mail, const Account &account,
                   const QStringList &attachmentPaths, QString &error)
{
    if (!account.isTokenValid()) {
        error = QObject::tr("Outlook access token is missing or expired");
        return false;
    }
    QJsonObject message;
    message.insert(QStringLiteral("subject"), mail.subject());
    QJsonObject body;
    body.insert(QStringLiteral("contentType"), QStringLiteral("HTML"));
    body.insert(QStringLiteral("content"), mail.bodyHtml());
    message.insert(QStringLiteral("body"), body);
    message.insert(QStringLiteral("toRecipients"), graphRecipients(mail.to().isEmpty() ? mail.recipient() : mail.to()));
    message.insert(QStringLiteral("ccRecipients"), graphRecipients(mail.cc()));
    message.insert(QStringLiteral("bccRecipients"), graphRecipients(mail.bcc()));
    QJsonArray attachments;
    QMimeDatabase mimeDatabase;
    for (const QString &path : attachmentPaths) {
        QFile file(path);
        if (!file.open(QIODevice::ReadOnly)) {
            error = QObject::tr("Could not read attachment: %1").arg(path);
            return false;
        }
        const QFileInfo info(path);
        QJsonObject attachment;
        attachment.insert(QStringLiteral("@odata.type"), QStringLiteral("#microsoft.graph.fileAttachment"));
        attachment.insert(QStringLiteral("name"), info.fileName());
        attachment.insert(QStringLiteral("contentType"), mimeDatabase.mimeTypeForFile(info).name());
        attachment.insert(QStringLiteral("contentBytes"), QString::fromLatin1(file.readAll().toBase64()));
        attachments.append(attachment);
    }
    message.insert(QStringLiteral("attachments"), attachments);
    QJsonObject payload;
    payload.insert(QStringLiteral("message"), message);
    payload.insert(QStringLiteral("saveToSentItems"), true);
    const QUrl url(QStringLiteral("https://graph.microsoft.com/v1.0/me/sendMail"));
    return postJson(url, account.accessToken().toUtf8(), payload, 202, error);
}

}

MailService::MailService(AccountService *accountService, QObject *parent)
    : QObject(parent)
    , m_composer(new EmailComposerBridge(this))
    , m_accountService(accountService)
{
}

QVector<MailItem> MailService::getMails(const QString &folderId)
{
    return MailItemDao::findByFolderId(folderId.toInt());
}

void MailService::sendMail(const MailItem &mail, const QString &accountId)
{
    sendMail(mail, accountId, {});
}

void MailService::sendMail(const MailItem &mail, const QString &accountId,
                           const QStringList &attachmentPaths)
{
    if (!m_accountService) {
        emit mailSendFailed(accountId, tr("AccountService not available"));
        return;
    }
    Account *account = m_accountService->findAccountById(accountId.toLongLong());
    if (!account) {
        emit mailSendFailed(accountId, tr("Account not found"));
        return;
    }
    const Account accountCopy = *account;
    delete account;
    QFuture<bool> future = QtConcurrent::run([mail, accountCopy, attachmentPaths]() {
        QString error;
        bool ok = false;
        if (accountCopy.type() == AccountType::Gmail)
            ok = sendGmailApi(mail, accountCopy, attachmentPaths, error);
        else if (accountCopy.type() == AccountType::Outlook)
            ok = sendOutlookApi(mail, accountCopy, attachmentPaths, error);
        else
            ok = sendSmtp(mail, accountCopy, attachmentPaths, error);
        if (!ok) qWarning() << "Send failed:" << error;
        return ok;
    });
    auto *watcher = new QFutureWatcher<bool>(this);
    connect(watcher, &QFutureWatcher<bool>::finished, this, [this, watcher, accountId]() {
        const bool ok = watcher->result();
        watcher->deleteLater();
        if (ok) emit mailSent(accountId);
        else emit mailSendFailed(accountId, tr("Mail server rejected the message"));
    });
    watcher->setFuture(future);
}

void MailService::fetchMails(const QString &accountId, const QString &folderId)
{
    // Determine provider type from account
    if (!m_accountService) {
        emit mailFetchError(accountId, folderId, tr("AccountService not available"));
        return;
    }
    Account *acc = m_accountService->findAccountById(accountId.toLongLong());
    if (!acc) {
        emit mailFetchError(accountId, folderId, tr("Account not found"));
        return;
    }
    QString providerType = QStringLiteral("imap"); // fallback
    if (acc) {
        switch (acc->type()) {
        case AccountType::IMAP:
            providerType = QStringLiteral("imap");
            break;
        case AccountType::POP3:
            providerType = QStringLiteral("pop3");
            break;
        case AccountType::Gmail:
            providerType = QStringLiteral("gmail");
            break;
        case AccountType::Outlook:
            providerType = QStringLiteral("outlook");
            break;
        default:
            providerType = QStringLiteral("imap");
            break;
        }
    }

    Synchronizer *sync = SynchronizerProvider::instance().createSynchronizer(accountId, providerType);
    if (!sync) {
        emit mailFetchError(accountId, folderId, tr("Failed to create synchronizer"));
        return;
    }


    // Initialize synchronizer with account details before using it
    if (!sync->initialize(*acc)) {
        delete acc;
        emit mailFetchError(accountId, folderId, tr("Failed to initialize synchronizer"));
        SynchronizerProvider::instance().unregisterSynchronizer(accountId);
        return;
    }
    delete acc;

    // Connect progress signals (if they exist) using string-based syntax for compatibility
    QObject::connect(sync, SIGNAL(progressChanged(int)), this, SIGNAL(progressChanged(int)), Qt::QueuedConnection);
    QObject::connect(sync, SIGNAL(statusMessage(const QString&)), this, SIGNAL(statusMessage(const QString&)), Qt::QueuedConnection);

    QFuture<QVector<MailItem>> future = QtConcurrent::run([this, sync, folderId]() {
        // SinceUid = 0 for full sync; could be stored per folder but omitted for simplicity
        return sync->fetchMailItems(folderId, 0);
    });

    QFutureWatcher<QVector<MailItem>> *watcher = new QFutureWatcher<QVector<MailItem>>(this);
    QObject::connect(watcher, &QFutureWatcher<QVector<MailItem>>::finished, this, [this, sync, watcher, accountId, folderId]() {
        // Disconnect progress signals
        QObject::disconnect(sync, SIGNAL(progressChanged(int)), this, SIGNAL(progressChanged(int)));
        QObject::disconnect(sync, SIGNAL(statusMessage(const QString&)), this, SIGNAL(statusMessage(const QString&)));
        QVector<MailItem> items = watcher->result();
        QVector<MailItem> persisted;
        for (MailItem &item : items) {
            if (persistFetchedItem(item, accountId, folderId)) persisted.append(item);
        }
        watcher->deleteLater();
        emit mailFetched(accountId, folderId, persisted);
    });
    watcher->setFuture(future);
}

bool MailService::persistFetchedItem(MailItem &item, const QString &accountId,
                                      const QString &folderId)
{
    if (item.rawMime().isEmpty()) return MailItemDao::upsert(item);

    MimeStorageService storage;
    ParsedMimeMessage parsed;
    QVector<QString> paths;
    if (!storage.parseMessage(item.rawMime(), parsed)
        || !storage.storeMessage(accountId, folderId, item, item.rawMime(), &paths)) {
        qWarning() << "Failed to persist MIME message" << item.messageId();
        return false;
    }
    if (!MailItemDao::upsert(item)) {
        storage.deleteEmlFile(item.fileId());
        return false;
    }

    QVector<StoredAttachmentRecord> records;
    for (int i = 0; i < parsed.attachments.size(); ++i) {
        const ParsedMimeAttachment &source = parsed.attachments.at(i);
        StoredAttachmentRecord record;
        record.fileName = source.fileName;
        record.mimeType = source.mimeType;
        record.contentId = source.contentId;
        record.size = source.data.size();
        if (i < paths.size()) record.storedPath = paths.at(i);
        records.append(record);
    }
    if (!MailItemDao::replaceAttachments(item.id(), records)) {
        storage.deleteEmlFile(item.fileId());
        return false;
    }
    return true;
}

void MailService::moveMail(const QString &mailItemId, const QString &targetFolderId)
{
    bool ok;
    qint64 id = mailItemId.toLongLong(&ok);
    if (!ok) return;
    std::optional<MailItem> opt = MailItemDao::findById(id);
    if (!opt) return;
    MailItem item = *opt;
    item.setFolderId(targetFolderId.toInt());
    if (MailItemDao::update(item))
        emit mailMoved(mailItemId);
}

void MailService::deleteMail(const QString &mailItemId)
{
    bool ok;
    qint64 id = mailItemId.toLongLong(&ok);
    if (!ok) return;
    const std::optional<MailItem> item = MailItemDao::findById(id);
    if (MailItemDao::remove(id)) {
        if (item && !item->fileId().isEmpty()) {
            MimeStorageService storage;
            storage.deleteEmlFile(item->fileId());
        }
        emit mailDeleted(mailItemId);
    }
}

void MailService::markAsRead(const QString &mailItemId, bool read)
{
    bool ok;
    qint64 id = mailItemId.toLongLong(&ok);
    if (!ok) return;
    std::optional<MailItem> opt = MailItemDao::findById(id);
    if (!opt) return;
    MailItem item = *opt;
    item.setRead(read);
    if (MailItemDao::update(item))
        emit mailReadStateChanged(mailItemId, read);
}

