#include "mimestorage.h"
#include <QStandardPaths>
#include <QDebug>
#include <QUuid>
#include <QCryptographicHash>
#include <QRegularExpression>
#include <QTextStream>
#include <QFileInfo>
#include <QUrl>
#include <QDateTime>
#include <QSaveFile>
#include <QMap>

MimeStorageService::MimeStorageService(QObject *parent)
    : QObject(parent)
{
}

QString MimeStorageService::storagePath() const
{
    QString path = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/mails";
    return ensureDirectory(path);
}

QString MimeStorageService::ensureDirectory(const QString &path) const
{
    QDir dir(path);
    if (!dir.exists()) {
        dir.mkpath(".");
    }
    return dir.absolutePath();
}

QString MimeStorageService::safeComponent(const QString &value) const
{
    QString result = value;
    result.replace(QRegularExpression(QStringLiteral("[^A-Za-z0-9._-]")), QStringLiteral("_"));
    if (result.isEmpty() || result == QStringLiteral(".") || result == QStringLiteral(".."))
        result = QStringLiteral("unknown");
    return result.left(180);
}

QString MimeStorageService::saveEmlFile(const QString &accountId, const QString &folderId, const MailItem &mail)
{
    QByteArray raw;
    raw.append("From: ").append(mail.sender().toUtf8()).append("\r\n");
    raw.append("To: ").append(mail.recipient().toUtf8()).append("\r\n");
    if (!mail.cc().isEmpty()) raw.append("Cc: ").append(mail.cc().toUtf8()).append("\r\n");
    if (!mail.bcc().isEmpty()) raw.append("Bcc: ").append(mail.bcc().toUtf8()).append("\r\n");
    raw.append("Subject: ").append(mail.subject().toUtf8()).append("\r\n");
    raw.append("Date: ").append(mail.date().toString(Qt::RFC2822Date).toUtf8()).append("\r\n");
    raw.append("MIME-Version: 1.0\r\nContent-Type: text/html; charset=UTF-8\r\n\r\n");
    raw.append(mail.bodyHtml().toUtf8()).append("\r\n");
    return saveRawEmlFile(accountId, folderId, raw, mail.messageId());
}

QString MimeStorageService::saveRawEmlFile(const QString &accountId, const QString &folderId,
                                           const QByteArray &rawMime, const QString &stableId)
{
    if (rawMime.isEmpty()) return QString();

    const QString accountPart = safeComponent(accountId);
    const QString folderPart = safeComponent(folderId);
    const QByteArray digest = QCryptographicHash::hash(rawMime, QCryptographicHash::Sha256).toHex();
    const QString fileName = safeComponent(stableId.isEmpty()
        ? QString::fromLatin1(digest)
        : stableId) + QStringLiteral("_") + QString::fromLatin1(digest.left(12)) + QStringLiteral(".eml");
    const QString relative = accountPart + "/" + folderPart + "/" + fileName;
    const QString path = QDir(storagePath()).filePath(relative);
    ensureDirectory(QFileInfo(path).absolutePath());

    QSaveFile file(path);
    if (!file.open(QIODevice::WriteOnly) || file.write(rawMime) != rawMime.size() || !file.commit()) {
        qWarning() << "[MimeStorage] Failed to save raw MIME:" << path << file.errorString();
        return QString();
    }
    return relative.left(relative.size() - 4); // fileId excludes .eml
}

bool MimeStorageService::storeMessage(const QString &accountId, const QString &folderId,
                                      MailItem &mail, const QByteArray &rawMime,
                                      QVector<QString> *storedAttachmentPaths)
{
    ParsedMimeMessage parsed;
    if (!parseMessage(rawMime, parsed)) return false;

    if (!parsed.messageId.isEmpty()) mail.setMessageId(parsed.messageId);
    if (!parsed.subject.isEmpty()) mail.setSubject(parsed.subject);
    if (!parsed.from.isEmpty()) mail.setSender(parsed.from);
    if (!parsed.to.isEmpty()) { mail.setTo(parsed.to); mail.setRecipient(parsed.to); }
    if (!parsed.cc.isEmpty()) mail.setCc(parsed.cc);
    if (!parsed.bcc.isEmpty()) mail.setBcc(parsed.bcc);
    if (parsed.date.isValid()) mail.setDate(parsed.date);
    mail.setBodyHtml(parsed.bodyHtml);
    mail.setSize(rawMime.size());
    mail.setRawMime(rawMime);

    const QString fileId = saveRawEmlFile(accountId, folderId, rawMime, mail.messageId());
    if (fileId.isEmpty()) return false;
    mail.setFileId(fileId);

    QVector<QString> names;
    QVector<QString> savedPaths;
    for (const ParsedMimeAttachment &attachment : parsed.attachments) {
        const QString path = saveAttachment(fileId, attachment.fileName, attachment.data);
        if (path.isEmpty()) {
            for (const QString &savedPath : savedPaths) QFile::remove(savedPath);
            deleteEmlFile(fileId);
            return false;
        }
        savedPaths.append(path);
        names.append(attachment.fileName);
        if (storedAttachmentPaths) storedAttachmentPaths->append(path);
    }
    mail.setAttachments(names);
    return true;
}

QString MimeStorageService::getEmlFilePath(const QString &fileId) const
{
    QString relative = fileId;
    if (relative.endsWith(QStringLiteral(".eml"))) relative.chop(4);
    const QString base = QDir::cleanPath(storagePath());
    const QString path = QDir::cleanPath(QDir(base).filePath(relative + QStringLiteral(".eml")));
    if (!path.startsWith(base + QDir::separator()) || !QFileInfo::exists(path)) return QString();
    return path;
}

QByteArray MimeStorageService::readEmlFile(const QString &fileId) const
{
    QString path = getEmlFilePath(fileId);
    if (path.isEmpty()) return {};

    QFile file(path);
    if (file.open(QIODevice::ReadOnly)) {
        return file.readAll();
    }
    return {};
}

bool MimeStorageService::deleteEmlFile(const QString &fileId)
{
    QString path = getEmlFilePath(fileId);
    if (path.isEmpty()) return false;
    const bool removed = QFile::remove(path);
    QDir attachments(QFileInfo(path).absolutePath() + QStringLiteral("/attachments"));
    if (attachments.exists()) attachments.removeRecursively();
    return removed;
}

QStringList MimeStorageService::listAttachments(const QString &mailItemId) const
{
    const QString emlPath = getEmlFilePath(mailItemId);
    if (emlPath.isEmpty()) return {};
    QDir dir(QFileInfo(emlPath).absolutePath() + QStringLiteral("/attachments"));
    return dir.entryList(QDir::Files, QDir::Name);
}

QString MimeStorageService::saveAttachment(const QString &fileId, const QString &fileName, const QByteArray &data)
{
    const QString emlPath = getEmlFilePath(fileId);
    if (emlPath.isEmpty()) return QString();
    const QString dir = ensureDirectory(QFileInfo(emlPath).absolutePath() + QStringLiteral("/attachments"));
    const QString originalName = QFileInfo(fileName).fileName();
    const QString safeName = safeComponent(originalName.isEmpty()
        ? QStringLiteral("attachment") : originalName);
    QString path = QDir(dir).filePath(safeName);
    if (QFileInfo::exists(path)) {
        const QFileInfo info(path);
        const QString suffix = info.completeSuffix().isEmpty()
            ? QString() : QStringLiteral(".") + info.completeSuffix();
        const QString stem = info.completeBaseName();
        path = QDir(dir).filePath(stem + QStringLiteral("_")
            + QString::fromLatin1(QCryptographicHash::hash(data, QCryptographicHash::Sha256).toHex().left(12))
            + suffix);
    }
    QSaveFile file(path);
    if (!file.open(QIODevice::WriteOnly) || file.write(data) != data.size() || !file.commit()) return QString();
    return path;
}

QString MimeStorageService::decodeHeaderValue(const QString &value) const
{
    QString result = value;
    const QRegularExpression rx(QStringLiteral("=\\?([^?]+)\\?([bBqQ])\\?([^?]+)\\?="));
    QRegularExpressionMatchIterator it = rx.globalMatch(result);
    while (it.hasNext()) {
        const QRegularExpressionMatch match = it.next();
        QByteArray encoded = match.captured(3).toLatin1();
        QByteArray decoded;
        if (match.captured(2).compare(QStringLiteral("b"), Qt::CaseInsensitive) == 0)
            decoded = QByteArray::fromBase64(encoded);
        else {
            encoded.replace('_', ' ');
            QByteArray out;
            for (int i = 0; i < encoded.size(); ++i) {
                if (encoded[i] == '=' && i + 2 < encoded.size()) {
                    bool ok = false;
                    const int value = QByteArray(encoded.mid(i + 1, 2)).toInt(&ok, 16);
                    if (ok) { out.append(char(value)); i += 2; continue; }
                }
                out.append(encoded[i]);
            }
            decoded = out;
        }
        QString decodedText = QString::fromUtf8(decoded);
        if (decodedText.isEmpty() && !decoded.isEmpty()) decodedText = QString::fromLatin1(decoded);
        result.replace(match.captured(0), decodedText);
    }
    return result.trimmed();
}

QByteArray MimeStorageService::decodeTransfer(const QByteArray &data, const QString &encoding) const
{
    const QString normalized = encoding.trimmed().toLower();
    if (normalized == QStringLiteral("base64")) return QByteArray::fromBase64(data);
    if (normalized != QStringLiteral("quoted-printable")) return data;

    QByteArray result;
    for (int i = 0; i < data.size(); ++i) {
        if (data[i] == '=' && i + 1 < data.size()) {
            if (data[i + 1] == '\r' && i + 2 < data.size() && data[i + 2] == '\n') { i += 2; continue; }
            if (data[i + 1] == '\n') { ++i; continue; }
            if (i + 2 < data.size()) {
                bool ok = false;
                const int value = QByteArray(data.mid(i + 1, 2)).toInt(&ok, 16);
                if (ok) { result.append(char(value)); i += 2; continue; }
            }
        }
        result.append(data[i]);
    }
    return result;
}

void MimeStorageService::parseMimePart(const QByteArray &part, ParsedMimeMessage &message,
                                       QString *plainText, QString *htmlText) const
{
    const int crlf = part.indexOf("\r\n\r\n");
    const int lf = part.indexOf("\n\n");
    int split = crlf >= 0 ? crlf : lf;
    const int separatorSize = crlf >= 0 ? 4 : 2;
    const QByteArray headerBytes = split >= 0 ? part.left(split) : part;
    const QByteArray body = split >= 0 ? part.mid(split + separatorSize) : QByteArray();

    QMap<QString, QString> headers;
    QString current;
    for (QString line : QString::fromLatin1(headerBytes).split(QRegularExpression("\\r?\\n"))) {
        if ((line.startsWith(' ') || line.startsWith('\t')) && !current.isEmpty()) {
            headers[current] += QStringLiteral(" ") + line.trimmed();
            continue;
        }
        const int colon = line.indexOf(':');
        if (colon < 0) continue;
        current = line.left(colon).trimmed().toLower();
        headers[current] = line.mid(colon + 1).trimmed();
    }
    auto header = [&](const QString &name) { return headers.value(name.toLower()); };
    const QString contentType = header(QStringLiteral("content-type"));
    const QString disposition = header(QStringLiteral("content-disposition"));
    const QString transfer = header(QStringLiteral("content-transfer-encoding"));

    QRegularExpression boundaryRx(QStringLiteral("(?:^|;)\\s*boundary\\s*=\\s*(?:\\\"([^\\\"]+)\\\"|([^;\\s]+))"), QRegularExpression::CaseInsensitiveOption);
    const QRegularExpressionMatch boundaryMatch = boundaryRx.match(contentType);
    if (contentType.startsWith(QStringLiteral("multipart/"), Qt::CaseInsensitive) && boundaryMatch.hasMatch()) {
        const QString boundary = boundaryMatch.captured(1).isEmpty() ? boundaryMatch.captured(2) : boundaryMatch.captured(1);
        const QByteArray marker = QByteArrayLiteral("--") + boundary.toUtf8();
        int pos = 0;
        while ((pos = body.indexOf(marker, pos)) >= 0) {
            int start = pos + marker.size();
            if (body.mid(start, 2) == QByteArrayLiteral("--")) break;
            if (body.mid(start, 2) == QByteArrayLiteral("\r\n")) start += 2;
            else if (body.mid(start, 1) == QByteArrayLiteral("\n")) ++start;
            int end = body.indexOf(marker, start);
            if (end < 0) end = body.size();
            QByteArray child = body.mid(start, end - start);
            while (child.endsWith("\r\n") || child.endsWith('\n')) child.chop(child.endsWith("\r\n") ? 2 : 1);
            parseMimePart(child, message, plainText, htmlText);
            pos = end;
        }
        return;
    }

    const QByteArray decoded = decodeTransfer(body, transfer);
    QRegularExpression parameterRx(QStringLiteral("(?:^|;)\\s*(?:filename|name)(?:\\*\\d+)?\\*?\\s*=\\s*(?:\\\"([^\\\"]+)\\\"|([^;\\s]+))"), QRegularExpression::CaseInsensitiveOption);
    const QRegularExpressionMatch dispositionMatch = parameterRx.match(disposition);
    const QRegularExpressionMatch typeMatch = parameterRx.match(contentType);
    QString fileName = dispositionMatch.hasMatch()
        ? (dispositionMatch.captured(1).isEmpty() ? dispositionMatch.captured(2) : dispositionMatch.captured(1))
        : (typeMatch.hasMatch() ? (typeMatch.captured(1).isEmpty() ? typeMatch.captured(2) : typeMatch.captured(1)) : QString());
    const int encodedPrefix = fileName.indexOf(QStringLiteral("''"));
    if (encodedPrefix >= 0) fileName = fileName.mid(encodedPrefix + 2);
    fileName = QUrl::fromPercentEncoding(fileName.toUtf8());
    fileName = decodeHeaderValue(fileName);
    const QString mimeType = contentType.section(';', 0, 0).trimmed().toLower();
    const bool isAttachment = disposition.startsWith(QStringLiteral("attachment"), Qt::CaseInsensitive)
        || !fileName.isEmpty() || (!mimeType.startsWith(QStringLiteral("text/")) && !mimeType.isEmpty());

    if (isAttachment) {
        ParsedMimeAttachment attachment;
        attachment.fileName = fileName.isEmpty() ? QStringLiteral("attachment-%1").arg(message.attachments.size() + 1) : fileName;
        attachment.mimeType = mimeType;
        attachment.contentId = header(QStringLiteral("content-id")).trimmed();
        attachment.contentId.remove('<');
        attachment.contentId.remove('>');
        attachment.data = decoded;
        message.attachments.append(attachment);
        return;
    }

    const QString text = QString::fromUtf8(decoded);
    if (mimeType == QStringLiteral("text/html")) {
        if (htmlText && htmlText->isEmpty()) *htmlText = text;
    } else if (mimeType == QStringLiteral("text/plain") || mimeType.isEmpty()) {
        if (plainText && plainText->isEmpty()) *plainText = text;
    }
}

bool MimeStorageService::parseMessage(const QByteArray &rawMime, ParsedMimeMessage &message) const
{
    if (rawMime.isEmpty()) return false;
    message = ParsedMimeMessage();
    QString plainText;
    QString htmlText;
    parseMimePart(rawMime, message, &plainText, &htmlText);

    auto parseHeaders = [&](const QByteArray &data) {
        const int split = data.indexOf("\r\n\r\n") >= 0 ? data.indexOf("\r\n\r\n") : data.indexOf("\n\n");
        const QByteArray headerData = split >= 0 ? data.left(split) : data;
        QMap<QString, QString> headers;
        QString current;
        for (QString line : QString::fromLatin1(headerData).split(QRegularExpression("\\r?\\n"))) {
            if ((line.startsWith(' ') || line.startsWith('\t')) && !current.isEmpty()) { headers[current] += " " + line.trimmed(); continue; }
            const int colon = line.indexOf(':');
            if (colon < 0) continue;
            current = line.left(colon).trimmed().toLower();
            headers[current] = line.mid(colon + 1).trimmed();
        }
        return headers;
    };
    const QMap<QString, QString> headers = parseHeaders(rawMime);
    message.messageId = decodeHeaderValue(headers.value(QStringLiteral("message-id"))).remove('<').remove('>');
    message.subject = decodeHeaderValue(headers.value(QStringLiteral("subject")));
    message.from = decodeHeaderValue(headers.value(QStringLiteral("from")));
    message.to = decodeHeaderValue(headers.value(QStringLiteral("to")));
    message.cc = decodeHeaderValue(headers.value(QStringLiteral("cc")));
    message.bcc = decodeHeaderValue(headers.value(QStringLiteral("bcc")));
    message.date = QDateTime::fromString(headers.value(QStringLiteral("date")), Qt::RFC2822Date);
    if (!message.date.isValid()) message.date = QDateTime::currentDateTimeUtc();
    if (!htmlText.isEmpty()) message.bodyHtml = htmlText;
    else {
        QString escaped = plainText.toHtmlEscaped();
        escaped.replace(QStringLiteral("\r\n"), QStringLiteral("<br>"));
        escaped.replace(QStringLiteral("\n"), QStringLiteral("<br>"));
        message.bodyHtml = escaped;
    }
    return true;
}
