// SPDX-License-Identifier: GPL-2.0-only
/*
 * Digital Voice Modem - Common Library
 * GPLv2 Open Source. Use is subject to license terms.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 *  Copyright (C) 2023-2026 Bryan Biedenkapp, N2PLL
 *  Copyright (C) 2024 Patrick McDonnell, W3AXL
 *
 */
#include "lookups/TalkgroupRulesLookup.h"
#include "Log.h"
#include "Timer.h"
#include "Utils.h"

using namespace lookups;

#include <string>
#include <vector>

// ---------------------------------------------------------------------------
//  Static Class Members
// ---------------------------------------------------------------------------

std::mutex TalkgroupRulesLookup::s_mutex;
bool TalkgroupRulesLookup::s_locked = false;

// ---------------------------------------------------------------------------
//  Macros
// ---------------------------------------------------------------------------

// Lock the table.
#define __LOCK_TABLE()                          \
    std::lock_guard<std::mutex> lock(s_mutex);  \
    s_locked = true;

// Unlock the table.
#define __UNLOCK_TABLE() s_locked = false;

// Spinlock wait for table to be read unlocked.
#define __SPINLOCK()                            \
    if (s_locked) {                             \
        while (s_locked)                        \
            Thread::sleep(2U);                  \
    }

// ---------------------------------------------------------------------------
//  Public Class Members
// ---------------------------------------------------------------------------

/* Initializes a new instance of the TalkgroupRulesLookup class. */

TalkgroupRulesLookup::TalkgroupRulesLookup(const std::string& filename, uint32_t reloadTime, bool acl, bool verbose) : Thread(),
    m_rulesFile(filename),
    m_reloadTime(reloadTime),
    m_rules(),
    m_lastLoadTime(0U),
    m_acl(acl),
    m_stop(false),
    m_verbose(verbose),
    m_groupHangTime(5U),
    m_sendTalkgroups(false),
    m_groupVoice()
{
    /* stub */
}

/* Finalizes a instance of the TalkgroupRulesLookup class. */

TalkgroupRulesLookup::~TalkgroupRulesLookup() = default;

/* Thread entry point. This function is provided to run the thread for the lookup table. */

void TalkgroupRulesLookup::entry()
{
    if (m_reloadTime == 0U) {
        return;
    }

    Timer timer(1U, 60U * m_reloadTime);
    timer.start();

    while (!m_stop) {
        sleep(1000U);

        timer.clock();
        if (timer.hasExpired()) {
            load();
            timer.start();
        }
    }
}

/* Stops and unloads this lookup table. */

void TalkgroupRulesLookup::stop(bool noDestroy)
{
    if (m_reloadTime == 0U) {
        if (!noDestroy)
            delete this;
        return;
    }

    m_stop = true;

    wait();
}

/* Reads the lookup table from the specified lookup table file. */

bool TalkgroupRulesLookup::read()
{
    bool ret = load();

    if (m_reloadTime > 0U)
        run();
    setName("host:tg-lookup-tbl");

    return ret;
}

/* Clears all entries from the lookup table. */

void TalkgroupRulesLookup::clear()
{
    __LOCK_TABLE();

    m_groupVoice.clear();

    __UNLOCK_TABLE();
}

/* Adds a new entry to the lookup table by the specified unique ID. */

void TalkgroupRulesLookup::addEntry(uint32_t id, uint8_t slot, bool enabled, bool affiliated, bool nonPreferred,
    uint8_t strapping)
{
    TalkgroupRuleGroupVoiceSource source;
    TalkgroupRuleConfig config;
    source.tgId(id);
    source.tgSlot(slot);
    config.active(enabled);
    config.affiliated(affiliated);
    config.nonPreferred(nonPreferred);
    config.strapping(strapping);

    __LOCK_TABLE();

    auto it = std::find_if(m_groupVoice.begin(), m_groupVoice.end(),
        [&](TalkgroupRuleGroupVoice& x) {
            if (slot != 0U) {
                return x.source().tgId() == id && x.source().tgSlot() == slot;
            }

            return x.source().tgId() == id;
        });
    if (it != m_groupVoice.end()) {
        source = it->source();
        source.tgId(id);
        source.tgSlot(slot);
        
        config = it->config();
        config.active(enabled);
        config.affiliated(affiliated);
        config.nonPreferred(nonPreferred);
        config.strapping(strapping);

        TalkgroupRuleGroupVoice entry = *it;
        entry.config(config);
        entry.source(source);

        m_groupVoice[it - m_groupVoice.begin()] = entry;
    }
    else {
        TalkgroupRuleGroupVoice entry;
        entry.config(config);
        entry.source(source);

        m_groupVoice.push_back(entry);
    }

    __UNLOCK_TABLE();
}

/* Adds a new entry to the lookup table by the specified unique ID. */

void TalkgroupRulesLookup::addEntry(TalkgroupRuleGroupVoice groupVoice)
{
    if (groupVoice.isInvalid())
        return;

    TalkgroupRuleGroupVoice entry = groupVoice;
    uint32_t id = entry.source().tgId();
    uint8_t slot = entry.source().tgSlot();

    __LOCK_TABLE();

    auto it = std::find_if(m_groupVoice.begin(), m_groupVoice.end(),
        [&](TalkgroupRuleGroupVoice& x) {
            if (slot != 0U) {
                return x.source().tgId() == id && x.source().tgSlot() == slot;
            }

            return x.source().tgId() == id;
        });
    if (it != m_groupVoice.end()) {
        m_groupVoice[it - m_groupVoice.begin()] = entry;
    }
    else {
        m_groupVoice.push_back(entry);
    }

    __UNLOCK_TABLE();
}

/* Erases an existing entry from the lookup table by the specified unique ID. */

void TalkgroupRulesLookup::eraseEntry(uint32_t id, uint8_t slot)
{
    __LOCK_TABLE();

    auto it = std::find_if(m_groupVoice.begin(), m_groupVoice.end(),
        [&](TalkgroupRuleGroupVoice& x) {
            return x.source().tgId() == id && x.source().tgSlot() == slot;
        });
    if (it != m_groupVoice.end()) {
        m_groupVoice.erase(it);
    }

    __UNLOCK_TABLE();
}

/* Finds a table entry in this lookup table. */

TalkgroupRuleGroupVoice TalkgroupRulesLookup::find(uint32_t id, uint8_t slot)
{
    TalkgroupRuleGroupVoice entry;

    __SPINLOCK();

    std::lock_guard<std::mutex> lock(s_mutex);
    auto it = std::find_if(m_groupVoice.begin(), m_groupVoice.end(),
        [&](TalkgroupRuleGroupVoice& x)
        {
            if (slot != 0U) {
                return x.source().tgId() == id && x.source().tgSlot() == slot;
            }

            return x.source().tgId() == id;
        });
    if (it != m_groupVoice.end()) {
        entry = *it;
    } else {
        entry = TalkgroupRuleGroupVoice();
    }

    return entry;
}

/* Finds a table entry in this lookup table. */

TalkgroupRuleGroupVoice TalkgroupRulesLookup::findByRewrite(uint32_t peerId, uint32_t id, uint8_t slot)
{
    TalkgroupRuleGroupVoice entry;

    __SPINLOCK();

    std::lock_guard<std::mutex> lock(s_mutex);
    auto it = std::find_if(m_groupVoice.begin(), m_groupVoice.end(),
        [&](TalkgroupRuleGroupVoice& x) {
            if (x.config().rewrite().size() == 0)
                return false;

            std::vector<TalkgroupRuleRewrite> rewrite = x.config().rewrite();
            auto innerIt = std::find_if(rewrite.begin(), rewrite.end(),
                [&](TalkgroupRuleRewrite& y) {
                    if (slot != 0U) {
                        return y.peerId() == peerId && y.tgId() == id && y.tgSlot() == slot;
                    }

                    return y.peerId() == peerId && y.tgId() == id;
                });

            if (innerIt != rewrite.end())
                return true;
            return false;
        });
    if (it != m_groupVoice.end()) {
        entry = *it;
    } else {
        entry = TalkgroupRuleGroupVoice();
    }

    return entry;
}

/* Saves loaded talkgroup rules. */

bool TalkgroupRulesLookup::commit(bool quiet)
{
    return save(quiet);
}

/* Flag indicating whether talkgroup ID access control is enabled or not. */

bool TalkgroupRulesLookup::getACL()
{
    return m_acl;
}

// ---------------------------------------------------------------------------
//  Private Class Members
// ---------------------------------------------------------------------------

/* Loads the table from the passed lookup table file. */

bool TalkgroupRulesLookup::load()
{
    if (m_rulesFile.length() <= 0) {
        return false;
    }

    try {
        bool ret = yaml::Parse(m_rules, m_rulesFile.c_str());
        if (!ret) {
            LogError(LOG_HOST, "Cannot open the talkgroup rules lookup file - %s - error parsing YML", m_rulesFile.c_str());
            return false;
        }
    }
    catch (yaml::OperationException const& e) {
        LogError(LOG_HOST, "Cannot open the talkgroup rules lookup file - %s (%s)", m_rulesFile.c_str(), e.message());
        return false;
    }

    // clear table
    clear();

    __LOCK_TABLE();

    yaml::Node& groupVoiceList = m_rules["groupVoice"];

    if (groupVoiceList.size() == 0U) {
        ::LogError(LOG_HOST, "No group voice rules list defined!");
        s_locked = false;
        return false;
    }

    for (size_t i = 0; i < groupVoiceList.size(); i++) {
        TalkgroupRuleGroupVoice groupVoice = TalkgroupRuleGroupVoice(groupVoiceList[i]);
        m_groupVoice.push_back(groupVoice);

        if (m_verbose) {
            std::string groupName = groupVoice.name();
            uint32_t tgId = groupVoice.source().tgId();
            uint8_t tgSlot = groupVoice.source().tgSlot();
            bool active = groupVoice.config().active();
            bool parrot = groupVoice.config().parrot();
            bool affil = groupVoice.config().affiliated();
            uint8_t strapping = groupVoice.config().strapping();

            uint32_t incCount = groupVoice.config().inclusion().size();
            uint32_t excCount = groupVoice.config().exclusion().size();
            uint32_t rewrCount = groupVoice.config().rewrite().size();
            uint32_t alwyCount = groupVoice.config().alwaysSend().size();
            uint32_t prefCount = groupVoice.config().preferred().size();
            uint32_t permRIDCount = groupVoice.config().permittedRIDs().size();

            if (incCount > 0 && excCount > 0) {
                ::LogWarning(LOG_HOST, "Talkgroup (%s) defines both inclusions and exclusions! Inclusion rules take precedence and exclusion rules will be ignored.", groupName.c_str());
            }

            if (alwyCount > 0 && affil) {
                ::LogWarning(LOG_HOST, "Talkgroup (%s) is marked as affiliation required and has a defined always send list! Always send peers take rule precedence and defined peers will always receive traffic.", groupName.c_str());
            }

            ::LogInfoEx(LOG_HOST, "Talkgroup NAME: %s SRC_TGID: %u SRC_TS: %u ACTIVE: %u PARROT: %u AFFILIATED: %u STRAPPING: %u INCLUSIONS: %u EXCLUSIONS: %u REWRITES: %u ALWAYS: %u PREFERRED: %u PERMITTED RIDS: %u", groupName.c_str(), tgId, tgSlot, active, parrot, affil, strapping, incCount, excCount, rewrCount, alwyCount, prefCount, permRIDCount);
        }
    }

    __UNLOCK_TABLE();

    size_t size = m_groupVoice.size();
    if (size == 0U) {
        return false;
    }

    uint64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
    m_lastLoadTime = now;

    LogInfoEx(LOG_HOST, "Loaded %lu entries into talkgroup rules table", size);

    return true;
}

/* Saves the table to the passed lookup table file. */

bool TalkgroupRulesLookup::save(bool quiet)
{
    // Make sure file is valid
    if (m_rulesFile.length() <= 0) {
        return false;
    }

    std::lock_guard<std::mutex> lock(s_mutex);
    
    // New list for our new group voice rules
    yaml::Node groupVoiceList;
    yaml::Node newRules;

    for (auto entry : m_groupVoice) {
        yaml::Node& gv = groupVoiceList.push_back();
        entry.getYaml(gv);
        //LogDebugEx(LOG_HOST, "TalkgroupRulesLookup::save()", "Added TGID %s to yaml TG list", gv["name"].as<std::string>().c_str());
    }

    //LogDebugEx(LOG_HOST, "TalkgroupRulesLookup::save()", "Got final GroupVoiceList YAML size of %u", groupVoiceList.size());
    
    // Set the new rules
    newRules["groupVoice"] = groupVoiceList;

    // Make sure we actually did stuff right
    if (newRules["groupVoice"].size() != m_groupVoice.size()) {
        LogError(LOG_HOST, "Generated YAML node for group lists did not match loaded group size! (%u != %u)", newRules["groupVoice"].size(), m_groupVoice.size());
        return false;
    }

    try {
        if (!quiet)
            LogInfoEx(LOG_HOST, "Saving talkgroup rules file to %s", m_rulesFile.c_str());
        yaml::Serialize(newRules, m_rulesFile.c_str());
    }
    catch (yaml::OperationException const& e) {
        LogError(LOG_HOST, "Cannot save the talkgroup rules lookup file - %s (%s)", m_rulesFile.c_str(), e.message());
        return false;
    }

    return true;
}
