/*
 *  Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 2 or (at your option)
 *  version 3 of the License.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "OpData01.h"
#include "OpVaultReader.h"

#include "core/Group.h"
#include "core/Tools.h"

#include <QDebug>
#include <QJsonDocument>
#include <QJsonObject>
#include <QUuid>

/*!
 * This will \c qCritical() if unable to open the file for reading.
 * @param file the \c .attachment file to decode
 * @return \c nullptr if unable to take action, else a pair of metadata and the actual attachment bits
 * \sa https://support.1password.com/opvault-design/#attachments
 */
bool OpVaultReader::readAttachment(const QString& filePath,
                                   const QByteArray& itemKey,
                                   const QByteArray& itemHmacKey,
                                   QJsonObject& metadata,
                                   QByteArray& payload)
{
    QFile file(filePath);
    if (!file.open(QIODevice::ReadOnly)) {
        qCritical() << QString("Unable to open \"%s\" for reading").arg(file.fileName());
        return false;
    }

    QString magic("OPCLDAT");
    QByteArray magicBytes = file.read(7);
    if (magicBytes != magic.toUtf8()) {
        qCritical() << "Expected OPCLDAT but found <<" << magicBytes.toHex() << ">>";
        return false;
    }

    QByteArray version = file.read(1);
    if (version[0] != '\001' && version[0] != '\002') {
        qCritical() << "Unexpected version number; wanted 1 or 2, got <<" << version << ">>";
        return false;
    }
    const QByteArray& metadataLenBytes = file.read(2);
    if (metadataLenBytes.size() != 2) {
        qCritical() << "Unable to read all metadata length bytes; wanted 2 bytes, got " << metadataLenBytes.size()
                    << ": <<" << metadataLenBytes.toHex() << ">>";
        return false;
    }
    const auto b0 = static_cast<unsigned char>(metadataLenBytes[0]);
    const auto b1 = static_cast<unsigned char>(metadataLenBytes[1]);
    int metadataLen = ((0xFF & b1) << 8) | (0xFF & b0);

    // no really: it's labeled "Junk" in the spec
    int junkBytesRead = file.read(2).size();
    if (junkBytesRead != 2) {
        qCritical() << "Unable to read all \"junk\" bytes; wanted 2 bytes, got " << junkBytesRead;
        return false;
    }

    const QByteArray& iconLenBytes = file.read(4);
    if (iconLenBytes.size() != 4) {
        qCritical() << "Unable to read all \"iconLen\" bytes; wanted 4 bytes, got " << iconLenBytes.size();
        return false;
    }

    int iconLen = 0;
    for (int i = 0, len = iconLenBytes.size(); i < len; ++i) {
        char ch = iconLenBytes[i];
        auto b = static_cast<unsigned char>(ch & 0xFF);
        iconLen = (b << (i * 8)) | iconLen;
    }

    QByteArray metadataJsonBytes = file.read(metadataLen);
    if (metadataJsonBytes.size() != metadataLen) {
        qCritical() << "Unable to read all bytes of metadata JSON; wanted " << metadataLen << "but read "
                    << metadataJsonBytes.size();
        return false;
    }
    QByteArray iconBytes = file.read(iconLen);
    if (iconBytes.size() != iconLen) {
        qCritical() << "Unable to read all icon bytes; wanted " << iconLen << "but read " << iconBytes.size();
        // apologies for the icon being fatal, but it would take some gear-turning
        // to re-sync where in the attach header we are
        return false;
    }

    // we don't actually _care_ what the icon bytes are,
    // but they damn well better be valid opdata01 and pass its HMAC
    OpData01 icon01;
    if (!icon01.decode(iconBytes, itemKey, itemHmacKey)) {
        qCritical() << "Unable to decipher attachment icon in " << filePath << ": " << icon01.errorString();
        return false;
    }

    QJsonParseError jsError;
    QJsonDocument jDoc = QJsonDocument::fromJson(metadataJsonBytes, &jsError);
    if (jsError.error != QJsonParseError::ParseError::NoError) {
        qCritical() << "Found invalid attachment metadata JSON at offset " << jsError.offset << ": error("
                    << jsError.error << "): " << jsError.errorString() << "\n<<" << metadataJsonBytes << ">>";
        return false;
    }
    if (!jDoc.isObject()) {
        qCritical() << "Expected " << metadataJsonBytes << "to be a JSON Object";
        return false;
    }

    metadata = jDoc.object();
    if (metadata.contains("trashed") && metadata["trashed"].toBool()) {
        return false;
    }

    if (!metadata.contains("contentsSize")) {
        qWarning() << "Expected attachment metadata to contain \"contentsSize\" but nope: " << metadata;
        return false;
    } else if (!metadata["contentsSize"].isDouble()) {
        qWarning() << "Expected attachment metadata to contain numeric \"contentsSize\" but nope: " << metadata;
        return false;
    }
    int bytesLen = metadata["contentsSize"].toInt();
    const QByteArray encData = file.readAll();
    if (encData.size() < bytesLen) {
        qCritical() << "Unable to read all of the attachment payload; wanted " << bytesLen << "but got"
                    << encData.size();
        return false;
    }

    OpData01 att01;
    if (!att01.decode(encData, itemKey, itemHmacKey)) {
        qCritical() << "Unable to decipher attachment payload: " << att01.errorString();
        return false;
    }

    payload = att01.getClearText();
    return true;
}

/*!
 * \sa https://support.1password.com/opvault-design/#attachments
 */
void OpVaultReader::fillAttachments(Entry* entry,
                                    const QDir& attachmentDir,
                                    const QByteArray& entryKey,
                                    const QByteArray& entryHmacKey)
{
    /*!
     * Attachment files are named with the UUID of the item that they are attached to followed by an underscore
     * and then followed by the UUID of the attachment itself. The file is then given the extension .attachment.
     */
    auto fileFilter = QString("%1_*.attachment").arg(entry->uuidToHex().toUpper());
    const auto& attachInfoList = attachmentDir.entryInfoList(QStringList() << fileFilter, QDir::Files);
    int attachmentCount = attachInfoList.size();
    if (attachmentCount == 0) {
        return;
    }

    for (const auto& info : attachInfoList) {
        if (!info.isReadable()) {
            qCritical() << QString("Attachment file \"%1\" is not readable").arg(info.absoluteFilePath());
            continue;
        }
        fillAttachment(entry, info, entryKey, entryHmacKey);
    }
}

void OpVaultReader::fillAttachment(Entry* entry,
                                   const QFileInfo& info,
                                   const QByteArray& entryKey,
                                   const QByteArray& entryHmacKey)
{
    QJsonObject attachMetadata;
    QByteArray attachPayload;
    if (!readAttachment(info.absoluteFilePath(), entryKey, entryHmacKey, attachMetadata, attachPayload)) {
        return;
    }

    if (!attachMetadata.contains("overview")) {
        qWarning() << "Expected \"overview\" in attachment metadata";
        return;
    }

    const QString& overB64 = attachMetadata["overview"].toString();
    OpData01 over01;

    if (over01.decodeBase64(overB64, m_overviewKey, m_overviewHmacKey)) {
        QByteArray overviewJson = over01.getClearText();
        QJsonDocument overDoc = QJsonDocument::fromJson(overviewJson);
        if (overDoc.isObject()) {
            QJsonObject overObj = overDoc.object();
            attachMetadata.remove("overview");
            for (QString& key : overObj.keys()) {
                const QJsonValueRef& value = overObj[key];
                QString insertAs = key;
                for (int aa = 0; attachMetadata.contains(insertAs) && aa < 5; ++aa) {
                    insertAs = QString("%1_%2").arg(key, aa);
                }
                attachMetadata[insertAs] = value;
            }
        } else {
            qWarning() << "Expected JSON Object in \"overview\" but nope: " << overDoc;
        }
    } else {
        qCritical()
            << QString("Unable to decode attach.overview for \"%1\": %2").arg(info.fileName(), over01.errorString());
    }

    QByteArray payload;
    payload.append(QString("attachment file is actually %1 bytes\n").arg(info.size()).toUtf8());
    for (QString& key : attachMetadata.keys()) {
        const QJsonValueRef& value = attachMetadata[key];
        QByteArray valueBytes;
        if (value.isString()) {
            valueBytes = value.toString().toUtf8();
        } else if (value.isDouble()) {
            valueBytes = QString("%1").arg(value.toInt()).toUtf8();
        } else if (value.isBool()) {
            valueBytes = value.toBool() ? "true" : "false";
        } else {
            valueBytes = QString("Unexpected metadata type in attachment: %1").arg(value.type()).toUtf8();
        }
        payload.append(key.toUtf8()).append(":=").append(valueBytes).append("\n");
    }

    QString attachKey = info.baseName();
    if (attachMetadata.contains("filename")) {
        QJsonValueRef attFilename = attachMetadata["filename"];
        if (attFilename.isString()) {
            attachKey = attFilename.toString();
        } else {
            qWarning() << QString("Unexpected type of attachment \"filename\": %1").arg(attFilename.type());
        }
    }

    entry->attachments()->set(attachKey, attachPayload);
}
