/**
 * Copyright - See the COPYRIGHT that is included with this distribution.
 * pvxs is distributed subject to a Software License Agreement found
 * in file LICENSE that is included with this distribution.
 */

#include <algorithm>
#include <vector>
#include <string>
#include <sstream>
#include <limits>
#include <cmath>

#include <dbDefs.h>
#include <osiSock.h>
#include <epicsMath.h>
#include <epicsStdlib.h>
#include <epicsString.h>

#include <pvxs/log.h>
#include "serverconn.h"
#include "clientimpl.h"
#include "utilpvt.h"
#include "evhelper.h"

DEFINE_LOGGER(serversetup, "pvxs.server.setup");
DEFINE_LOGGER(clientsetup, "pvxs.client.setup");
DEFINE_LOGGER(config, "pvxs.config");

namespace pvxs {

SockEndpoint::SockEndpoint(const char* ep, uint16_t defport)
{
    // <IP46>
    // <IP46>,<ttl#>
    // <IP46>@ifacename
    // <IP46>,<ttl#>@ifacename
    auto comma = strchr(ep, ',');
    auto at = strchr(ep, '@');

    if(comma && at && comma > at) {
        throw std::runtime_error(SB()<<'"'<<escape(ep)<<"\" comma expected before @");
    }

    if(!comma && !at) {
        addr.setAddress(ep, defport);

    } else { // comma || at
        auto firstsep = comma ? comma : at;
        addr.setAddress(std::string(ep, firstsep-ep), defport);

        if(comma && !at) {
            ttl = parseTo<int64_t>(comma+1);

        } else if(comma) {
            ttl = parseTo<int64_t>(std::string(comma+1, at-comma-1));
        }

        if(at)
            iface = at+1;
    }

    auto& ifmap = IfaceMap::instance();

    if(addr.family()==AF_INET6) {
        if(iface.empty() && addr->in6.sin6_scope_id) {
            // interface index provide with IPv6 address
            // we map back to symbolic name for storage
            iface = ifmap.name_of(addr->in6.sin6_scope_id);
        }
        addr->in6.sin6_scope_id = 0;

    } else if(addr.family()==AF_INET && addr.isMCast() && !iface.empty()) {
        SockAddr ifaddr(AF_INET);

        if(evutil_inet_pton(AF_INET, iface.c_str(), &ifaddr->in.sin_addr.s_addr)==1) {
            // map interface address to symbolic name

            iface = ifmap.name_of(ifaddr);
        }
    }

    if(!iface.empty() && !ifmap.index_of(iface)) {
        log_warn_printf(config, "Invalid interface address or name: \"%s\"\n", iface.c_str());
    }
}

MCastMembership SockEndpoint::resolve() const
{
    if(!addr.isMCast())
        throw std::logic_error("not mcast");

    auto& ifmap = IfaceMap::instance();

    MCastMembership m;
    m.af = addr.family();
    if(m.af==AF_INET) {
        auto& req = m.req.in;
        req.imr_multiaddr.s_addr = addr->in.sin_addr.s_addr;

        if(!iface.empty()) {
            auto iface = ifmap.address_of(this->iface);
            if(iface.family()==AF_INET) {
                req.imr_interface.s_addr = iface->in.sin_addr.s_addr;
            }
        }

    } else if(m.af==AF_INET6) {
        auto& req = m.req.in6;
        req.ipv6mr_multiaddr = addr->in6.sin6_addr;

        if(!iface.empty()) {
            req.ipv6mr_interface = ifmap.index_of(this->iface);
            if(!req.ipv6mr_interface) {
                log_warn_printf(config, "Unable to resolve interface '%s'\n", iface.c_str());
            }
        }

    } else {
        throw std::logic_error("Unsupported address family");
    }
    return m;
}

std::ostream& operator<<(std::ostream& strm, const SockEndpoint& addr)
{
    strm<<addr.addr;
    if(addr.addr.isMCast()) {
        if(addr.ttl)
            strm<<','<<addr.ttl;
        if(!addr.iface.empty())
            strm<<'@'<<addr.iface;
    }
    return strm;
}

bool operator==(const SockEndpoint& lhs, const SockEndpoint& rhs)
{
    return lhs.addr==rhs.addr && lhs.ttl==rhs.ttl && lhs.iface==rhs.iface;
}

namespace {

/* Historically pvAccessCPP used $EPICS_PVA_CONN_TMO as the period
 * between sending CMD_ECHO.  *::Config::tcpTimeout is the actual
 * inactivity timeout period.  Apply a scaling factor to add a
 * go from one to the other.
 */
constexpr double tmoScale = 4.0/3.0; // 40 second idle timeout / 30 configured

void split_addr_into(const char* name, std::vector<std::string>& out, const std::string& inp,
                     uint16_t defaultPort, bool required=false)
{
    size_t pos=0u;

    // parse, resolve host names, then re-print.
    // Catch syntax errors early, and normalize prior to removing duplicates
    while(pos<inp.size()) {
        auto start = inp.find_first_not_of(" \t\r\n", pos);
        auto end = inp.find_first_of(" \t\r\n", start);
        pos = end;

        if(start<end) {
            auto temp(inp.substr(start, end==std::string::npos ? end : end-start));
            try {
                SockEndpoint ep(temp);
                if(ep.addr.port()==0)
                    ep.addr.setPort(defaultPort);
                out.push_back(SB()<<ep);

            } catch(std::exception& e){
                if(required)
                    throw std::runtime_error(SB()<<"invalid endpoint \""<<temp<<"\" "<<e.what());
                log_err_printf(config, "%s ignoring invalid '%s' : %s\n", name, temp.c_str(), e.what());
            }
        }
    }

    // remove any duplicates
    std::sort(out.begin(), out.end());
    out.erase(std::unique(out.begin(), out.end()),
              out.end());
}

std::string join_addr(const std::vector<std::string>& in)
{
    std::ostringstream strm;
    bool first=true;
    for(auto& addr : in) {
        if(first)
            first = false;
        else
            strm<<' ';
        strm<<addr;
    }
    return strm.str();
}

void parse_bool(bool& dest, const std::string& name, const std::string& val)
{
    if(epicsStrCaseCmp(val.c_str(), "YES")==0 || val=="1") {
        dest = true;
    } else if(epicsStrCaseCmp(val.c_str(), "NO")==0 || val=="0") {
        dest = false;
    } else {
        log_err_printf(config, "%s invalid bool value (YES/NO) : '%s'\n",
                       name.c_str(), val.c_str());
    }
}

void parse_timeout(double& dest, const std::string& name, const std::string& val)
{
    double temp;
    try {
        temp = parseTo<double>(val);

        if(!std::isfinite(temp)
                || temp<0.0
                || temp>double(std::numeric_limits<time_t>::max()))
            throw std::out_of_range("Out of range");

        dest = temp*tmoScale;
    } catch(std::exception& e) {
        log_err_printf(serversetup, "%s invalid double value : '%s'\n",
                       name.c_str(), val.c_str());
    }
}

struct PickOne {
    const std::map<std::string, std::string>& defs;
    bool useenv;

    std::string name, val;

    bool operator()(std::initializer_list<const char*> names) {
        for(auto candidate : names) {
            if(useenv) {
                if(auto eval = getenv(candidate)) {
                    name = candidate;
                    val = eval;
                    return true;
                }

            } else {
                auto it = defs.find(candidate);
                if(it!=defs.end()) {
                    name = candidate;
                    val = it->second;
                    return true;
                }
            }
        }
        return false;
    }
};

std::vector<SockEndpoint> parseAddresses(const std::vector<std::string>& addrs, uint16_t defport=0)
{
    std::vector<SockEndpoint> ret;
    for(const auto& addr : addrs) {
        try {
            ret.emplace_back(addr, defport);
        }catch(std::runtime_error& e){
            log_warn_printf(config, "Ignoring %s : %s\n", addr.c_str(), e.what());
            continue;
        }
    }
    return ret;
}

void printAddresses(std::vector<std::string>& out, const std::vector<SockEndpoint>& inp)
{
    std::vector<std::string> temp;
    temp.reserve(inp.size());

    for(auto& addr : inp) {
        temp.emplace_back(SB()<<addr);
    }
    out = std::move(temp);
}

// Fill out address list by appending broadcast addresses
// of any and all local interface addresses already included
void expandAddrList(const std::vector<SockEndpoint>& ifaces,
                    std::vector<SockEndpoint>& addrs)
{
    SockAttach attach;
    evsocket dummy(AF_INET, SOCK_DGRAM, 0);

    for(auto& saddr : ifaces) {
        auto matchAddr = &saddr.addr;

        if(evsocket::ipstack==evsocket::Linsock && saddr.addr.family()==AF_INET6 && saddr.addr.isAny()) {
            // special case handling to match "promote" in server::Config::expand()
            // treat [::] as 0.0.0.0
            matchAddr = nullptr;

        } else if(saddr.addr.family()!=AF_INET) {
            continue;
        }

        for(auto& addr : dummy.broadcasts(matchAddr)) {
            addr.setPort(0u);
            addrs.emplace_back(addr);
        }
    }
}

void addGroups(std::vector<SockEndpoint>& ifaces,
               const std::vector<SockEndpoint>& addrs)
{
    auto& ifmap = IfaceMap::instance();
    std::set<std::string> allifaces;

    for(const auto& addr : addrs) {
        if(!addr.addr.isMCast())
            continue;

        if(!addr.iface.empty()) {
            // interface already specified
            ifaces.push_back(addr);

        } else {
            // no interface specified, treat as wildcard
            if(allifaces.empty())
                allifaces = ifmap.all_external();

            for(auto& iface : allifaces) {
                auto ifaceaddr(addr);
                ifaceaddr.iface = iface;
                ifaces.push_back(ifaceaddr);
            }
        }
    }
}

// remove duplicates while preserving order of first appearance
template<typename A>
void removeDups(std::vector<A>& addrs)
{
    std::sort(addrs.begin(), addrs.end());
    addrs.erase(std::unique(addrs.begin(), addrs.end()),
                addrs.end());
}

// special handling for SockEndpoint where duplication is based on
// address,interface.  Duplicates are combined with the longest TTL.
template<>
void removeDups(std::vector<SockEndpoint>& addrs)
{
    std::map<std::pair<SockAddr, std::string>, size_t> seen;
    for(size_t i=0; i<addrs.size(); ) {
        auto& ep = addrs[i];
        auto key = std::make_pair(ep.addr, ep.iface);
        auto it = seen.find(key);
        if(it==seen.end()) { // first sighting
            seen[key] = i++;

        } else { // duplicate
            auto& orig = addrs[it->second];

            if(ep.ttl > orig.ttl) { // w/ longer TTL
                orig.ttl = ep.ttl;
            }

            addrs.erase(addrs.begin()+i);
            // 'ep' and 'orig' are invalidated
        }
    }
}

void enforceTimeout(double& tmo)
{
    /* Inactivity timeouts with PVA have a long (and growing) history.
     *
     * - Originally pvAccessCPP clients didn't send CMD_ECHO, and servers would never timeout.
     * - Since module version 7.0.0 (in Base 7.0.3) clients send echo every 15 seconds, and
     *   either peer will timeout after 30 seconds of inactivity.
     * - pvAccessJava clients send CMD_ECHO every 30 seconds, and timeout after 60 seconds.
     *
     * So this was a bug, with c++ server timeout racing with Java client echo.
     *
     * - As a compromise, continue to send echo at least every 15 seconds,
     *   and increase default timeout to 40.
     */
    if(!std::isfinite(tmo) || tmo <= 0.0 || tmo >= double(std::numeric_limits<time_t>::max()))
        tmo = 40.0;
    else if(tmo < 2.0)
        tmo = 2.0;
}

} // namespace

namespace server {

static
void _fromDefs(Config& self, const std::map<std::string, std::string>& defs, bool useenv)
{
    PickOne pickone{defs, useenv};

    if(pickone({"EPICS_PVAS_SERVER_PORT", "EPICS_PVA_SERVER_PORT"})) {
        try {
            self.tcp_port = parseTo<uint64_t>(pickone.val);
        }catch(std::exception& e) {
            log_err_printf(serversetup, "%s invalid integer : %s", pickone.name.c_str(), e.what());
        }
    }

    if(pickone({"EPICS_PVAS_BROADCAST_PORT", "EPICS_PVA_BROADCAST_PORT"})) {
        try {
            self.udp_port = parseTo<uint64_t>(pickone.val);
        }catch(std::exception& e) {
            log_err_printf(serversetup, "%s invalid integer : %s", pickone.name.c_str(), e.what());
        }
    }

    if(pickone({"EPICS_PVAS_INTF_ADDR_LIST"})) {
        split_addr_into(pickone.name.c_str(), self.interfaces, pickone.val, self.tcp_port, true);
    }

    if(pickone({"EPICS_PVAS_IGNORE_ADDR_LIST"})) {
        split_addr_into(pickone.name.c_str(), self.ignoreAddrs, pickone.val, 0, true);
    }

    if(pickone({"EPICS_PVAS_BEACON_ADDR_LIST", "EPICS_PVA_ADDR_LIST"})) {
        split_addr_into(pickone.name.c_str(), self.beaconDestinations, pickone.val, self.udp_port);
    }

    if(pickone({"EPICS_PVAS_AUTO_BEACON_ADDR_LIST", "EPICS_PVA_AUTO_ADDR_LIST"})) {
        parse_bool(self.auto_beacon, pickone.name, pickone.val);
    }

    if(pickone({"EPICS_PVA_CONN_TMO"})) {
        parse_timeout(self.tcpTimeout, pickone.name, pickone.val);
    }
}

Config& Config::applyEnv()
{
    _fromDefs(*this, std::map<std::string, std::string>(), true);
    return *this;
}

Config Config::isolated(int family)
{
    Config ret;

    ret.udp_port = 0u;
    ret.tcp_port = 0u;
    ret.auto_beacon = false;
    switch(family) {
    case AF_INET:
        ret.interfaces.emplace_back("127.0.0.1");
        ret.beaconDestinations.emplace_back("127.0.0.1");
        break;
    case AF_INET6:
        ret.interfaces.emplace_back("::1");
        ret.beaconDestinations.emplace_back("::1");
        break;
    default:
        throw std::logic_error(SB()<<"Unsupported address family "<<family);
    }

    return ret;
}

Config& Config::applyDefs(const std::map<std::string, std::string>& defs)
{
    _fromDefs(*this, defs, false);
    return *this;
}

void Config::updateDefs(defs_t& defs) const
{
    defs["EPICS_PVA_BROADCAST_PORT"] = defs["EPICS_PVAS_BROADCAST_PORT"] = SB()<<udp_port;
    defs["EPICS_PVA_SERVER_PORT"]    = defs["EPICS_PVAS_SERVER_PORT"]    = SB()<<tcp_port;
    defs["EPICS_PVA_AUTO_ADDR_LIST"] = defs["EPICS_PVAS_AUTO_BEACON_ADDR_LIST"] = auto_beacon ? "YES" : "NO";
    defs["EPICS_PVA_ADDR_LIST"]      = defs["EPICS_PVAS_BEACON_ADDR_LIST"] = join_addr(beaconDestinations);
    defs["EPICS_PVA_INTF_ADDR_LIST"] = defs["EPICS_PVAS_INTF_ADDR_LIST"]   = join_addr(interfaces);
    defs["EPICS_PVAS_IGNORE_ADDR_LIST"]   = join_addr(ignoreAddrs);
    defs["EPICS_PVA_CONN_TMO"] = SB()<<tcpTimeout/tmoScale;
}

void Config::expand()
{
    if(tcp_port==0)
        tcp_port = 5075;

    auto ifaces(parseAddresses(interfaces));
    auto bdest(parseAddresses(beaconDestinations));

    // empty interface address list implies the wildcard
    // (because no addresses isn't interesting...)
    if(ifaces.empty()) {
        ifaces.emplace_back(SockAddr::any(AF_INET));
    }

    auto& ifmap = IfaceMap::instance();

    for(size_t i=0; i<ifaces.size(); i++) {
        auto& ep = ifaces[i];

        if(evsocket::canIPv6 && ep.addr.isAny()) {
            // special handling for IP4/6 wildcard addresses

            if(evsocket::ipstack==evsocket::Linsock && ep.addr.family()==AF_INET) {
                // Linux IP stack disallows binding both 0.0.0.0 and [::] for the same port.
                // so promote to IPv6 when possible
                ep.addr = SockAddr::any(AF_INET6, ep.addr.port());
                log_debug_printf(serversetup, "Promote 0.0.0.0 -> [::]%s", "\n");

            } else if(evsocket::ipstack!=evsocket::Linsock) {
                /* Other IP stacks allow binding different sockets.
                 * OSX has the added oddity of ordering dependence.
                 * 0.0.0.0 and then :: is allowed, but not the reverse.
                 *
                 * So when possible, we always bind both in the allowed order.
                 */
                ep.addr = SockAddr::any(AF_INET, ep.addr.port());
                ifaces.emplace(ifaces.begin()+i+1u,
                               SockAddr::any(AF_INET6, ep.addr.port()));
                i++; // continue after newly inserted EP
            }

        } else if(!ep.addr.isMCast()) {
            // no-op

        } else if(!ep.iface.empty()) {
            ifaces.emplace_back(ifmap.address_of(ep.iface));
        } else {
            ifaces.emplace_back(SockAddr::any(ep.addr.family()));
        }
        // ep invalidated by emplace()
    }

    if(auto_beacon) {
        // use interface list add ipv4 broadcast addresses to beaconDestinations.
        // 0.0.0.0 -> adds all bcasts
        // otherwise add bcast for each iface address
        expandAddrList(ifaces, bdest);
        addGroups(ifaces, bdest);
        auto_beacon = false;
    }

    removeDups(ifaces);
    printAddresses(interfaces, ifaces);
    removeDups(bdest);
    printAddresses(beaconDestinations, bdest);
    removeDups(ignoreAddrs);

    enforceTimeout(tcpTimeout);

}

std::ostream& operator<<(std::ostream& strm, const Config& conf)
{
    auto showAddrs = [&strm](const char* var, const std::vector<std::string>& addrs) {
        strm<<indent{}<<var<<"=\"";
        bool first = true;
        for(auto& iface : addrs) {
            if(first)
                first = false;
            else
                strm<<' ';
            strm<<iface;
        }
        strm<<"\"\n";
    };

    showAddrs("EPICS_PVAS_INTF_ADDR_LIST", conf.interfaces);
    showAddrs("EPICS_PVAS_BEACON_ADDR_LIST", conf.beaconDestinations);
    showAddrs("EPICS_PVAS_IGNORE_ADDR_LIST", conf.ignoreAddrs);

    strm<<indent{}<<"EPICS_PVAS_AUTO_BEACON_ADDR_LIST="<<(conf.auto_beacon?"YES":"NO")<<'\n';

    strm<<indent{}<<"EPICS_PVAS_SERVER_PORT="<<conf.tcp_port<<'\n';

    strm<<indent{}<<"EPICS_PVAS_BROADCAST_PORT="<<conf.udp_port<<'\n';

    strm<<indent{}<<"EPICS_PVA_CONN_TMO="<<conf.tcpTimeout/tmoScale<<'\n';

    return strm;
}

} // namespace server

namespace client {

static
void _fromDefs(Config& self, const std::map<std::string, std::string>& defs, bool useenv)
{
    PickOne pickone{defs, useenv};

    if(pickone({"EPICS_PVA_BROADCAST_PORT"})) {
        try {
            self.udp_port = parseTo<uint64_t>(pickone.val);
        }catch(std::exception& e) {
            log_warn_printf(clientsetup, "%s invalid integer : %s", pickone.name.c_str(), e.what());
        }
    }
    if(self.udp_port==0u) {
        log_warn_printf(clientsetup, "ignoring EPICS_PVA_BROADCAST_PORT=%d\n", 0);
        self.udp_port = 5076;
    }

    if(pickone({"EPICS_PVA_SERVER_PORT", "EPICS_PVAS_SERVER_PORT"})) {
        try {
            self.tcp_port = parseTo<uint64_t>(pickone.val);
        }catch(std::exception& e) {
            log_warn_printf(clientsetup, "%s invalid integer : %s", pickone.name.c_str(), e.what());
        }
    }
    if(self.tcp_port==0u && !self.nameServers.empty()) {
        log_warn_printf(clientsetup, "ignoring EPICS_PVA_SERVER_PORT=%d\n", 0);
        self.tcp_port = 5075;
    }

    if(pickone({"EPICS_PVA_ADDR_LIST"})) {
        split_addr_into(pickone.name.c_str(), self.addressList, pickone.val, self.udp_port);
    }

    if(pickone({"EPICS_PVA_NAME_SERVERS"})) {
        split_addr_into(pickone.name.c_str(), self.nameServers, pickone.val, self.tcp_port);
    }

    if(pickone({"EPICS_PVA_AUTO_ADDR_LIST"})) {
        parse_bool(self.autoAddrList, pickone.name, pickone.val);
    }

    if(pickone({"EPICS_PVA_INTF_ADDR_LIST"})) {
        split_addr_into(pickone.name.c_str(), self.interfaces, pickone.val, 0);
    }

    if(pickone({"EPICS_PVA_CONN_TMO"})) {
        parse_timeout(self.tcpTimeout, pickone.name, pickone.val);
    }
}

Config& Config::applyEnv()
{
    _fromDefs(*this, std::map<std::string, std::string>(), true);
    return *this;
}

Config& Config::applyDefs(const std::map<std::string, std::string>& defs)
{
    _fromDefs(*this, defs, false);
    return *this;
}

void Config::updateDefs(defs_t& defs) const
{
    defs["EPICS_PVA_BROADCAST_PORT"] = SB()<<udp_port;
    defs["EPICS_PVA_SERVER_PORT"] = SB()<<tcp_port;
    defs["EPICS_PVA_AUTO_ADDR_LIST"] = autoAddrList ? "YES" : "NO";
    defs["EPICS_PVA_ADDR_LIST"] = join_addr(addressList);
    defs["EPICS_PVA_INTF_ADDR_LIST"] = join_addr(interfaces);
    defs["EPICS_PVA_CONN_TMO"] = SB()<<tcpTimeout/tmoScale;
}

void Config::expand()
{
    if(udp_port==0)
        throw std::runtime_error("Client can't use UDP random port");

    if(tcp_port==0)
        tcp_port = 5075;

    auto ifaces(parseAddresses(interfaces));
    auto addrs(parseAddresses(addressList));

    if(ifaces.empty())
        ifaces.emplace_back(SockAddr::any(AF_INET));

    if(autoAddrList) {
        expandAddrList(ifaces, addrs);
        addGroups(ifaces, addrs);
        autoAddrList = false;
    }

    printAddresses(interfaces, ifaces);
    removeDups(addrs);
    printAddresses(addressList, addrs);

    enforceTimeout(tcpTimeout);
}

std::ostream& operator<<(std::ostream& strm, const Config& conf)
{
    bool first;

    strm<<indent{}<<"EPICS_PVA_ADDR_LIST=\"";
    first = true;
    for(auto& iface : conf.addressList) {
        if(first)
            first = false;
        else
            strm<<' ';
        strm<<iface;
    }
    strm<<"\"\n";

    strm<<indent{}<<"EPICS_PVA_AUTO_ADDR_LIST="<<(conf.autoAddrList?"YES":"NO")<<'\n';

    strm<<indent{}<<"EPICS_PVA_BROADCAST_PORT="<<conf.udp_port<<'\n';

    strm<<indent{}<<"EPICS_PVA_SERVER_PORT="<<conf.tcp_port<<'\n';

    strm<<indent{}<<"EPICS_PVA_CONN_TMO="<<conf.tcpTimeout/tmoScale<<'\n';

    return strm;
}

} // namespace client

} // namespace pvxs
