/*
 * SPDX-FileCopyrightText: 2015 Aleix Pol Gonzalez <aleixpol@kde.org>
 *
 * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
 */

#include <QCryptographicHash>
#include <QIODevice>
#include <QDBusMessage>
#include <QCoreApplication>
#include <QTextStream>
#include <QFile>

#include <KAboutData>

#include "interfaces/devicesmodel.h"
#include "interfaces/notificationsmodel.h"
#include "interfaces/dbusinterfaces.h"
#include "interfaces/dbushelpers.h"
#include "interfaces/conversationmessage.h"
#include "kdeconnect-version.h"

#include <dbushelper.h>

int main(int argc, char** argv)
{
    QCoreApplication app(argc, argv);
    KAboutData about(QStringLiteral("kdeconnect-cli"),
                     QStringLiteral("kdeconnect-cli"),
                     QStringLiteral(KDECONNECT_VERSION_STRING),
                     i18n("KDE Connect CLI tool"),
                     KAboutLicense::GPL,
                     i18n("(C) 2015 Aleix Pol Gonzalez"));
    KAboutData::setApplicationData(about);

    about.addAuthor( i18n("Aleix Pol Gonzalez"), QString(), QStringLiteral("aleixpol@kde.org") );
    about.addAuthor( i18n("Albert Vaca Cintora"), QString(), QStringLiteral("albertvaka@gmail.com") );
    QCommandLineParser parser;
    parser.addOption(QCommandLineOption(QStringList(QStringLiteral("l")) << QStringLiteral("list-devices"), i18n("List all devices")));
    parser.addOption(QCommandLineOption(QStringList(QStringLiteral("a")) << QStringLiteral("list-available"), i18n("List available (paired and reachable) devices")));
    parser.addOption(QCommandLineOption(QStringLiteral("id-only"), i18n("Make --list-devices or --list-available print only the devices id, to ease scripting")));
    parser.addOption(QCommandLineOption(QStringLiteral("name-only"), i18n("Make --list-devices or --list-available print only the devices name, to ease scripting")));
    parser.addOption(QCommandLineOption(QStringLiteral("id-name-only"), i18n("Make --list-devices or --list-available print only the devices id and name, to ease scripting")));
    parser.addOption(QCommandLineOption(QStringLiteral("refresh"), i18n("Search for devices in the network and re-establish connections")));
    parser.addOption(QCommandLineOption(QStringLiteral("pair"), i18n("Request pairing to a said device")));
    parser.addOption(QCommandLineOption(QStringLiteral("ring"), i18n("Find the said device by ringing it.")));
    parser.addOption(QCommandLineOption(QStringLiteral("unpair"), i18n("Stop pairing to a said device")));
    parser.addOption(QCommandLineOption(QStringLiteral("ping"), i18n("Sends a ping to said device")));
    parser.addOption(QCommandLineOption(QStringLiteral("ping-msg"), i18n("Same as ping but you can set the message to display"), i18n("message")));
    parser.addOption(QCommandLineOption(QStringLiteral("share"), i18n("Share a file to a said device"), QStringLiteral("path")));
    parser.addOption(QCommandLineOption(QStringLiteral("share-text"), i18n("Share text to a said device"), QStringLiteral("text")));
    parser.addOption(QCommandLineOption(QStringLiteral("list-notifications"), i18n("Display the notifications on a said device")));
    parser.addOption(QCommandLineOption(QStringLiteral("lock"), i18n("Lock the specified device")));
    parser.addOption(QCommandLineOption(QStringLiteral("send-sms"), i18n("Sends an SMS. Requires destination"), i18n("message")));
    parser.addOption(QCommandLineOption(QStringLiteral("destination"), i18n("Phone number to send the message"), i18n("phone number")));
    //parser.addOption(QCommandLineOption(QStringLiteral("attachment"), i18n("File urls to send attachments with the message (can be passed multiple times)"), i18n("file urls"))); // Disabling this until we have Android support
    parser.addOption(QCommandLineOption(QStringList(QStringLiteral("device")) << QStringLiteral("d"), i18n("Device ID"), QStringLiteral("dev")));
    parser.addOption(QCommandLineOption(QStringList(QStringLiteral("name")) << QStringLiteral("n"), i18n("Device Name"), QStringLiteral("name")));
    parser.addOption(QCommandLineOption(QStringLiteral("encryption-info"), i18n("Get encryption info about said device")));
    parser.addOption(QCommandLineOption(QStringLiteral("list-commands"), i18n("Lists remote commands and their ids")));
    parser.addOption(QCommandLineOption(QStringLiteral("execute-command"), i18n("Executes a remote command by id"), QStringLiteral("id")));
    parser.addOption(QCommandLineOption(QStringList{QStringLiteral("k"), QStringLiteral("send-keys")}, i18n("Sends keys to a said device"), QStringLiteral("key")));
    parser.addOption(QCommandLineOption(QStringLiteral("my-id"), i18n("Display this device's id and exit")));
    parser.addOption(QCommandLineOption(QStringLiteral("photo"), i18n("Open the connected device's camera and transfer the photo"), QStringLiteral("path")));

    //Hidden because it's an implementation detail
    QCommandLineOption deviceAutocomplete(QStringLiteral("shell-device-autocompletion"));
    deviceAutocomplete.setFlags(QCommandLineOption::HiddenFromHelp);
    deviceAutocomplete.setDescription(QStringLiteral("Outputs all available devices id's with their name and paired status")); //Not visible, so no translation needed
    deviceAutocomplete.setValueName(QStringLiteral("shell"));
    parser.addOption(deviceAutocomplete);
    about.setupCommandLine(&parser);

    parser.process(app);
    about.processCommandLine(&parser);

    const QString id = QStringLiteral("kdeconnect-cli-") + QString::number(QCoreApplication::applicationPid());
    DaemonDbusInterface iface;

    if (parser.isSet(QStringLiteral("my-id"))) {
        QTextStream(stdout) << iface.selfId() << endl;
    } else if (parser.isSet(QStringLiteral("l")) || parser.isSet(QStringLiteral("a"))) {
        bool reachable = false;
        if (parser.isSet(QStringLiteral("a"))) {
            reachable = true;
        } else {
            blockOnReply(iface.acquireDiscoveryMode(id));
            QThread::sleep(2);
        }
        const QStringList devices = blockOnReply<QStringList>(iface.devices(reachable, false));

        bool displayCount = true;
        for (const QString& id : devices) {
            if (parser.isSet(QStringLiteral("id-only"))) {
                QTextStream(stdout) << id << endl;
                displayCount = false;
            } else if (parser.isSet(QStringLiteral("name-only"))) {
                DeviceDbusInterface deviceIface(id);
                QTextStream(stdout) << deviceIface.name() << endl;
                displayCount = false;
            } else if (parser.isSet(QStringLiteral("id-name-only"))) {
                DeviceDbusInterface deviceIface(id);
                QTextStream(stdout) << id << ' ' << deviceIface.name() << endl;
                displayCount = false;
            } else {
                DeviceDbusInterface deviceIface(id);
                QString statusInfo;
                const bool isReachable = deviceIface.isReachable();
                const bool isTrusted = deviceIface.isTrusted();
                if (isReachable && isTrusted) {
                    statusInfo = i18n("(paired and reachable)");
                } else if (isReachable) {
                    statusInfo = i18n("(reachable)");
                } else if (isTrusted) {
                    statusInfo = i18n("(paired)");
                }
                QTextStream(stdout) << "- " << deviceIface.name()
                        << ": " << deviceIface.id() << ' ' << statusInfo << endl;
            }
        }
        if (displayCount) {
            QTextStream(stderr) << i18np("1 device found", "%1 devices found", devices.size()) << endl;
        } else if (devices.isEmpty()) {
            QTextStream(stderr) << i18n("No devices found") << endl;
        }

        blockOnReply(iface.releaseDiscoveryMode(id));
    } else if (parser.isSet(QStringLiteral("shell-device-autocompletion"))) {
        //Outputs a list of reachable devices in zsh autocomplete format, with the name as description
        const QStringList devices = blockOnReply<QStringList>(iface.devices(true, false));
        for (const QString &id : devices) {
            DeviceDbusInterface deviceIface(id);
            QString statusInfo;
            const bool isTrusted = deviceIface.isTrusted();
            if (isTrusted) {
                statusInfo = i18n("(paired)");
            } else {
                statusInfo = i18n("(unpaired)");
            }

            //Description: "device name (paired/unpaired)"
            QString description = deviceIface.name() + QLatin1Char(' ') + statusInfo;
            //Replace characters
            description.replace(QLatin1Char('\\'), QStringLiteral("\\\\"));
            description.replace(QLatin1Char('['),  QStringLiteral("\\["));
            description.replace(QLatin1Char(']'),  QStringLiteral("\\]"));
            description.replace(QLatin1Char('\''), QStringLiteral("\\'"));
            description.replace(QLatin1Char('\"'), QStringLiteral("\\\""));
            description.replace(QLatin1Char('\n'), QLatin1Char(' '));
            description.remove(QLatin1Char('\0'));

            //Output id and description
            QTextStream(stdout) << id << '[' << description << ']' << endl;
        }

        //Exit with 1 if we didn't find a device
        return int(devices.isEmpty());
    } else if(parser.isSet(QStringLiteral("refresh"))) {
        QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kdeconnect"), QStringLiteral("/modules/kdeconnect"), QStringLiteral("org.kde.kdeconnect.daemon"), QStringLiteral("forceOnNetworkChange"));
        blockOnReply(DBusHelper::sessionBus().asyncCall(msg));
    } else {

        QString device = parser.value(QStringLiteral("device"));
        if (device.isEmpty() && parser.isSet(QStringLiteral("name"))) {
            device = blockOnReply(iface.deviceIdByName(parser.value(QStringLiteral("name"))));
            if (device.isEmpty()) {
                QTextStream(stderr) << "Couldn't find device: " << parser.value(QStringLiteral("name")) << endl;
                return 1;
            }
        }
        if(device.isEmpty()) {
            QTextStream(stderr) << i18n("No device specified: Use -d <Device ID> or -n <Device Name> to specify a device. \nDevice ID's and names may be found using \"kdeconnect-cli -l\" \nView complete help with --help option") << endl;
            return 1;
        }

        if (!blockOnReply<QStringList>(iface.devices(false, false)).contains(device)) {
            QTextStream(stderr) << "Couldn't find device with id \"" << device << "\". To specify a device by name use -n <devicename>" << endl;
            return 1;
        }

        if (parser.isSet(QStringLiteral("share"))) {
            QStringList urls;

            QUrl url = QUrl::fromUserInput(parser.value(QStringLiteral("share")), QDir::currentPath());
            urls.append(url.toString());

            // Check for more arguments
            const auto args = parser.positionalArguments();
            for (const QString& input : args) {
                QUrl url = QUrl::fromUserInput(input, QDir::currentPath());
                urls.append(url.toString());
            }

            QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kdeconnect"), QStringLiteral("/modules/kdeconnect/devices/") + device + QStringLiteral("/share"),
                                                              QStringLiteral("org.kde.kdeconnect.device.share"), QStringLiteral("shareUrls"));

            msg.setArguments(QVariantList() << QVariant(urls));
            blockOnReply(DBusHelper::sessionBus().asyncCall(msg));

            for (const QString& url : qAsConst(urls)) {
                QTextStream(stdout) << i18n("Shared %1", url) << endl;
            }
        } else if (parser.isSet(QStringLiteral("share-text"))) {
            QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kdeconnect"), QStringLiteral("/modules/kdeconnect/devices/") + device + QStringLiteral("/share"), QStringLiteral("org.kde.kdeconnect.device.share"), QStringLiteral("shareText"));
            msg.setArguments(QVariantList() << parser.value(QStringLiteral("share-text")));
            blockOnReply(DBusHelper::sessionBus().asyncCall(msg));
            QTextStream(stdout) << i18n("Shared text: %1", parser.value(QStringLiteral("share-text"))) << endl;
        } else if(parser.isSet(QStringLiteral("pair"))) {
            DeviceDbusInterface dev(device);
            if (!dev.isReachable()) {
                //Device doesn't exist, go into discovery mode and wait up to 30 seconds for the device to appear
                QEventLoop wait;
                QTextStream(stderr) << i18n("waiting for device...") << endl;
                blockOnReply(iface.acquireDiscoveryMode(id));

                QObject::connect(&iface, &DaemonDbusInterface::deviceAdded, &iface, [&](const QString& deviceAddedId) {
                    if (device == deviceAddedId) {
                        wait.quit();
                    }
                });
                QTimer::singleShot(30 * 1000, &wait, &QEventLoop::quit);

                wait.exec();
            }

            if (!dev.isReachable()) {
                QTextStream(stderr) << i18n("Device not found") << endl;
            } else if(blockOnReply<bool>(dev.isTrusted())) {
                QTextStream(stderr) << i18n("Already paired") << endl;
            } else {
                QTextStream(stderr) << i18n("Pair requested") << endl;
                blockOnReply(dev.requestPair());
            }
            blockOnReply(iface.releaseDiscoveryMode(id));
        } else if(parser.isSet(QStringLiteral("unpair"))) {
            DeviceDbusInterface dev(device);
            if (!dev.isTrusted()) {
                QTextStream(stderr) << i18n("Already not paired") << endl;
            } else {
                QTextStream(stderr) << i18n("Unpaired") << endl;
                blockOnReply(dev.unpair());
            }
        } else if(parser.isSet(QStringLiteral("ping")) || parser.isSet(QStringLiteral("ping-msg"))) {
            QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kdeconnect"), QStringLiteral("/modules/kdeconnect/devices/") + device + QStringLiteral("/ping"), QStringLiteral("org.kde.kdeconnect.device.ping"), QStringLiteral("sendPing"));
            if (parser.isSet(QStringLiteral("ping-msg"))) {
                QString message = parser.value(QStringLiteral("ping-msg"));
                msg.setArguments(QVariantList() << message);
            }
            blockOnReply(DBusHelper::sessionBus().asyncCall(msg));
        } else if(parser.isSet(QStringLiteral("send-sms"))) {
            if (parser.isSet(QStringLiteral("destination"))) {
                qDBusRegisterMetaType<ConversationAddress>();
                QVariantList addresses;

                const QStringList addressList = parser.value(QStringLiteral("destination")).split(QRegularExpression(QStringLiteral("\\s+")));

                for (const QString& input : addressList) {
                    ConversationAddress address(input);
                    addresses << QVariant::fromValue(address);
                }

                const QString message = parser.value(QStringLiteral("send-sms"));

                const QStringList rawAttachmentUrlsList = parser.values(QStringLiteral("attachment"));

                QVariantList attachments;
                for (const QString& attachmentUrl : rawAttachmentUrlsList) {
                    // TODO: Construct attachment objects from the list of Urls
                    Q_UNUSED(attachmentUrl);
                }

                DeviceConversationsDbusInterface conversationDbusInterface(device);
                auto reply = conversationDbusInterface.sendWithoutConversation(addresses, message, attachments);

                reply.waitForFinished();
            } else {
                QTextStream(stderr) << i18n("error: should specify the SMS's recipient by passing --destination <phone number>");
                return 1;
            }
        } else if(parser.isSet(QStringLiteral("ring"))) {
            QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kdeconnect"), QStringLiteral("/modules/kdeconnect/devices/") + device + QStringLiteral("/findmyphone"), QStringLiteral("org.kde.kdeconnect.device.findmyphone"), QStringLiteral("ring"));
            blockOnReply(DBusHelper::sessionBus().asyncCall(msg));
        } else if(parser.isSet(QStringLiteral("photo"))) {
            const QString fileName = parser.value(QStringLiteral("photo"));
            if (!fileName.isEmpty()) {
                QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kdeconnect"), QStringLiteral("/modules/kdeconnect/devices/") + device + QStringLiteral("/photo"), QStringLiteral("org.kde.kdeconnect.device.photo"), QStringLiteral("requestPhoto"));
                msg.setArguments({fileName});
                blockOnReply(DBusHelper::sessionBus().asyncCall(msg));
            } else {
                QTextStream(stderr) << i18n("Please specify a filename for the photo") << endl;
            }
        } else if(parser.isSet(QStringLiteral("send-keys"))) {
            QString seq = parser.value(QStringLiteral("send-keys"));
            QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kdeconnect"), QStringLiteral("/modules/kdeconnect/devices/") + device + QStringLiteral("/remotekeyboard"), QStringLiteral("org.kde.kdeconnect.device.remotekeyboard"), QStringLiteral("sendKeyPress"));
            if (seq.trimmed() == QLatin1String("-")) {
                // from stdin
                QFile in;
                if(in.open(stdin,QIODevice::ReadOnly | QIODevice::Unbuffered)) {
                    while (!in.atEnd()) {
                        QByteArray line = in.readLine();  // sanitize to ASCII-codes > 31?
                        msg.setArguments({QString::fromLatin1(line), -1, false, false, false});
                        blockOnReply(DBusHelper::sessionBus().asyncCall(msg));
                    }
                    in.close();
                }
            } else {
                msg.setArguments({seq, -1, false, false, false});
                blockOnReply(DBusHelper::sessionBus().asyncCall(msg));
            }
        } else if(parser.isSet(QStringLiteral("list-notifications"))) {
            NotificationsModel notifications;
            notifications.setDeviceId(device);
            for(int i=0, rows=notifications.rowCount(); i<rows; ++i) {
                QModelIndex idx = notifications.index(i);
                QTextStream(stdout) << "- " << idx.data(NotificationsModel::AppNameModelRole).toString()
                    << ": " << idx.data(NotificationsModel::NameModelRole).toString() << endl;
            }
        } else if(parser.isSet(QStringLiteral("list-commands"))) {
            RemoteCommandsDbusInterface iface(device);
            const auto cmds = QJsonDocument::fromJson(iface.commands()).object();
            for (auto it = cmds.constBegin(), itEnd = cmds.constEnd(); it!=itEnd; ++it) {
                const QJsonObject cont = it->toObject();
                QTextStream(stdout) << it.key() << ": " << cont.value(QStringLiteral("name")).toString() << ": " << cont.value(QStringLiteral("command")).toString() << endl;
            }
        } else if(parser.isSet(QStringLiteral("execute-command"))) {
            RemoteCommandsDbusInterface iface(device);
            blockOnReply(iface.triggerCommand(parser.value(QStringLiteral("execute-command"))));
        } else if(parser.isSet(QStringLiteral("encryption-info"))) {
            DeviceDbusInterface dev(device);
            QString info = blockOnReply<QString>(dev.encryptionInfo()); // QSsl::Der = 1
            QTextStream(stdout) << info << endl;
        } else {
            QTextStream(stderr) << i18n("Nothing to be done") << endl;
        }
    }
    QMetaObject::invokeMethod(&app, "quit", Qt::QueuedConnection);

    return app.exec();
}
