/*
    GPS Interface
    Copyright (C) 2023 Jasem Mutlaq (mutlaqja@ikarustech.com)

    This library is free software; you can redistribute it and/or
    modify it under the terms of the GNU Lesser General Public
    License as published by the Free Software Foundation; either
    version 2.1 of the License, or (at your option) any later version.

    This library 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
    Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public
    License along with this library; if not, write to the Free Software
    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA

*/

#include "indigpsinterface.h"

#include "indilogger.h"

#include <cstring>
#include <cmath>
#ifdef __linux__
#include <cerrno>
#include <cstdlib>
#include <ctime>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#endif

namespace INDI
{

//////////////////////////////////////////////////////////////////////
///
//////////////////////////////////////////////////////////////////////
GPSInterface::GPSInterface(DefaultDevice * defaultDevice) : m_DefaultDevice(defaultDevice)
{
    m_UpdateTimer.callOnTimeout(std::bind(&GPSInterface::checkGPSState, this));
    m_UpdateTimer.setSingleShot(true);
}

//////////////////////////////////////////////////////////////////////
///
//////////////////////////////////////////////////////////////////////
void GPSInterface::initProperties(const char * groupName)
{
    time(&m_GPSTime);

    // @INDI_STANDARD_PROPERTY@
    PeriodNP[0].fill("PERIOD", "Period (s)", "%.f", 0, 3600, 60.0, 0);
    PeriodNP.fill(m_DefaultDevice->getDeviceName(), "GPS_REFRESH_PERIOD", "Refresh", groupName, IP_RW, 0, IPS_IDLE);

    // @INDI_STANDARD_PROPERTY@
    RefreshSP[0].fill("REFRESH", "GPS", ISS_OFF);
    RefreshSP.fill(m_DefaultDevice->getDeviceName(), "GPS_REFRESH", "Refresh", groupName, IP_RW, ISR_ATMOST1, 0, IPS_IDLE);

    // @INDI_STANDARD_PROPERTY@
    LocationNP[LOCATION_LATITUDE].fill("LAT", "Lat (dd:mm:ss)", "%010.6m", -90, 90, 0, 0.0);
    LocationNP[LOCATION_LONGITUDE].fill("LONG", "Lon (dd:mm:ss)", "%010.6m", 0, 360, 0, 0.0);
    LocationNP[LOCATION_ELEVATION].fill("ELEV", "Elevation (m)", "%g", -200, 10000, 0, 0);
    LocationNP.fill(m_DefaultDevice->getDeviceName(), "GEOGRAPHIC_COORD", "Location", groupName, IP_RO, 60, IPS_IDLE);

    // System Time Settings
    // @INDI_STANDARD_PROPERTY@
    SystemTimeUpdateSP[UPDATE_NEVER].fill("UPDATE_NEVER", "Never", ISS_OFF);
    SystemTimeUpdateSP[UPDATE_ON_STARTUP].fill("UPDATE_ON_STARTUP", "On Startup", ISS_ON);
    SystemTimeUpdateSP[UPDATE_ON_REFRESH].fill("UPDATE_ON_REFRESH", "On Refresh", ISS_OFF);
    SystemTimeUpdateSP.fill(m_DefaultDevice->getDeviceName(), "SYSTEM_TIME_UPDATE", "System Time", groupName, IP_RW,
                            ISR_1OFMANY, 60,
                            IPS_IDLE);
    SystemTimeUpdateSP.load();

    // @INDI_STANDARD_PROPERTY@
    TimeTP[0].fill("UTC", "UTC Time", nullptr);
    TimeTP[1].fill("OFFSET", "UTC Offset", nullptr);
    TimeTP.fill(m_DefaultDevice->getDeviceName(), "TIME_UTC", "UTC", groupName, IP_RO, 60, IPS_IDLE);
}

//////////////////////////////////////////////////////////////////////
///
//////////////////////////////////////////////////////////////////////
void GPSInterface::checkGPSState()
{
    IPState state = updateGPS();

    LocationNP.setState(state);
    TimeTP.setState(state);
    RefreshSP.setState(state);

    switch (state)
    {
        // Ok
        case IPS_OK:
            LocationNP.apply();
            TimeTP.apply();

            // Update system time
            // This ideally should be done only ONCE
            switch (SystemTimeUpdateSP.findOnSwitchIndex())
            {
                case UPDATE_ON_STARTUP:
                    if (m_SystemTimeUpdated == false)
                    {
                        setSystemTime(m_GPSTime);
                        m_SystemTimeUpdated = true;
                    }
                    break;

                case UPDATE_ON_REFRESH:
                    setSystemTime(m_GPSTime);
                    break;

                default:
                    break;
            }

            if (PeriodNP[0].getValue() > 0)
            {
                // We got data OK, but if we are required to update once in a while, we'll call it.
                m_UpdateTimer.setInterval(PeriodNP[0].getValue() * 1000);
                m_UpdateTimer.start();
            }
            else
                m_UpdateTimer.stop();
            return;
            break;

        // GPS fix is in progress or alert
        case IPS_ALERT:
            LocationNP.apply();
            TimeTP.apply();
            break;

        default:
            break;
    }

    m_UpdateTimer.setInterval(5000);
    m_UpdateTimer.start();
}

//////////////////////////////////////////////////////////////////////
///
//////////////////////////////////////////////////////////////////////
bool GPSInterface::updateProperties()
{
    if (m_DefaultDevice->isConnected())
    {
        // Update GPS and send values to client
        IPState state = updateGPS();

        LocationNP.setState(state);
        m_DefaultDevice->defineProperty(LocationNP);
        TimeTP.setState(state);
        m_DefaultDevice->defineProperty(TimeTP);
        RefreshSP.setState(state);
        m_DefaultDevice->defineProperty(RefreshSP);
        m_DefaultDevice->defineProperty(PeriodNP);
        m_DefaultDevice->defineProperty(SystemTimeUpdateSP);

#ifdef __linux__
        // Disable NTP once on connect so GPS can set authoritative system time
        disableNTPForGPSTime();
#endif

        if (state != IPS_OK)
        {
            if (state == IPS_BUSY)
                DEBUGDEVICE(m_DefaultDevice->getDeviceName(), Logger::DBG_SESSION, "GPS fix is in progress...");

            m_UpdateTimer.setInterval(5000);
            m_UpdateTimer.start();
        }
        else if (PeriodNP[0].getValue() > 0)
        {
            m_UpdateTimer.setInterval(PeriodNP[0].getValue());
            m_UpdateTimer.start();
        }
    }
    else
    {
        m_DefaultDevice->deleteProperty(LocationNP);
        m_DefaultDevice->deleteProperty(TimeTP);
        m_DefaultDevice->deleteProperty(RefreshSP);
        m_DefaultDevice->deleteProperty(PeriodNP);
        m_DefaultDevice->deleteProperty(SystemTimeUpdateSP);
        m_UpdateTimer.stop();
        m_SystemTimeUpdated = false;

#ifdef __linux__
        // Restore NTP on disconnect if we were the ones who disabled it
        restoreNTPOnDisconnect();
#endif
    }

    return true;
}

//////////////////////////////////////////////////////////////////////
///
//////////////////////////////////////////////////////////////////////
bool GPSInterface::processNumber(const char * dev, const char * name, double values[], char * names[], int n)
{
    INDI_UNUSED(dev);

    if (PeriodNP.isNameMatch(name))
    {
        double prevPeriod = PeriodNP[0].getValue();
        PeriodNP.update(values, names, n);
        // Do not remove timer if GPS update is still in progress
        if (m_UpdateTimer.isActive() && RefreshSP.getState() != IPS_BUSY)
        {
            m_UpdateTimer.stop();
        }

        if (PeriodNP[0].getValue() == 0)
        {
            DEBUGDEVICE(m_DefaultDevice->getDeviceName(), Logger::DBG_SESSION, "GPS Update Timer disabled.");
        }
        else
        {
            m_UpdateTimer.setInterval(PeriodNP[0].value * 1000);
            m_UpdateTimer.start();
            // Need to warn user this is not recommended. Startup values should be enough
            if (prevPeriod == 0)
            {
                DEBUGDEVICE(m_DefaultDevice->getDeviceName(), Logger::DBG_SESSION,  "GPS Update Timer enabled.");
                if (SystemTimeUpdateSP[UPDATE_ON_REFRESH].getState() == ISS_ON)
                    DEBUGDEVICE(m_DefaultDevice->getDeviceName(), Logger::DBG_WARNING,
                                "Updating system-wide time repeatedly may lead to undesirable side-effects.");
            }
        }

        PeriodNP.setState(IPS_OK);
        PeriodNP.apply();

        return true;
    }

    return false;
}

//////////////////////////////////////////////////////////////////////
///
//////////////////////////////////////////////////////////////////////
bool GPSInterface::processSwitch(const char * dev, const char * name, ISState * states, char * names[], int n)
{
    INDI_UNUSED(dev);

    // Refresh
    if (RefreshSP.isNameMatch(name))
    {
        RefreshSP[0].s = ISS_OFF;
        RefreshSP.setState(IPS_OK);
        RefreshSP.apply();

        // Manual trigger
        checkGPSState();
        return true;
    }
    // System Time Update
    else if (SystemTimeUpdateSP.isNameMatch(name))
    {
        SystemTimeUpdateSP.update(states, names, n);
        SystemTimeUpdateSP.setState(IPS_OK);
        SystemTimeUpdateSP.apply();
        if (SystemTimeUpdateSP.findOnSwitchIndex() == UPDATE_ON_REFRESH)
            DEBUGDEVICE(m_DefaultDevice->getDeviceName(), Logger::DBG_WARNING,
                        "Updating system time on refresh may lead to undesirable effects on system time accuracy.");
        return true;
    }

    return false;
}

//////////////////////////////////////////////////////////////////////
///
//////////////////////////////////////////////////////////////////////
bool GPSInterface::setSystemTime(time_t &raw_time)
{
#ifdef __linux__
    // Detect if running inside a Flatpak sandbox using the canonical env variable
    if (getenv("FLATPAK_ID") != nullptr)
    {
        // Format time as ISO 8601 for timedatectl
        struct tm *tm_info = gmtime(&raw_time);
        char timeStr[32];
        strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", tm_info);

        // Use flatpak-spawn to delegate to timedatectl on the host system.
        // timedatectl handles polkit authentication itself; pkexec is not needed.
        // We use fork()+execvp() instead of system() to avoid shell injection risks.
        pid_t pid = fork();
        if (pid == 0)
        {
            // Child process: exec flatpak-spawn directly (no shell involved)
            const char *args[] = { "flatpak-spawn", "--host", "timedatectl", "set-time", timeStr, nullptr };
            execvp("flatpak-spawn", const_cast<char * const *>(args));
            // If execvp returns, it failed
            _exit(EXIT_FAILURE);
        }
        else if (pid > 0)
        {
            int status = 0;
            waitpid(pid, &status, 0);
            if (!WIFEXITED(status) || WEXITSTATUS(status) != 0)
            {
                DEBUGFDEVICE(m_DefaultDevice->getDeviceName(), Logger::DBG_WARNING,
                             "Failed to update system time via timedatectl (exit code: %d)",
                             WIFEXITED(status) ? WEXITSTATUS(status) : -1);
                return false;
            }
        }
        else
        {
            DEBUGFDEVICE(m_DefaultDevice->getDeviceName(), Logger::DBG_WARNING,
                         "Failed to fork process for timedatectl: %s", strerror(errno));
            return false;
        }
        return true;
    }

    // Native (non-Flatpak) path: use D-Bus via busctl to call systemd-timedated
    // This avoids the EPERM issue with clock_settime() even when CAP_SYS_TIME is set.
    else
    {
        int64_t usec = static_cast<int64_t>(raw_time) * 1000000LL;
        char usecStr[32];
        snprintf(usecStr, sizeof(usecStr), "%lld", static_cast<long long>(usec));

        const char *args[] =
        {
            "busctl", "call",
            "org.freedesktop.timedate1",
            "/org/freedesktop/timedate1",
            "org.freedesktop.timedate1",
            "SetTime", "xbb",
            usecStr, "false", "false",
            nullptr
        };

        if (!runCommand(args))
        {
            DEBUGDEVICE(m_DefaultDevice->getDeviceName(), Logger::DBG_WARNING,
                        "Failed to update system time via busctl timedate1 SetTime");
            return false;
        }
    }
#else
    INDI_UNUSED(raw_time);
#endif
    return true;
}


#ifdef __linux__
//////////////////////////////////////////////////////////////////////
///
//////////////////////////////////////////////////////////////////////
bool GPSInterface::runCommand(const char *args[])
{
    pid_t pid = fork();
    if (pid == 0)
    {
        // Child process
        execvp(args[0], const_cast<char * const *>(args));
        _exit(EXIT_FAILURE);
    }
    else if (pid > 0)
    {
        int status = 0;
        waitpid(pid, &status, 0);
        return WIFEXITED(status) && WEXITSTATUS(status) == 0;
    }
    else
    {
        DEBUGFDEVICE(m_DefaultDevice->getDeviceName(), Logger::DBG_WARNING,
                     "Failed to fork for command %s: %s", args[0], strerror(errno));
        return false;
    }
}

//////////////////////////////////////////////////////////////////////
///
//////////////////////////////////////////////////////////////////////
bool GPSInterface::isNTPActive()
{
    int pipefd[2];
    if (pipe(pipefd) != 0)
        return false;

    pid_t pid = fork();
    if (pid == 0)
    {
        // Child: redirect stdout to pipe
        close(pipefd[0]);
        dup2(pipefd[1], STDOUT_FILENO);
        close(pipefd[1]);

        const char *args[] =
        {
            "timedatectl", "show", "-p", "NTP", "--value", nullptr
        };
        execvp("timedatectl", const_cast<char * const *>(args));
        _exit(EXIT_FAILURE);
    }
    else if (pid > 0)
    {
        // Parent: read output from pipe
        close(pipefd[1]);

        char buffer[32] = {};
        ssize_t n = read(pipefd[0], buffer, sizeof(buffer) - 1);
        close(pipefd[0]);

        int status = 0;
        waitpid(pid, &status, 0);

        if (n <= 0)
            return false;

        // Strip trailing newline
        if (n > 0 && buffer[n - 1] == '\n')
            buffer[n - 1] = '\0';

        return strcmp(buffer, "yes") == 0;
    }
    else
    {
        close(pipefd[0]);
        close(pipefd[1]);
        return false;
    }
}

//////////////////////////////////////////////////////////////////////
///
//////////////////////////////////////////////////////////////////////
bool GPSInterface::setNTPEnabled(bool enabled)
{
    const char *args[] =
    {
        "timedatectl", "set-ntp", enabled ? "true" : "false", nullptr
    };
    return runCommand(args);
}

//////////////////////////////////////////////////////////////////////
///
//////////////////////////////////////////////////////////////////////
void GPSInterface::disableNTPForGPSTime()
{
    if (isNTPActive())
    {
        DEBUGDEVICE(m_DefaultDevice->getDeviceName(), Logger::DBG_SESSION,
                    "NTP is active; disabling for GPS time authority...");
        if (setNTPEnabled(false))
        {
            m_NTPDisabledByGPS = true;
            DEBUGDEVICE(m_DefaultDevice->getDeviceName(), Logger::DBG_SESSION,
                        "NTP disabled by GPS interface.");
        }
        else
        {
            DEBUGDEVICE(m_DefaultDevice->getDeviceName(), Logger::DBG_WARNING,
                        "Failed to disable NTP.");
        }
    }
    else
    {
        DEBUGDEVICE(m_DefaultDevice->getDeviceName(), Logger::DBG_SESSION,
                    "NTP already inactive; GPS will not re-enable it on disconnect.");
        m_NTPDisabledByGPS = false;
    }
}

//////////////////////////////////////////////////////////////////////
///
//////////////////////////////////////////////////////////////////////
void GPSInterface::restoreNTPOnDisconnect()
{
    if (m_NTPDisabledByGPS)
    {
        DEBUGDEVICE(m_DefaultDevice->getDeviceName(), Logger::DBG_SESSION,
                    "Restoring NTP after GPS disconnect...");
        if (setNTPEnabled(true))
        {
            m_NTPDisabledByGPS = false;
            DEBUGDEVICE(m_DefaultDevice->getDeviceName(), Logger::DBG_SESSION,
                        "NTP re-enabled.");
        }
        else
        {
            DEBUGDEVICE(m_DefaultDevice->getDeviceName(), Logger::DBG_WARNING,
                        "Failed to re-enable NTP.");
        }
    }
}
#endif


//////////////////////////////////////////////////////////////////////
///
//////////////////////////////////////////////////////////////////////
IPState GPSInterface::updateGPS()
{
    DEBUGDEVICE(m_DefaultDevice->getDeviceName(), Logger::DBG_ERROR,
                "updateGPS() must be implemented in GPS device child class to update TIME_UTC and GEOGRAPHIC_COORD properties.");
    return IPS_ALERT;
}


bool GPSInterface::saveConfigItems(FILE * fp)
{
    PeriodNP.save(fp);
    SystemTimeUpdateSP.save(fp);
    return true;
}

}
