// SPDX-License-Identifier: GPL-3.0-or-later

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright (C) 2013 - 2024, nymea GmbH
* Copyright (C) 2024 - 2025, chargebyte austria GmbH
*
* This file is part of nymea-plugins.
*
* nymea-plugins 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 3 of the License, or
* (at your option) any later version.
*
* nymea-plugins 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 nymea-plugins. If not, see <https://www.gnu.org/licenses/>.
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

#include "kecontact.h"
#include "extern-plugininfo.h"

#include <QVariant>
#include <QJsonDocument>

KeContact::KeContact(const QHostAddress &address, KeContactDataLayer *dataLayer, QObject *parent) :
    QObject(parent),
    m_dataLayer(dataLayer),
    m_address(address)
{
    qCDebug(dcKeba()) << "Creating KeContact connection for address" << m_address;
    m_requestTimeoutTimer = new QTimer(this);
    m_requestTimeoutTimer->setSingleShot(true);
    connect(m_requestTimeoutTimer, &QTimer::timeout, this, [this]() {
        // This timer will be started when a request is sent and stopped or resetted when a response has been received
        setReachable(false);

        if (m_currentRequest.isValid()) {
            // Schedule pause timer to send next request
            qCWarning(dcKeba()) << "Command timeouted" << m_currentRequest.command();
            emit commandExecuted(m_currentRequest.requestId(), false);
        }

        // Timeout...send the next request right the way since at least 5 seconds passed since tha last command
        m_currentRequest = KeContactRequest();
        sendNextCommand();
    });

    m_pauseTimer = new QTimer(this);
    m_pauseTimer->setSingleShot(true);
    connect(m_pauseTimer, &QTimer::timeout, this, [this](){
        sendNextCommand();
    });

    connect(m_dataLayer, &KeContactDataLayer::datagramReceived, this, &KeContact::onReceivedDatagram);
}

KeContact::~KeContact()
{
    qCDebug(dcKeba()) << "Deleting KeContact connection for address" << m_address.toString();
}

QHostAddress KeContact::address() const
{
    return m_address;
}

QUuid KeContact::start(const QByteArray &rfidToken, const QByteArray &rfidClassifier)
{
    if (!m_dataLayer) {
        qCWarning(dcKeba()) << "UDP socket not initialized";
        setReachable(false);
        return QUuid();
    }

    QByteArray datagram = "start " + rfidToken + " " + rfidClassifier;
    KeContactRequest request(QUuid::createUuid(), datagram);
    qCDebug(dcKeba()) << "Start: Datagram:" << datagram;
    m_requestQueue.enqueue(request);
    sendNextCommand();
    return request.requestId();
}

QUuid KeContact::stop(const QByteArray &rfidToken)
{
    if (!m_dataLayer) {
        qCWarning(dcKeba()) << "UDP socket not initialized";
        setReachable(false);
        return QUuid();
    }

    QByteArray datagram = "stop " + rfidToken;
    KeContactRequest request(QUuid::createUuid(), datagram);
    qCDebug(dcKeba()) << "Stop: Datagram:" << datagram;
    m_requestQueue.enqueue(request);
    sendNextCommand();
    return request.requestId();
}

void KeContact::setAddress(const QHostAddress &address)
{
    if (m_address == address)
        return;

    qCDebug(dcKeba()) << "Updating Keba connection address from" << m_address.toString() << "to" << address.toString();
    m_address = address;
}

bool KeContact::reachable() const
{
    return m_reachable;
}

void KeContact::sendCommand(const QByteArray &command)
{
    if (!m_dataLayer) {
        qCWarning(dcKeba()) << "UDP socket not initialized";
        setReachable(false);
        return;
    }

    m_dataLayer->write(m_address, command);
    m_requestTimeoutTimer->start(5000);
}

void KeContact::sendNextCommand()
{
    // No message left, we are done
    if (m_requestQueue.isEmpty())
        return;

    // Still a request pending
    if (m_currentRequest.isValid())
        return;

    m_currentRequest = m_requestQueue.dequeue();
    sendCommand(m_currentRequest.command());
}

void KeContact::setReachable(bool reachable)
{
    if (m_reachable == reachable)
        return;

    if (reachable) {
        qCDebug(dcKeba()) << "The keba wallbox on" << m_address.toString() << "is now reachable again.";
    } else {
        qCWarning(dcKeba()) << "The keba wallbox on" << m_address.toString() << "is not reachable any more.";
        m_requestQueue.clear();
        m_currentRequest = KeContactRequest();
    }

    m_reachable = reachable;
    emit reachableChanged(m_reachable);
}

QUuid KeContact::enableOutput(bool state)
{
    if (!m_dataLayer) {
        qCWarning(dcKeba()) << "UDP socket not initialized";
        setReachable(false);
        return QUuid();
    }

    // Print information that we are executing now the update action;
    QByteArray datagram;
    if (state){
        datagram.append("ena 1");
    } else{
        datagram.append("ena 0");
    }

    KeContactRequest request(QUuid::createUuid(), datagram);
    request.setDelayUntilNextCommand(2000);
    qCDebug(dcKeba()) << "Enable output: Datagram:" << datagram;
    m_requestQueue.enqueue(request);
    sendNextCommand();
    return request.requestId();
}

QUuid KeContact::setMaxAmpere(int milliAmpere)
{
    if (!m_dataLayer) {
        qCWarning(dcKeba()) << "UDP socket not initialized";
        setReachable(false);
        return QUuid();
    }

    if (milliAmpere < 6000 || milliAmpere > 63000) {
        qCWarning(dcKeba()) << "KeContact: Set max ampere, currtime mA out of range [6000, 63000]" << milliAmpere;
        return QUuid();
    }

    // Print information that we are executing now the update action
    qCDebug(dcKeba()) << "Update max current to : " << milliAmpere;
    QString commandLine = QString("currtime %1 1").arg(milliAmpere);
    QByteArray datagram = commandLine.toUtf8();
    KeContactRequest request(QUuid::createUuid(), datagram);
    request.setDelayUntilNextCommand(1200);
    qCDebug(dcKeba()) << "Set max charging amps: Datagram:" << datagram;
    m_requestQueue.enqueue(request);
    sendNextCommand();
    return request.requestId();
}

QUuid KeContact::setMaxAmpereGeneral(int milliAmpere)
{
    if (!m_dataLayer) {
        qCWarning(dcKeba()) << "UDP socket not initialized";
        setReachable(false);
        return QUuid();
    }

    if (milliAmpere < 6000 || milliAmpere > 63000) {
        qCWarning(dcKeba()) << "KeContact: Set max ampere curr, mA out of range [6000, 63000]" << milliAmpere;
        return QUuid();
    }

    // Print information that we are executing now the update action
    qCDebug(dcKeba()) << "Update general max current to: " << milliAmpere;
    QString commandLine = QString("curr %1").arg(milliAmpere);
    QByteArray datagram = commandLine.toUtf8();
    KeContactRequest request(QUuid::createUuid(), datagram);
    request.setDelayUntilNextCommand(1200);
    m_requestQueue.enqueue(request);
    sendNextCommand();
    return request.requestId();
}

QUuid KeContact::displayMessage(const QByteArray &message)
{
    if (!m_dataLayer) {
        qCWarning(dcKeba()) << "UDP socket not initialized";
        setReachable(false);
        return QUuid();
    }

    /* Text shown on the display. Maximum 23 ASCII characters can be used. 0 .. 23 characters
    ~ == Σ
    $ == blank
    , == comma
    */

    qCDebug(dcKeba()) << "Set display message: " << message;
    QByteArray datagram;
    QByteArray modifiedMessage = message;
    modifiedMessage.replace(" ", "$");
    if (modifiedMessage.size() > 23) {
        modifiedMessage.resize(23);
    }
    datagram.append("display 0 0 0 0 " + modifiedMessage);
    KeContactRequest request(QUuid::createUuid(), datagram);
    qCDebug(dcKeba()) << "Display message: Datagram:" << datagram;
    m_requestQueue.enqueue(request);
    sendNextCommand();
    return request.requestId();
}

QUuid KeContact::chargeWithEnergyLimit(double energy)
{
    if (!m_dataLayer) {
        qCWarning(dcKeba()) << "UDP socket not initialized";
        setReachable(false);
        return QUuid();
    }

    QByteArray datagram;
    datagram.append("setenergy " + QVariant(static_cast<int>(energy*10000)).toByteArray());
    KeContactRequest request(QUuid::createUuid(), datagram);
    qCDebug(dcKeba()) << "Charge with energy limit: Datagram: " << datagram;
    m_requestQueue.enqueue(request);
    sendNextCommand();
    return request.requestId();
}

QUuid KeContact::setFailsafe(int timeout, int current, bool save)
{
    if (!m_dataLayer) {
        qCWarning(dcKeba()) << "UDP socket not initialized";
        setReachable(false);
        return QUuid();
    }

    QByteArray data;
    data.append("failsave");
    data.append(" "+QVariant(timeout).toByteArray());
    data.append(" "+QVariant(current).toByteArray());
    data.append((save ? " 1":" 0"));
    KeContactRequest request(QUuid::createUuid(), data);
    qCDebug(dcKeba()) << "Set failsafe mode: Datagram: " << data;
    m_requestQueue.enqueue(request);
    sendNextCommand();
    return request.requestId();
}

void KeContact::getDeviceInformation()
{
    QByteArray data;
    data.append("i");
    KeContactRequest request(QUuid::createUuid(), data);
    qCDebug(dcKeba()) << "Get device information: Datagram: " << data;
    m_requestQueue.enqueue(request);
    sendNextCommand();
}

void KeContact::getReport1()
{
    getReport(1);
}

void KeContact::getReport2()
{
    getReport(2);
}

void KeContact::getReport3()
{
    getReport(3);
}

void KeContact::getReport1XX(int reportNumber)
{
    getReport(reportNumber);
}

QUuid KeContact::setOutputX2(bool state)
{
    if (!m_dataLayer) {
        qCWarning(dcKeba()) << "UDP socket not initialized";
        setReachable(false);
        return QUuid();
    }


    QByteArray datagram;
    datagram.append("output " + QVariant((state ? 1 : 0)).toByteArray());

    KeContactRequest request(QUuid::createUuid(), datagram);
    qCDebug(dcKeba()) << "Set Output X2, state:" << state << "Datagram:" << datagram;
    m_requestQueue.enqueue(request);
    sendNextCommand();
    return request.requestId();
}

void KeContact::getReport(int reportNumber)
{
    if (!m_dataLayer) {
        qCWarning(dcKeba()) << "UDP socket not initialized";
        setReachable(false);
        return;
    }

    QByteArray datagram;
    datagram.append("report " + QVariant(reportNumber).toByteArray());

    KeContactRequest request(QUuid::createUuid(), datagram);
    qCDebug(dcKeba()) << "Get report" << reportNumber << "Datagram:" << datagram;
    m_requestQueue.enqueue(request);
    sendNextCommand();
}

QUuid KeContact::unlockCharger()
{
    if (!m_dataLayer) {
        qCWarning(dcKeba()) << "UDP socket not initialized";
        setReachable(false);
        return QUuid();
    }

    QByteArray datagram;
    datagram.append("unlock");

    KeContactRequest request(QUuid::createUuid(), datagram);
    qCDebug(dcKeba()) << "Unlock charger: Datagram:" << datagram;
    m_requestQueue.enqueue(request);
    sendNextCommand();
    return request.requestId();
}

void KeContact::onReceivedDatagram(const QHostAddress &address, const QByteArray &datagram)
{
    // Make sure the datagram is for this keba
    if (address != m_address)
        return;

    if (datagram.contains("TCH-OK")){
        // We received valid data from the address over the data link, so the wallbox must be reachable
        setReachable(true);

        //Command response has been received, now send the next command
        m_requestTimeoutTimer->stop();

        if (m_currentRequest.isValid()) {
            if (datagram.contains("done")) {
                qCDebug(dcKeba()) << "Command" << m_currentRequest.command() << "finished successfully";
                emit commandExecuted(m_currentRequest.requestId(), true);
            } else {
                qCWarning(dcKeba()) << "Command" << m_currentRequest.command() << "finished with error" << datagram;
                emit commandExecuted(m_currentRequest.requestId(), false);
            }

            // Schedule pause timer to send next request
            m_pauseTimer->start(m_currentRequest.delayUntilNextCommand());
            m_currentRequest = KeContactRequest();
        } else {
            //Probably the response has taken too long and the requestId has been already removed
            qCWarning(dcKeba()) << "Received command OK response without pending request." << datagram;
        }
    } else if (datagram.left(8).contains("Firmware")){
        // We received valid data from the address over the data link, so the wallbox must be reachable
        setReachable(true);

        // Command response has been received, now send the next command
        m_requestTimeoutTimer->stop();
        if (m_currentRequest.isValid()) {
            // Schedule pause timer to send next request
            m_pauseTimer->start(m_currentRequest.delayUntilNextCommand());
            m_currentRequest = KeContactRequest();
        }

        qCDebug(dcKeba()) << "Firmware information received";
        QByteArrayList firmware = datagram.split(':');
        if (firmware.length() >= 2) {
            emit deviceInformationReceived(firmware[1]);
        }
    } else {
        //Command response has been received, now send the next command
        m_requestTimeoutTimer->stop();
        if (m_currentRequest.isValid()) {
            // Schedule pause timer to send next request
            m_pauseTimer->start(m_currentRequest.delayUntilNextCommand());
            m_currentRequest = KeContactRequest();
        }

        // Convert the rawdata to a json document
        QJsonParseError error;
        QJsonDocument jsonDoc = QJsonDocument::fromJson(datagram, &error);
        if (error.error != QJsonParseError::NoError) {
            qCWarning(dcKeba()) << "Failed to parse JSON data" << datagram << ":" << error.errorString();
            return;
        }

        QVariantMap data = jsonDoc.toVariant().toMap();

        if (data.contains("ID")) {
            int id = data.value("ID").toInt();
            if (id == 1) {
                // We received valid data from the address over the data link, so the wallbox must be reachable
                setReachable(true);

                ReportOne reportOne;
                //qCDebug(dcKeba()) << "Report 1 received";
                reportOne.product      = data.value("Product").toString();
                reportOne.firmware     = data.value("Firmware").toString();
                reportOne.serialNumber = data.value("Serial").toString();
                //"Backend:"
                //"timeQ": 3
                //"DIP-Sw1": "0x22"
                //"DIP-Sw2":
                reportOne.dipSw1 = data.value("DIP-Sw1").toString().remove("0x").toUInt(nullptr, 16);
                reportOne.dipSw2 = data.value("DIP-Sw2").toString().remove("0x").toUInt(nullptr, 16);

                if (data.contains("COM-module")) {
                    reportOne.comModule = (data.value("COM-module").toInt() == 1);
                } else {
                    reportOne.comModule = false;
                }
                if (data.contains("Sec")) {
                    reportOne.comModule = data.value("Sec").toInt();
                } else {
                    reportOne.comModule = 0;
                }
                emit reportOneReceived(reportOne);

            } else if (id == 2) {
                // We received valid data from the address over the data link, so the wallbox must be reachable
                setReachable(true);

                ReportTwo reportTwo;
                //qCDebug(dcKeba()) << "Report 2 received";
                int state = data.value("State").toInt();
                reportTwo.state = State(state);
                reportTwo.error1 = data.value("Error1").toInt();
                reportTwo.error2 = data.value("Error2").toInt();
                reportTwo.plugState = PlugState(data.value("Plug").toInt());
                reportTwo.enableUser = data.value("Enable user").toBool();
                reportTwo.enableSys = data.value("Enable sys").toBool();
                reportTwo.maxCurrent = data.value("Max curr").toInt() / 1000.00;
                reportTwo.maxCurrentPercentage = data.value("Max curr %").toInt() / 10.00;
                reportTwo.currentHardwareLimitation = data.value("Curr HW").toInt() / 1000.00;
                reportTwo.currentUser = data.value("Curr user").toInt() / 1000.00;
                reportTwo.currTimer = data.value("Curr timer").toInt() / 1000.00;
                reportTwo.timeoutCt = data.value("Tmo CT").toInt();
                reportTwo.currentFailsafe = data.value("Curr FS").toInt() / 1000.00;
                reportTwo.timeoutFailsafe = data.value("Tmo FS").toInt();
                reportTwo.setEnergy = data.value("Setenergy").toInt() / 10000.00;
                reportTwo.output = data.value("Output").toInt();
                reportTwo.input= data.value("Input").toInt();
                reportTwo.serialNumber = data.value("Serial").toString();
                reportTwo.seconds = data.value("Sec").toInt();
                // Not documented:
                //"AuthON": 0
                //"Authreq": 0
                emit reportTwoReceived(reportTwo);

            } else if (id == 3) {
                // We received valid data from the address over the data link, so the wallbox must be reachable
                setReachable(true);

                ReportThree reportThree;
                //qCDebug(dcKeba()) << "Report 3 received";
                reportThree.currentPhase1 = data.value("I1").toInt() / 1000.00;
                reportThree.currentPhase2 = data.value("I2").toInt() / 1000.00;
                reportThree.currentPhase3 = data.value("I3").toInt() / 1000.00;
                reportThree.voltagePhase1 = data.value("U1").toInt();
                reportThree.voltagePhase2 = data.value("U2").toInt();
                reportThree.voltagePhase3 = data.value("U3").toInt();
                reportThree.power         = data.value("P").toInt() / 1000.00;
                reportThree.powerFactor   = data.value("PF").toInt() / 10.00;
                reportThree.energySession = data.value("E pres").toInt() / 10000.00;
                reportThree.energyTotal   = data.value("E total").toInt() / 10000.00;
                reportThree.serialNumber  = data.value("Serial").toString();
                reportThree.seconds  = data.value("Sec").toInt();
                emit reportThreeReceived(reportThree);
            } else if (id >= 100) {
                // We received valid data from the address over the data link, so the wallbox must be reachable
                setReachable(true);

                Report1XX report;
                //qCDebug(dcKeba()) << "Report" << id << "received";
                report.sessionId = data.value("Session ID").toInt();
                report.currHW = data.value("Curr HW").toInt();
                report.startEnergy = data.value("E start").toInt() / 10000.00;
                report.presentEnergy = data.value("E pres").toInt() / 10000.00;
                report.startTime = data.value("started[s]").toInt();
                report.endTime = data.value("ended[s]").toInt();
                report.stopReason = data.value("reason").toInt();
                report.rfidTag = data.value("RFID tag").toByteArray();
                report.rfidClass = data.value("RFID class").toByteArray();
                report.serialNumber = data.value("Serial").toString();
                report.seconds = data.value("Sec").toInt();
                emit report1XXReceived(id, report);
            }
        } else {
            // Broadcast message, lets see what we recognize

            if (data.contains("State")) {
                // We received valid data from the address over the data link, so the wallbox must be reachable
                setReachable(true);
                emit broadcastReceived(BroadcastType::BroadcastTypeState, data.value("State"));
            }
            if (data.contains("Plug")) {
                // We received valid data from the address over the data link, so the wallbox must be reachable
                setReachable(true);
                emit broadcastReceived(BroadcastType::BroadcastTypePlug, data.value("Plug"));
            }
            if (data.contains("Input")) {
                // We received valid data from the address over the data link, so the wallbox must be reachable
                setReachable(true);
                emit broadcastReceived(BroadcastType::BroadcastTypeInput, data.value("Input"));
            }
            if (data.contains("Enable sys")) {
                // We received valid data from the address over the data link, so the wallbox must be reachable
                setReachable(true);
                emit broadcastReceived(BroadcastType::BroadcastTypeEnableSys, data.value("Enable sys"));
            }
            if (data.contains("Max curr")) {
                // We received valid data from the address over the data link, so the wallbox must be reachable
                setReachable(true);
                emit broadcastReceived(BroadcastType::BroadcastTypeMaxCurr, data.value("Max curr"));
            }
            if (data.contains("E pres")) {
                // We received valid data from the address over the data link, so the wallbox must be reachable
                setReachable(true);
                emit broadcastReceived(BroadcastType::BroadcastTypeEPres, data.value("E pres"));
            }
        }
    }
}
