/*
    SPDX-FileCopyrightText: 2020 Volker Krause <vkrause@kde.org>

    SPDX-License-Identifier: LGPL-2.0-or-later
*/

#include "importexport.h"

#include <itinerary_version_detailed.h>

#include "bundle-constants.h"
#include "documentmanager.h"
#include "favoritelocationmodel.h"
#include "healthcertificatemanager.h"
#include "livedata.h"
#include "livedatamanager.h"
#include "logging.h"
#include "passmanager.h"
#include "pkpassmanager.h"
#include "reservationmanager.h"
#include "transfer.h"
#include "transfermanager.h"
#include "tripgroup.h"
#include "tripgroupmanager.h"

#include <kitinerary_version.h>
#include <KItinerary/File>
#include <KItinerary/JsonLdDocument>

#include <KPublicTransport/Location>
#include <KPublicTransport/LocationHistoryModel>

#include <QCoreApplication>
#include <QDirIterator>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QSettings>
#include <QStandardPaths>
#include <QSysInfo>
#include <QTemporaryFile>

using namespace Qt::Literals::StringLiterals;

static qsizetype copySettings(const QSettings *from, QSettings *to)
{
    const auto keys = from->allKeys();
    for (const auto &key : keys) {
        to->setValue(key, from->value(key));
    }
    return keys.size();
}

Exporter::Exporter(KItinerary::File *file)
    : m_file(file)
{
    QJsonObject versionData{
        {"formatVersion"_L1, BUNDLE_FORMAT_VERSION},
        {"generator"_L1, QCoreApplication::applicationName()},
        {"generatorVersion"_L1, QLatin1StringView(ITINERARY_DETAILED_VERSION_STRING)},
        {"generatorPlatform"_L1, QSysInfo::prettyProductName()}
    };
    m_file->addCustomData(BUNDLE_VERSION_DOMAIN, "version"_L1, QJsonDocument(versionData).toJson(QJsonDocument::Compact));
}

void Exporter::exportReservations(const ReservationManager *resMgr)
{
    for (const auto &batchId : resMgr->batches()) {
        exportReservationBatch(resMgr, batchId);
    }
}

void Exporter::exportReservationBatch(const ReservationManager *resMgr, const QString &batchId)
{
    const auto resIds = resMgr->reservationsForBatch(batchId);
    for (const auto &resId : resIds) {
        m_file->addReservation(resId, resMgr->reservation(resId));
    }
}

void Exporter::exportPasses(const PkPassManager *pkPassMgr)
{
    const auto passIds = pkPassMgr->passes();
    for (const auto &passId : passIds) {
        exportPkPass(pkPassMgr, passId);
    }
}

void Exporter::exportPkPass(const PkPassManager *pkPassMgr, const QString &passId)
{
    if (passId.isEmpty()) {
        return;
    }
    m_file->addPass(passId, pkPassMgr->rawData(passId));
}

void Exporter::exportDocuments(const DocumentManager *docMgr)
{
    const auto docIds = docMgr->documents();
    for (const auto &docId : docIds) {
        exportDocument(docMgr, docId);
    }
}

void Exporter::exportDocument(const DocumentManager *docMgr, const QString &docId)
{
    const auto fileName = docMgr->documentFilePath(docId);
    QFile file(fileName);
    if (!file.open(QFile::ReadOnly)) {
        qCWarning(Log) << "failed to open" << fileName << "for exporting" << file.errorString();
        return;
    }
    m_file->addDocument(docId, docMgr->documentInfo(docId), file.readAll());
}

void Exporter::exportTransfers(const ReservationManager *resMgr, const TransferManager *transferMgr)
{
    std::vector<FavoriteLocation> favLocs;
    for (const auto &batchId : resMgr->batches()) {
        exportTransfersForBatch(batchId, transferMgr, favLocs);
    }
}

static void addFavoriteLocation(const KPublicTransport::Location &loc, const QString &name, std::vector<FavoriteLocation> &favoriteLocations)
{
    FavoriteLocation fav;
    fav.setName(name);
    fav.setLatitude(loc.latitude());
    fav.setLongitude(loc.longitude());
    if (!fav.isValid()) {
        return;
    }

    if (std::find(favoriteLocations.begin(), favoriteLocations.end(), fav) == favoriteLocations.end()) {
        favoriteLocations.push_back(std::move(fav));
    }
}

void Exporter::exportTransfersForBatch(const QString &batchId, const TransferManager *transferMgr, std::vector<FavoriteLocation> &favoriteLocations)
{
    if (const auto t = transferMgr->transfer(batchId, Transfer::Before); t.state() != Transfer::UndefinedState) {
        m_file->addCustomData(BUNDLE_TRANSFER_DOMAIN, t.identifier(), QJsonDocument(Transfer::toJson(t)).toJson());
        if (t.floatingLocationType() == Transfer::FavoriteLocation) {
            addFavoriteLocation(t.from(), t.fromName(), favoriteLocations);
        }
    }
    if (const auto t = transferMgr->transfer(batchId, Transfer::After); t.state() != Transfer::UndefinedState) {
        m_file->addCustomData(BUNDLE_TRANSFER_DOMAIN, t.identifier(), QJsonDocument(Transfer::toJson(t)).toJson());
        if (t.floatingLocationType() == Transfer::FavoriteLocation) {
            addFavoriteLocation(t.to(), t.toName(), favoriteLocations);
        }
    }
}

void Exporter::exportTripGroups(const TripGroupManager *tripGroupMgr)
{
    const auto tgIds = tripGroupMgr->tripGroups();
    for (const auto &tgId : tgIds) {
        exportTripGroup(tgId, tripGroupMgr->tripGroup(tgId));
    }
}

void Exporter::exportTripGroup(const QString &tripGroupId, const TripGroup &tg)
{
    m_file->addCustomData(BUNDLE_TRIPGROUP_DOMAIN, tripGroupId, QJsonDocument(TripGroup::toJson(tg)).toJson());
}

void Exporter::exportFavoriteLocations(const std::vector<FavoriteLocation> &favLocs)
{
    if (!favLocs.empty()) {
        m_file->addCustomData(BUNDLE_FAVORITE_LOCATION_DOMAIN,
                              u"locations"_s,
                              QJsonDocument(FavoriteLocation::toJson(favLocs)).toJson());
    }
}

void Exporter::exportPasses(const PassManager *passMgr)
{
    for (int i = 0; i < passMgr->rowCount(); ++i) {
        const auto idx = passMgr->index(i, 0);
        m_file->addCustomData(BUNDLE_PASS_DOMAIN,
                              passMgr->data(idx, PassManager::PassIdRole).toString(),
                              passMgr->data(idx, PassManager::PassDataRole).toByteArray());
    }
}

void Exporter::exportPass(const QString &passId, const PassManager *passMgr)
{
    m_file->addCustomData(BUNDLE_PASS_DOMAIN, passId, passMgr->passData(passId));
}

void Exporter::exportHealthCertificates(const HealthCertificateManager *healthCertMgr)
{
    for (int i = 0; i < healthCertMgr->rowCount(); ++i) {
        const auto idx = healthCertMgr->index(i, 0);
        m_file->addCustomData(BUNDLE_HEALTH_CERTIFICATE_DOMAIN,
                              healthCertMgr->data(idx, HealthCertificateManager::StorageIdRole).toString(),
                              healthCertMgr->data(idx, HealthCertificateManager::RawDataRole).toByteArray());
    }
}

void Exporter::exportLiveData()
{
    for (const auto &id : LiveData::listAll()) {
        exportLiveDataForBatch(id);
    }
}

void Exporter::exportLiveDataForBatch(const QString &batchId)
{
    const auto ld = LiveData::load(batchId);
    if (ld.isEmpty()) {
        return;
    }

    m_file->addCustomData(BUNDLE_LIVE_DATA_DOMAIN, batchId, QJsonDocument(LiveData::toJson(ld)).toJson());
}

void Exporter::exportLocationSearchHistory()
{
    for (QDirIterator it(KPublicTransport::LocationHistoryModel::storagePath(), QDir::Files); it.hasNext();) {
        it.next();
        QFile f(it.filePath());
        if (!f.open(QFile::ReadOnly)) {
            qCWarning(Log) << f.fileName() << f.errorString();
            continue;
        }
        m_file->addCustomData(BUNDLE_LOCATION_HISTORY_DOMAIN, it.fileName(), f.readAll());
    }
}

void Exporter::exportPublicTransportAssets()
{
    const auto assetPath = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + "/org.kde.kpublictransport/assets/"_L1;
    for (QDirIterator it(assetPath, QDir::Files); it.hasNext();) {
        it.next();
        QFile f(it.filePath());
        if (!f.open(QFile::ReadOnly)) {
            qCWarning(Log) << f.fileName() << f.errorString();
            continue;
        }
#if KITINERARY_VERSION >= QT_VERSION_CHECK(6, 5, 41)
        m_file->addCustomData(BUNDLE_PUBLIC_TRANSPORT_ASSET_DOMAIN, it.fileName(), f.readAll(), it.fileInfo().lastModified());
#else
        m_file->addCustomData(BUNDLE_PUBLIC_TRANSPORT_ASSET_DOMAIN, it.fileName(), f.readAll());
#endif
    }
}

void Exporter::exportSettings()
{
    QTemporaryFile tmp;
    if (!tmp.open()) {
        qCWarning(Log) << "Failed to open temporary file:" << tmp.errorString();
        return;
    }

    QSettings settings;
    QSettings backup(tmp.fileName(), QSettings::IniFormat);
    tmp.close();
    copySettings(&settings, &backup);
    backup.sync();

    QFile f(backup.fileName());
    f.open(QFile::ReadOnly);
    m_file->addCustomData(BUNDLE_SETTINGS_DOMAIN, QStringLiteral("settings.ini"), f.readAll());
}

Importer::Importer(const KItinerary::File *file)
    : m_file(file)
{
}

int Importer::formatVersion() const
{
    const auto versionData = QJsonDocument::fromJson(m_file->customData(BUNDLE_VERSION_DOMAIN, "version"_L1)).object();
    return versionData.value("formatVersion"_L1).toInt(0);
}

qsizetype Importer::importReservations(ReservationManager *resMgr)
{
    const auto resIds = m_file->reservations();
    for (const auto &resId : resIds) {
        m_resIdMap.insert(resId, resMgr->addReservation(m_file->reservation(resId), resId));
    }
    return resIds.size();
}

qsizetype Importer::importPasses(PkPassManager *pkPassMgr)
{
    const auto passIds = m_file->passes();
    for (const auto &passId : passIds) {
        pkPassMgr->importPassFromData(m_file->passData(passId));
    }
    return passIds.size();
}

qsizetype Importer::importDocuments(DocumentManager *docMgr)
{
    const auto docIds = m_file->documents();
    for (const auto &docId : docIds) {
        docMgr->addDocument(docId, m_file->documentInfo(docId), m_file->documentData(docId));
    }
    return docIds.size();
}

qsizetype Importer::importTransfers(const ReservationManager *resMgr, TransferManager *transferMgr)
{
    int count = 0;
    const auto resIds = m_file->reservations();
    for (const auto &batchId : resIds) {
        auto t =
            Transfer::fromJson(QJsonDocument::fromJson(m_file->customData(BUNDLE_TRANSFER_DOMAIN, Transfer::identifier(batchId, Transfer::Before))).object());
        transferMgr->importTransfer(t);
        count += t.state() != Transfer::UndefinedState ? 1 : 0;
        t = Transfer::fromJson(QJsonDocument::fromJson(m_file->customData(BUNDLE_TRANSFER_DOMAIN, Transfer::identifier(batchId, Transfer::After))).object());
        transferMgr->importTransfer(t);
        count += t.state() != Transfer::UndefinedState ? 1 : 0;
    }
    return count;
}

qsizetype Importer::importTripGroups(TripGroupManager *tgMgr)
{
    const auto tgIds = m_file->listCustomData(BUNDLE_TRIPGROUP_DOMAIN);
    for (const auto &importId : tgIds) {
        auto importTg = TripGroup::fromJson(QJsonDocument::fromJson(m_file->customData(BUNDLE_TRIPGROUP_DOMAIN, importId)).object());
        QStringList elems;
        elems.reserve(importTg.elements().size());
        for (const auto &resId : importTg.elements()) {
            elems.push_back(m_resIdMap.value(resId));
        }
        importTg.setElements(elems);
        tgMgr->createGroup(importTg);
    }
    return tgIds.size();
}

qsizetype Importer::importFavoriteLocations(FavoriteLocationModel *favLocModel)
{
    auto favLocs = FavoriteLocation::fromJson(QJsonDocument::fromJson(m_file->customData(BUNDLE_FAVORITE_LOCATION_DOMAIN, u"locations"_s)).array());
    if (!favLocs.empty()) {
        favLocModel->setFavoriteLocations(std::move(favLocs));
    }
    return (qsizetype)favLocs.size();
}

qsizetype Importer::importPasses(PassManager *passMgr)
{
    const auto ids = m_file->listCustomData(BUNDLE_PASS_DOMAIN);
    for (const auto &id : ids) {
        const auto data = m_file->customData(BUNDLE_PASS_DOMAIN, id);
        const auto pass = KItinerary::JsonLdDocument::fromJsonSingular(QJsonDocument::fromJson(data).object());
        passMgr->import(pass, id);
    }
    return ids.size();
}

qsizetype Importer::importHealthCertificates(HealthCertificateManager *healthCertMgr)
{
    const auto certIds = m_file->listCustomData(BUNDLE_HEALTH_CERTIFICATE_DOMAIN);
    for (const auto &certId : certIds) {
        healthCertMgr->importCertificate(m_file->customData(BUNDLE_HEALTH_CERTIFICATE_DOMAIN, certId));
    }
    return certIds.size();
}

qsizetype Importer::importLiveData(LiveDataManager *liveDataMgr)
{
    const auto ids = m_file->listCustomData(BUNDLE_LIVE_DATA_DOMAIN);
    for (const auto &id : ids) {
        const auto obj = QJsonDocument::fromJson(m_file->customData(BUNDLE_LIVE_DATA_DOMAIN, id)).object();
        auto ld = LiveData::fromJson(obj);
        ld.store(id);
        liveDataMgr->importData(id, std::move(ld));
    }
    return ids.size();
}

qsizetype Importer::importLocationSearchHistory()
{
    const auto ids = m_file->listCustomData(BUNDLE_LOCATION_HISTORY_DOMAIN);
    KPublicTransport::LocationHistoryModel model;
    for (const auto &id : ids) {
        model.importEntry(m_file->customData(BUNDLE_LOCATION_HISTORY_DOMAIN, id));
    }
    return ids.size();
}

qsizetype Importer::importPublicTransportAssets()
{
    const auto ids = m_file->listCustomData(BUNDLE_PUBLIC_TRANSPORT_ASSET_DOMAIN);
    const auto assetPath = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + "/org.kde.kpublictransport/assets/"_L1;
    QDir().mkpath(assetPath);

    for (const auto &id: ids) {
        QFile f(assetPath + id);
        if (!f.open(QFile::WriteOnly|QFile::Truncate)) {
            qCWarning(Log) << "Failed to write asset file:" << f.errorString() << f.fileName();
            continue;
        }
        f.write(m_file->customData(BUNDLE_PUBLIC_TRANSPORT_ASSET_DOMAIN, id));
        f.close();

#if KITINERARY_VERSION >= QT_VERSION_CHECK(6, 5, 41)
        // mtime changes need to be done without content changes to take effect
        f.open(QFile::WriteOnly|QFile::Append);
        f.setFileTime(m_file->customDataFileTime(BUNDLE_PUBLIC_TRANSPORT_ASSET_DOMAIN, id), QFile::FileModificationTime);
        f.close();
#endif
    }

    return ids.size();
}

qsizetype Importer::importSettings()
{
    const auto iniData = m_file->customData(BUNDLE_SETTINGS_DOMAIN, QStringLiteral("settings.ini"));
    if (iniData.isEmpty()) {
        return 0;
    }

    QTemporaryFile tmp;
    if (!tmp.open()) {
        qCWarning(Log) << "Failed to open temporary file:" << tmp.errorString();
        return 0;
    }
    tmp.write(iniData);
    tmp.close();

    QSettings settings;
    const QSettings backup(tmp.fileName(), QSettings::IniFormat);
    return copySettings(&backup, &settings);
}
