// Copyright (C) 2012-2025 Internet Systems Consortium, Inc. ("ISC")
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

#include <config.h>

#include <dhcp/libdhcp++.h>
#include <dhcp/option_custom.h>
#include <dhcpsrv/cfgmgr.h>
#include <dhcpsrv/dhcpsrv_log.h>
#include <dhcpsrv/lease_mgr.h>
#include <exceptions/exceptions.h>
#include <stats/stats_mgr.h>
#include <util/encode/encode.h>
#include <util/str.h>

#include <boost/algorithm/string.hpp>

#include <algorithm>
#include <iostream>
#include <iterator>
#include <limits>
#include <map>
#include <sstream>
#include <string>

#include <time.h>

using namespace isc::asiolink;
using namespace isc::data;
using namespace isc::db;
using namespace isc::dhcp;
using namespace isc::util;
using namespace std;

namespace isc {
namespace dhcp {

LeasePageSize::LeasePageSize(const size_t page_size)
    : page_size_(page_size) {

    if (page_size_ == 0) {
        isc_throw(OutOfRange, "page size of retrieved leases must not be 0");
    }

    if (page_size_ > std::numeric_limits<uint32_t>::max()) {
        isc_throw(OutOfRange, "page size of retrieved leases must not be greater than "
                  << std::numeric_limits<uint32_t>::max());
    }
}

Lease6Ptr
LeaseMgr::getLease6(Lease::Type type, const DUID& duid,
                    uint32_t iaid, SubnetID subnet_id) const {
    Lease6Collection col = getLeases6(type, duid, iaid, subnet_id);

    if (col.size() > 1) {
        isc_throw(MultipleRecords, "More than one lease found for type "
                  << static_cast<int>(type) << ", duid "
                  << duid.toText() << ", iaid " << iaid
                  << " and subnet-id " << subnet_id);
    }
    if (col.empty()) {
        return (Lease6Ptr());
    }
    return (*col.begin());
}

void
LeaseMgr::recountLeaseStats4() {
    using namespace stats;

    StatsMgr& stats_mgr = StatsMgr::instance();

    LeaseStatsQueryPtr query = startLeaseStatsQuery4();
    if (!query) {
        /// NULL means not backend does not support recounting.
        return;
    }

    // Zero out the global stats.
    // Cumulative counters ("reclaimed-declined-addresses", "reclaimed-leases",
    // "cumulative-assigned-addresses") never get zeroed.
    int64_t zero = 0;
    stats_mgr.setValue("declined-addresses", zero);

    // Create if it does not exit reclaimed declined leases global stats.
    if (!stats_mgr.getObservation("reclaimed-declined-addresses")) {
        stats_mgr.setValue("reclaimed-declined-addresses", zero);
    }

    // Create if it does not exit reclaimed leases global stats.
    if (!stats_mgr.getObservation("reclaimed-leases")) {
        stats_mgr.setValue("reclaimed-leases", zero);
    }

    // Create if it does not exit cumulative global stats.
    if (!stats_mgr.getObservation("cumulative-assigned-addresses")) {
        stats_mgr.setValue("cumulative-assigned-addresses", zero);
    }

    // Clear subnet level stats.  This ensures we don't end up with corner
    // cases that leave stale values in place.
    const Subnet4Collection* subnets =
        CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->getAll();

    for (auto const& subnet : *subnets) {
        SubnetID subnet_id = subnet->getID();
        stats_mgr.setValue(StatsMgr::generateName("subnet", subnet_id,
                                                  "assigned-addresses"),
                           zero);

        stats_mgr.setValue(StatsMgr::generateName("subnet", subnet_id,
                                                  "declined-addresses"),
                           zero);

        const std::string name_rec_dec(StatsMgr::generateName("subnet", subnet_id,
                                                              "reclaimed-declined-addresses"));
        if (!stats_mgr.getObservation(name_rec_dec)) {
            stats_mgr.setValue(name_rec_dec, zero);
        }

        const std::string name_rec(StatsMgr::generateName("subnet", subnet_id,
                                                          "reclaimed-leases"));
        if (!stats_mgr.getObservation(name_rec)) {
            stats_mgr.setValue(name_rec, zero);
        }

        for (auto const& pool : subnet->getPools(Lease::TYPE_V4)) {
            const std::string name_aa(StatsMgr::generateName("subnet", subnet_id,
                                                             StatsMgr::generateName("pool", pool->getID(),
                                                                                    "assigned-addresses")));
            if (!stats_mgr.getObservation(name_aa)) {
                stats_mgr.setValue(name_aa, zero);
            }

            const std::string& name_da(StatsMgr::generateName("subnet", subnet_id,
                                                              StatsMgr::generateName("pool", pool->getID(),
                                                                                     "declined-addresses")));
            if (!stats_mgr.getObservation(name_da)) {
                stats_mgr.setValue(name_da, zero);
            }

            const std::string& pname_rec_dec(StatsMgr::generateName("subnet", subnet_id,
                                                                    StatsMgr::generateName("pool", pool->getID(),
                                                                                          "reclaimed-declined-addresses")));
            if (!stats_mgr.getObservation(pname_rec_dec)) {
                stats_mgr.setValue(pname_rec_dec, zero);
            }

            const std::string& pname_rec(StatsMgr::generateName("subnet", subnet_id,
                                                                StatsMgr::generateName("pool", pool->getID(),
                                                                                      "reclaimed-leases")));
            if (!stats_mgr.getObservation(pname_rec)) {
                stats_mgr.setValue(pname_rec, zero);
            }
        }
    }

    // Get counts per state per subnet. Iterate over the result set
    // updating the subnet and global values.
    LeaseStatsRow row;
    while (query->getNextRow(row)) {
        if (row.lease_state_ == Lease::STATE_DEFAULT) {
            // Add to subnet level value.
            stats_mgr.addValue(StatsMgr::generateName("subnet", row.subnet_id_,
                                                      "assigned-addresses"),
                               row.state_count_);
        } else if (row.lease_state_ == Lease::STATE_DECLINED) {
            // Set subnet level value.
            stats_mgr.setValue(StatsMgr::generateName("subnet", row.subnet_id_,
                                                      "declined-addresses"),
                               row.state_count_);

            // Add to the global value.
            stats_mgr.addValue("declined-addresses", row.state_count_);

            // Add to subnet level value.
            // Declined leases also count as assigned.
            stats_mgr.addValue(StatsMgr::generateName("subnet", row.subnet_id_,
                                                      "assigned-addresses"),
                               row.state_count_);
        }
    }

    query = startPoolLeaseStatsQuery4();
    if (!query) {
        /// NULL means not backend does not support recounting.
        return;
    }

    // Get counts per state per subnet and pool. Iterate over the result set
    // updating the subnet and pool and global values.
    while (query->getNextRow(row)) {
        if (row.lease_state_ == Lease::STATE_DEFAULT) {
            // Add to subnet and pool level value.
            stats_mgr.addValue(StatsMgr::generateName("subnet", row.subnet_id_,
                                                      StatsMgr::generateName("pool", row.pool_id_,
                                                      "assigned-addresses")),
                               row.state_count_);
        } else if (row.lease_state_ == Lease::STATE_DECLINED) {
            // Set subnet and pool level value.
            stats_mgr.setValue(StatsMgr::generateName("subnet", row.subnet_id_,
                                                      StatsMgr::generateName("pool", row.pool_id_,
                                                      "declined-addresses")),
                               row.state_count_);

            // Add to subnet and pool level value.
            // Declined leases also count as assigned.
            stats_mgr.addValue(StatsMgr::generateName("subnet", row.subnet_id_,
                                                      StatsMgr::generateName("pool", row.pool_id_,
                                                      "assigned-addresses")),
                               row.state_count_);
        }
    }
}

LeaseStatsQuery::LeaseStatsQuery(const SelectMode& select_mode)
    : first_subnet_id_(0), last_subnet_id_(0), select_mode_(select_mode) {
    if (select_mode != ALL_SUBNETS && select_mode != ALL_SUBNET_POOLS) {
        isc_throw(BadValue, "LeaseStatsQuery: mode must be either ALL_SUBNETS or ALL_SUBNET_POOLS");
    }
}

LeaseStatsQuery::LeaseStatsQuery(const SubnetID& subnet_id)
    : first_subnet_id_(subnet_id), last_subnet_id_(0),
    select_mode_(SINGLE_SUBNET) {

    if (first_subnet_id_ == 0) {
        isc_throw(BadValue, "LeaseStatsQuery: subnet_id_ must be > 0");
    }
}

LeaseStatsQuery::LeaseStatsQuery(const SubnetID& first_subnet_id,
                                 const SubnetID& last_subnet_id)
    : first_subnet_id_(first_subnet_id), last_subnet_id_(last_subnet_id),
    select_mode_(SUBNET_RANGE) {

    if (first_subnet_id_ == 0) {
        isc_throw(BadValue, "LeaseStatsQuery: first_subnet_id_ must be > 0");
    }

    if (last_subnet_id_ == 0) {
        isc_throw(BadValue, "LeaseStatsQuery: last_subnet_id_ must be > 0");
    }

    if (last_subnet_id_ <= first_subnet_id_) {
        isc_throw(BadValue,
                  "LeaseStatsQuery: last_subnet_id_must be > first_subnet_id_");
    }
}

LeaseStatsQueryPtr
LeaseMgr::startLeaseStatsQuery4() {
    return(LeaseStatsQueryPtr());
}

LeaseStatsQueryPtr
LeaseMgr::startPoolLeaseStatsQuery4() {
    return(LeaseStatsQueryPtr());
}

LeaseStatsQueryPtr
LeaseMgr::startSubnetLeaseStatsQuery4(const SubnetID& /* subnet_id */) {
    return(LeaseStatsQueryPtr());
}

LeaseStatsQueryPtr
LeaseMgr::startSubnetRangeLeaseStatsQuery4(const SubnetID& /* first_subnet_id */,
                                           const SubnetID& /* last_subnet_id */) {
    return(LeaseStatsQueryPtr());
}

bool
LeaseStatsQuery::getNextRow(LeaseStatsRow& /*row*/) {
    return (false);
}

void
LeaseMgr::recountLeaseStats6() {
    using namespace stats;

    StatsMgr& stats_mgr = StatsMgr::instance();

    LeaseStatsQueryPtr query = startLeaseStatsQuery6();
    if (!query) {
        /// NULL means not backend does not support recounting.
        return;
    }

    // Zero out the global stats.
    // Cumulative counters ("reclaimed-declined-addresses", "reclaimed-leases",
    // "cumulative-assigned-nas", "cumulative-assigned-pds",
    // "cumulative-registered-nas")) never get zeroed.
    int64_t zero = 0;
    stats_mgr.setValue("declined-addresses", zero);

    if (!stats_mgr.getObservation("reclaimed-declined-addresses")) {
        stats_mgr.setValue("reclaimed-declined-addresses", zero);
    }

    if (!stats_mgr.getObservation("reclaimed-leases")) {
        stats_mgr.setValue("reclaimed-leases", zero);
    }

    // Create if it does not exit cumulative nas global stats.
    if (!stats_mgr.getObservation("cumulative-assigned-nas")) {
        stats_mgr.setValue("cumulative-assigned-nas", zero);
    }

    // Create if it does not exit cumulative pds global stats.
    if (!stats_mgr.getObservation("cumulative-assigned-pds")) {
        stats_mgr.setValue("cumulative-assigned-pds", zero);
    }

    // Create if it does not exit cumulative registered nas global stats.
    if (!stats_mgr.getObservation("cumulative-registered-nas")) {
        stats_mgr.setValue("cumulative-registered-nas", zero);
    }

    // Clear subnet level stats.  This ensures we don't end up with corner
    // cases that leave stale values in place.
    const Subnet6Collection* subnets =
        CfgMgr::instance().getCurrentCfg()->getCfgSubnets6()->getAll();

    for (auto const& subnet : *subnets) {
        SubnetID subnet_id = subnet->getID();
        stats_mgr.setValue(StatsMgr::generateName("subnet", subnet_id,
                                                  "assigned-nas"),
                           zero);

        stats_mgr.setValue(StatsMgr::generateName("subnet", subnet_id,
                                                  "assigned-pds"),
                           zero);

        stats_mgr.setValue(StatsMgr::generateName("subnet", subnet_id,
                                                  "declined-addresses"),
                           zero);

        if (!stats_mgr.getObservation(
                StatsMgr::generateName("subnet", subnet_id,
                                       "reclaimed-declined-addresses"))) {
            stats_mgr.setValue(
                StatsMgr::generateName("subnet", subnet_id,
                                       "reclaimed-declined-addresses"),
                zero);
        }

        if (!stats_mgr.getObservation(
                StatsMgr::generateName("subnet", subnet_id,
                                       "reclaimed-leases"))) {
            stats_mgr.setValue(
                StatsMgr::generateName("subnet", subnet_id,
                                       "reclaimed-leases"),
                zero);
        }

        if (!stats_mgr.getObservation(
                StatsMgr::generateName("subnet", subnet_id,
                                       "registered-nas"))) {
            stats_mgr.setValue(
                StatsMgr::generateName("subnet", subnet_id,
                                       "registered-nas"),
                zero);
        }

        for (auto const& pool : subnet->getPools(Lease::TYPE_NA)) {
            const std::string& name_anas(StatsMgr::generateName("subnet", subnet_id,
                                                                StatsMgr::generateName("pool", pool->getID(),
                                                                                       "assigned-nas")));
            if (!stats_mgr.getObservation(name_anas)) {
                stats_mgr.setValue(name_anas, zero);
            }

            const std::string& name_da(StatsMgr::generateName("subnet", subnet_id,
                                                              StatsMgr::generateName("pool", pool->getID(),
                                                                                     "declined-addresses")));
            if (!stats_mgr.getObservation(name_da)) {
                stats_mgr.setValue(name_da, zero);
            }

            const std::string name_rec_dec(StatsMgr::generateName("subnet", subnet_id,
                                                                  StatsMgr::generateName("pool", pool->getID(),
                                                                                         "reclaimed-declined-addresses")));
            if (!stats_mgr.getObservation(name_rec_dec)) {
                stats_mgr.setValue(name_rec_dec, zero);
            }

            const std::string& name_rec(StatsMgr::generateName("subnet", subnet_id,
                                                               StatsMgr::generateName("pool", pool->getID(),
                                                                                      "reclaimed-leases")));
            if (!stats_mgr.getObservation(name_rec)) {
                stats_mgr.setValue(name_rec, zero);
            }
        }

        for (auto const& pool : subnet->getPools(Lease::TYPE_PD)) {
            const std::string& name_apds(StatsMgr::generateName("subnet", subnet_id,
                                                                StatsMgr::generateName("pd-pool", pool->getID(),
                                                                                       "assigned-pds")));
            if (!stats_mgr.getObservation(name_apds)) {
                stats_mgr.setValue(name_apds, zero);
            }

            const std::string& name_rec(StatsMgr::generateName("subnet", subnet_id,
                                                               StatsMgr::generateName("pd-pool", pool->getID(),
                                                                                      "reclaimed-leases")));
            if (!stats_mgr.getObservation(name_rec)) {
                stats_mgr.setValue(name_rec, zero);
            }
        }
    }

    // Get counts per state per subnet. Iterate over the result set
    // updating the subnet and global values.
    LeaseStatsRow row;
    while (query->getNextRow(row)) {
        switch(row.lease_type_) {
            case Lease::TYPE_NA:
                if (row.lease_state_ == Lease::STATE_DEFAULT) {
                    // Add to subnet level value.
                    stats_mgr.addValue(StatsMgr::generateName("subnet", row.subnet_id_,
                                                              "assigned-nas"),
                                       row.state_count_);
                } else if (row.lease_state_ == Lease::STATE_DECLINED) {
                    // Set subnet level value.
                    stats_mgr.setValue(StatsMgr::generateName("subnet", row.subnet_id_,
                                                              "declined-addresses"),
                                       row.state_count_);

                    // Add to the global value.
                    stats_mgr.addValue("declined-addresses", row.state_count_);

                    // Add to subnet level value.
                    // Declined leases also count as assigned.
                    stats_mgr.addValue(StatsMgr::generateName("subnet", row.subnet_id_,
                                                              "assigned-nas"),
                                       row.state_count_);
                } else if (row.lease_state_ == Lease::STATE_REGISTERED) {
                    // Add to subnet level value
                    stats_mgr.addValue(StatsMgr::generateName("subnet", row.subnet_id_,
                                                              "registered-nas"),
                                       row.state_count_);
                }
                break;

            case Lease::TYPE_PD:
                if (row.lease_state_ == Lease::STATE_DEFAULT) {
                    // Set subnet level value.
                    stats_mgr.setValue(StatsMgr::generateName("subnet", row.subnet_id_,
                                                              "assigned-pds"),
                                       row.state_count_);
                }
                break;

            default:
                // We don't support TYPE_TAs yet
                break;
        }
    }

    query = startPoolLeaseStatsQuery6();
    if (!query) {
        /// NULL means not backend does not support recounting.
        return;
    }

    // Get counts per state per subnet and pool. Iterate over the result set
    // updating the subnet and pool and global values.
    while (query->getNextRow(row)) {
        switch(row.lease_type_) {
            case Lease::TYPE_NA:
                if (row.lease_state_ == Lease::STATE_DEFAULT) {
                    // Add to subnet and pool level value.
                    stats_mgr.addValue(StatsMgr::generateName("subnet", row.subnet_id_,
                                                              StatsMgr::generateName("pool", row.pool_id_,
                                                              "assigned-nas")),
                                       row.state_count_);
                } else if (row.lease_state_ == Lease::STATE_DECLINED) {
                    // Set subnet and pool level value.
                    stats_mgr.setValue(StatsMgr::generateName("subnet", row.subnet_id_,
                                                              StatsMgr::generateName("pool", row.pool_id_,
                                                              "declined-addresses")),
                                       row.state_count_);

                    // Add to subnet and pool level value.
                    // Declined leases also count as assigned.
                    stats_mgr.addValue(StatsMgr::generateName("subnet", row.subnet_id_,
                                                              StatsMgr::generateName("pool", row.pool_id_,
                                                              "assigned-nas")),
                                       row.state_count_);
                }
                break;

            case Lease::TYPE_PD:
                if (row.lease_state_ == Lease::STATE_DEFAULT) {
                    // Set subnet and pool level value.
                    stats_mgr.setValue(StatsMgr::generateName("subnet", row.subnet_id_,
                                                              StatsMgr::generateName("pd-pool", row.pool_id_,
                                                              "assigned-pds")),
                                       row.state_count_);
                }
                break;

            default:
                // We don't support TYPE_TAs yet
                break;
        }
    }
}

LeaseStatsQueryPtr
LeaseMgr::startLeaseStatsQuery6() {
    return(LeaseStatsQueryPtr());
}

LeaseStatsQueryPtr
LeaseMgr::startPoolLeaseStatsQuery6() {
    return(LeaseStatsQueryPtr());
}

LeaseStatsQueryPtr
LeaseMgr::startSubnetLeaseStatsQuery6(const SubnetID& /* subnet_id */) {
    return(LeaseStatsQueryPtr());
}

LeaseStatsQueryPtr
LeaseMgr::startSubnetRangeLeaseStatsQuery6(const SubnetID& /* first_subnet_id */,
                                           const SubnetID& /* last_subnet_id */) {
    return(LeaseStatsQueryPtr());
}

std::string
LeaseMgr::getDBVersion() {
    isc_throw(NotImplemented, "LeaseMgr::getDBVersion() called");
}

void
LeaseMgr::setExtendedInfoTablesEnabled(const DatabaseConnection::ParameterMap& parameters) {
    std::string extended_info_tables;
    try {
        extended_info_tables = parameters.at("extended-info-tables");
    } catch (const exception&) {
        extended_info_tables = "false";
    }
    // If extended_info_tables is 'true' we will enable them.
    if (extended_info_tables == "true") {
        setExtendedInfoTablesEnabled(true);
    }
}

bool
LeaseMgr::upgradeLease4ExtendedInfo(const Lease4Ptr& lease,
                                    CfgConsistency::ExtendedInfoSanity check) {
    const OptionDefinition& rai_def = LibDHCP::DHO_DHCP_AGENT_OPTIONS_DEF();

    bool changed = false;
    if (!lease) {
        return (changed);
    }

    if (check == CfgConsistency::EXTENDED_INFO_CHECK_NONE) {
        return (changed);
    }

    ConstElementPtr user_context = lease->getContext();
    if (!user_context) {
        return (changed);
    }

    ConstElementPtr isc;
    ConstElementPtr extended_info;
    ElementPtr mutable_user_context;
    ElementPtr mutable_isc;
    string verifying = "";
    bool removed_extended_info = false;

    try {
        verifying = "user context";
        if (user_context->getType() != Element::map) {
            isc_throw(BadValue, "user context is not a map");
        }
        if (user_context->empty()) {
            changed = true;
            lease->setContext(ConstElementPtr());
            return (changed);
        }

        verifying = "isc";
        isc = user_context->get("ISC");
        if (!isc) {
            return (changed);
        }
        mutable_user_context =
            boost::const_pointer_cast<Element>(user_context);
        if (!mutable_user_context) {
            // Should not happen...
            mutable_user_context = copy(user_context, 0);
            lease->setContext(mutable_user_context);
        }

        if (isc->getType() != Element::map) {
            isc_throw(BadValue, "ISC entry is not a map");
        }
        if (isc->empty()) {
            changed = true;
            mutable_user_context->remove("ISC");
            if (mutable_user_context->empty()) {
                lease->setContext(ConstElementPtr());
            }
            return (changed);
        }

        verifying = "relay-agent-info";
        extended_info = isc->get("relay-agent-info");
        if (!extended_info) {
            return (changed);
        }
        mutable_isc = boost::const_pointer_cast<Element>(isc);
        if (!mutable_isc) {
            // Should not happen...
            mutable_isc = copy(isc, 0);
            mutable_user_context->set("ISC", mutable_isc);
        }

        if (extended_info->getType() == Element::string) {
            // Upgrade
            changed = true;
            ElementPtr upgraded = Element::createMap();
            upgraded->set("sub-options", extended_info);
            mutable_isc->set("relay-agent-info", upgraded);

            // Try to decode sub-options.
            verifying = "rai";
            string rai_hex = extended_info->stringValue();
            vector<uint8_t> rai_data;
            str::decodeFormattedHexString(rai_hex, rai_data);
            OptionCustomPtr rai(new OptionCustom(rai_def, Option::V4, rai_data));
            if (!rai) {
                isc_throw(BadValue, "can't create RAI option");
            }

            OptionPtr remote_id = rai->getOption(RAI_OPTION_REMOTE_ID);
            if (remote_id) {
                vector<uint8_t> bytes = remote_id->toBinary();
                if (bytes.size() > 0) {
                    upgraded->set("remote-id",
                                  Element::create(encode::encodeHex(bytes)));
                }
            }

            OptionPtr relay_id = rai->getOption(RAI_OPTION_RELAY_ID);
            if (relay_id) {
                vector<uint8_t> bytes = relay_id->toBinary(false);
                if (bytes.size() > 0) {
                    upgraded->set("relay-id",
                                  Element::create(encode::encodeHex(bytes)));
                }
            }

            LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE,
                      DHCPSRV_LEASE4_EXTENDED_INFO_UPGRADED)
                .arg(lease->addr_.toText());
            return (changed);
        } else if (extended_info->getType() != Element::map) {
            mutable_isc->remove("relay-agent-info");
            removed_extended_info = true;
            isc_throw(BadValue, "relay-agent-info is not a map or a string");
        }

        if (check == CfgConsistency::EXTENDED_INFO_CHECK_FIX) {
            return (changed);
        }

        // Try to decode sub-options.
        ConstElementPtr sub_options = extended_info->get("sub-options");
        if (sub_options) {
            verifying = "sub-options";
            if (sub_options->getType() != Element::string) {
                mutable_isc->remove("relay-agent-info");
                removed_extended_info = true;
                isc_throw(BadValue, "sub-options is not a string");
            }
            string rai_hex = sub_options->stringValue();
            vector<uint8_t> rai_data;
            str::decodeFormattedHexString(rai_hex, rai_data);
        }

        ConstElementPtr remote_id = extended_info->get("remote-id");
        if (remote_id) {
            verifying = "remote-id";
            if (remote_id->getType() != Element::string) {
                mutable_isc->remove("relay-agent-info");
                removed_extended_info = true;
                isc_throw(BadValue, "remote-id is not a string");
            }
            string remote_id_hex = remote_id->stringValue();
            vector<uint8_t> remote_id_data;
            encode::decodeHex(remote_id_hex, remote_id_data);
            if (remote_id_data.empty()) {
                mutable_isc->remove("relay-agent-info");
                removed_extended_info = true;
                isc_throw(BadValue, "remote-id is empty");
            }
        }

        ConstElementPtr relay_id = extended_info->get("relay-id");
        if (relay_id) {
            verifying = "relay-id";
            if (relay_id->getType() != Element::string) {
                mutable_isc->remove("relay-agent-info");
                removed_extended_info = true;
                isc_throw(BadValue, "relay-id is not a string");
            }
            string relay_id_hex = relay_id->stringValue();
            vector<uint8_t> relay_id_data;
            encode::decodeHex(relay_id_hex, relay_id_data);
            if (relay_id_data.empty()) {
                mutable_isc->remove("relay-agent-info");
                removed_extended_info = true;
                isc_throw(BadValue, "relay-id is empty");
            }
        }

        if (check != CfgConsistency::EXTENDED_INFO_CHECK_PEDANTIC) {
            return (changed);
        }

        verifying = "relay-agent-info";
        for (auto const& elem : extended_info->mapValue()) {
            if ((elem.first != "sub-options") &&
                (elem.first != "remote-id") &&
                (elem.first != "relay-id") &&
                (elem.first != "comment")) {
                mutable_isc->remove("relay-agent-info");
                removed_extended_info = true;
                isc_throw(BadValue, "spurious '" << elem.first <<
                          "' entry in relay-agent-info");
            }
        }

        return (changed);
    } catch (const exception& ex) {
        ostringstream err;
        err << "in " << verifying << " a problem was found: " << ex.what();
        LOG_ERROR(dhcpsrv_logger, DHCPSRV_LEASE4_EXTENDED_INFO_SANITY_FAIL)
            .arg(lease->addr_.toText())
            .arg(err.str());

        changed = true;
        if (verifying == "user context") {
            lease->setContext(ConstElementPtr());
        } else if (verifying == "isc") {
            mutable_user_context->remove("ISC");
            if (mutable_user_context->empty()) {
                lease->setContext(ConstElementPtr());
            }
        } else {
            if (!removed_extended_info) {
                mutable_isc->remove("relay-agent-info");
            }
            if (mutable_isc->empty()) {
                mutable_user_context->remove("ISC");
                if (mutable_user_context->empty()) {
                    lease->setContext(ConstElementPtr());
                }
            }
        }
        return (changed);
    }
}

bool
LeaseMgr::upgradeLease6ExtendedInfo(const Lease6Ptr& lease,
                                    CfgConsistency::ExtendedInfoSanity check) {
    bool changed = false;
    if (!lease) {
        return (changed);
    }

    if (check == CfgConsistency::EXTENDED_INFO_CHECK_NONE) {
        return (changed);
    }

    ConstElementPtr user_context = lease->getContext();
    if (!user_context) {
        return (changed);
    }

    ConstElementPtr isc;
    ConstElementPtr relay_info;
    ElementPtr mutable_user_context;
    ElementPtr mutable_isc;
    string verifying = "";
    bool removed_relay_info = false;
    bool upgraded = false;
    bool have_both = false;
    int i = -1;

    try {
        verifying = "user context";
        if (user_context->getType() != Element::map) {
            isc_throw(BadValue, "user context is not a map");
        }
        if (user_context->empty()) {
            changed = true;
            lease->setContext(ConstElementPtr());
            return (changed);
        }

        verifying = "isc";
        isc = user_context->get("ISC");
        if (!isc) {
            return (changed);
        }
        mutable_user_context =
            boost::const_pointer_cast<Element>(user_context);
        if (!mutable_user_context) {
            // Should not happen...
            mutable_user_context = copy(user_context, 0);
            lease->setContext(mutable_user_context);
        }

        if (isc->getType() != Element::map) {
            isc_throw(BadValue, "ISC entry is not a map");
        }
        if (isc->empty()) {
            changed = true;
            mutable_user_context->remove("ISC");
            if (mutable_user_context->empty()) {
                lease->setContext(ConstElementPtr());
            }
            return (changed);
        }
        mutable_isc = boost::const_pointer_cast<Element>(isc);
        if (!mutable_isc) {
            // Should not happen...
            mutable_isc = copy(isc, 0);
            mutable_user_context->set("ISC", mutable_isc);
        }

        relay_info = mutable_isc->get("relays");
        if (relay_info && isc->contains("relay-info")) {
            changed = true;
            mutable_isc->remove("relays");
            have_both = true;
            relay_info.reset();
        }
        if (relay_info) {
            // Upgrade
            changed = true;
            upgraded = true;
            verifying = "relays";
            mutable_isc->set("relay-info", relay_info);
            mutable_isc->remove("relays");

            if (relay_info->getType() != Element::list) {
                mutable_isc->remove("relay-info");
                removed_relay_info = true;
                isc_throw(BadValue, "relays is not a list");
            }
            if (relay_info->empty()) {
                mutable_isc->remove("relay-info");
                removed_relay_info = true;
                isc_throw(BadValue, "relays is empty");
            }

            verifying = "relay";
            for (i = 0; static_cast<size_t>(i) < relay_info->size(); ++i) {
                ElementPtr relay = relay_info->getNonConst(i);
                if (!relay) {
                    mutable_isc->remove("relay-info");
                    removed_relay_info = true;
                    isc_throw(BadValue, "null relay#" << i);
                }
                if (relay->getType() != Element::map) {
                    mutable_isc->remove("relay-info");
                    removed_relay_info = true;
                    isc_throw(BadValue, "relay#" << i << " is not a map");
                }

                // Try to decode options.
                ConstElementPtr options = relay->get("options");
                if (!options) {
                    continue;
                }

                verifying = "options";
                if (options->getType() != Element::string) {
                    mutable_isc->remove("relay-info");
                    removed_relay_info = true;
                    isc_throw(BadValue, "options is not a string");
                }
                string options_hex = options->stringValue();
                vector<uint8_t> options_data;
                str::decodeFormattedHexString(options_hex, options_data);
                OptionCollection opts;
                LibDHCP::unpackOptions6(options_data, DHCP6_OPTION_SPACE, opts);

                auto remote_id_it = opts.find(D6O_REMOTE_ID);
                if (remote_id_it != opts.end()) {
                    OptionPtr remote_id = remote_id_it->second;
                    if (remote_id) {
                        vector<uint8_t> bytes = remote_id->toBinary();
                        if (bytes.size() > 0) {
                            relay->set("remote-id",
                                       Element::create(encode::encodeHex(bytes)));
                        }
                    }
                }

                auto relay_id_it = opts.find(D6O_RELAY_ID);
                if (relay_id_it != opts.end()) {
                    OptionPtr relay_id = relay_id_it->second;
                    if (relay_id) {
                        vector<uint8_t> bytes = relay_id->toBinary(false);
                        if (bytes.size() > 0) {
                            relay->set("relay-id",
                                       Element::create(encode::encodeHex(bytes)));
                        }
                    }
                }
            }
        }

        verifying = (upgraded ? "relays" : "relay-info");
        i = -1;
        relay_info = mutable_isc->get("relay-info");
        if (!relay_info) {
            return (changed);
        }
        if (!upgraded && (relay_info->getType() != Element::list)) {
            mutable_isc->remove("relay-info");
            removed_relay_info = true;
            isc_throw(BadValue, "relay-info is not a list");
        }
        if (!upgraded && relay_info->empty()) {
            mutable_isc->remove("relay-info");
            removed_relay_info = true;
            isc_throw(BadValue, "relay-info is empty");
        }

        verifying = "relay";
        for (i = 0; static_cast<size_t>(i) < relay_info->size(); ++i) {
            ElementPtr relay = relay_info->getNonConst(i);
            if (!upgraded && !relay) {
                mutable_isc->remove("relay-info");
                removed_relay_info = true;
                isc_throw(BadValue, "null relay#" << i);
            }
            if (!upgraded && (relay->getType() != Element::map)) {
                mutable_isc->remove("relay-info");
                removed_relay_info = true;
                isc_throw(BadValue, "relay#" << i << " is not a map");
            }

            ConstElementPtr options = relay->get("options");
            if (!upgraded && options) {
                // Try to decode options.
                verifying = "options";
                if (options->getType() != Element::string) {
                    mutable_isc->remove("relay-info");
                    removed_relay_info = true;
                    isc_throw(BadValue, "options is not a string");
                }
                string options_hex = options->stringValue();
                vector<uint8_t> options_data;
                str::decodeFormattedHexString(options_hex, options_data);
                OptionCollection opts;
                LibDHCP::unpackOptions6(options_data, DHCP6_OPTION_SPACE, opts);
            }
            if (check == CfgConsistency::EXTENDED_INFO_CHECK_FIX) {
                continue;
            }

            verifying = "link";
            ConstElementPtr link_addr = relay->get("link");
            if (!link_addr) {
                mutable_isc->remove("relay-info");
                removed_relay_info = true;
                isc_throw(BadValue, "no link");
            }
            if (link_addr->getType() != Element::string) {
                mutable_isc->remove("relay-info");
                removed_relay_info = true;
                isc_throw(BadValue, "link is not a string");
            }
            IOAddress laddr(link_addr->stringValue());
            if (!laddr.isV6()) {
                mutable_isc->remove("relay-info");
                removed_relay_info = true;
                isc_throw(BadValue, "link is not an IPv6 address");
            }

            ConstElementPtr remote_id = relay->get("remote-id");
            if (!upgraded && remote_id) {
                verifying = "remote-id";
                if (remote_id->getType() != Element::string) {
                    mutable_isc->remove("relay-info");
                    removed_relay_info = true;
                    isc_throw(BadValue, "remote-id is not a string");
                }
                string remote_id_hex = remote_id->stringValue();
                vector<uint8_t> remote_id_data;
                encode::decodeHex(remote_id_hex, remote_id_data);
                if (remote_id_data.empty()) {
                    mutable_isc->remove("relay-info");
                    removed_relay_info = true;
                    isc_throw(BadValue, "remote-id is empty");
                }
            }

            ConstElementPtr relay_id = relay->get("relay-id");
            if (!upgraded && relay_id) {
                verifying = "relay-id";
                if (relay_id->getType() != Element::string) {
                    mutable_isc->remove("relay-info");
                    removed_relay_info = true;
                    isc_throw(BadValue, "relay-id is not a string");
                }
                string relay_id_hex = relay_id->stringValue();
                vector<uint8_t> relay_id_data;
                encode::decodeHex(relay_id_hex, relay_id_data);
                if (relay_id_data.empty()) {
                    mutable_isc->remove("relay-info");
                    removed_relay_info = true;
                    isc_throw(BadValue, "relay-id is empty");
                }
            }

            if (check != CfgConsistency::EXTENDED_INFO_CHECK_PEDANTIC) {
                continue;
            }

            verifying = "peer";
            ConstElementPtr peer_addr = relay->get("peer");
            if (!peer_addr) {
                mutable_isc->remove("relay-info");
                removed_relay_info = true;
                isc_throw(BadValue, "no peer");
            }
            if (peer_addr->getType() != Element::string) {
                mutable_isc->remove("relay-info");
                removed_relay_info = true;
                isc_throw(BadValue, "peer is not a string");
            }
            IOAddress paddr(peer_addr->stringValue());
            if (!paddr.isV6()) {
                mutable_isc->remove("relay-info");
                removed_relay_info = true;
                isc_throw(BadValue, "peer is not an IPv6 address");
            }

            verifying = "hop";
            ConstElementPtr hop = relay->get("hop");
            if (!hop) {
                mutable_isc->remove("relay-info");
                removed_relay_info = true;
                isc_throw(BadValue, "no hop");
            }
            if (hop->getType() != Element::integer) {
                mutable_isc->remove("relay-info");
                removed_relay_info = true;
                isc_throw(BadValue, "hop is not an integer");
            }

            verifying = (upgraded ? "relays" : "relay-info");
            for (auto const& elem : relay->mapValue()) {
                if ((elem.first != "hop") &&
                    (elem.first != "link") &&
                    (elem.first != "peer") &&
                    (elem.first != "options") &&
                    (elem.first != "remote-id") &&
                    (elem.first != "relay-id") &&
                    (elem.first != "comment")) {
                    mutable_isc->remove("relay-info");
                    removed_relay_info = true;
                    isc_throw(BadValue, "spurious '" << elem.first << "' entry");
                }
            }
        }

        if (upgraded) {
            LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE,
                      DHCPSRV_LEASE6_EXTENDED_INFO_UPGRADED)
                .arg(lease->addr_.toText());
        }

        return (changed);
    } catch (const exception& ex) {
        ostringstream err;
        err << "in " << verifying;
        if (i >= 0) {
            err << " [relay#" << i << "]";
        }
        err << " a problem was found: " << ex.what();
        LOG_ERROR(dhcpsrv_logger, DHCPSRV_LEASE6_EXTENDED_INFO_SANITY_FAIL)
            .arg(lease->addr_.toText())
            .arg(err.str());

        changed = true;
        have_both = !have_both;
        if (verifying == "user context") {
            lease->setContext(ConstElementPtr());
        } else if (verifying == "isc") {
            mutable_user_context->remove("ISC");
            if (mutable_user_context->empty()) {
                lease->setContext(ConstElementPtr());
            }
        } else {
            if (!removed_relay_info) {
                mutable_isc->remove("relay-info");
            }
            if (mutable_isc->empty()) {
                mutable_user_context->remove("ISC");
                if (mutable_user_context->empty()) {
                    lease->setContext(ConstElementPtr());
                }
            }
        }
        return (changed);
    }
}

void
LeaseMgr::extractLease4ExtendedInfo(const Lease4Ptr& lease,
                                    bool ignore_errors) {
    if (!lease) {
        return;
    }

    ConstElementPtr user_context = lease->getContext();
    if (!user_context) {
        return;
    }
    if (user_context->getType() != Element::map) {
        if (ignore_errors) {
            return;
        }
        isc_throw(BadValue, "user context is not a map");
    }
    if (user_context->empty()) {
        return;
    }

    ConstElementPtr isc = user_context->get("ISC");
    if (!isc) {
        return;
    }
    if (isc->getType() != Element::map) {
        if (ignore_errors) {
            return;
        }
        isc_throw(BadValue, "ISC entry is not a map");
    }
    if (isc->empty()) {
        return;
    }

    ConstElementPtr extended_info = isc->get("relay-agent-info");
    if (!extended_info) {
        return;
    }
    if (extended_info->getType() != Element::map) {
        if (ignore_errors) {
            return;
        }
        isc_throw(BadValue, "relay-agent-info is not a map");
    }
    if (extended_info->empty()) {
        return;
    }

    ConstElementPtr relay_id = extended_info->get("relay-id");
    if (relay_id) {
        if (relay_id->getType() == Element::string) {
            vector<uint8_t> bytes;
            try {
                encode::decodeHex(relay_id->stringValue(), bytes);
            } catch (...) {
                // Decode failed
                if (!ignore_errors) {
                    throw;
                }
            }
            lease->relay_id_ = bytes;
        } else if (!ignore_errors) {
            isc_throw(BadValue, "relay-id entry is not a string");
        }
    }

    ConstElementPtr remote_id = extended_info->get("remote-id");
    if (remote_id) {
        if (remote_id->getType() == Element::string) {
            vector<uint8_t> bytes;
            try {
                encode::decodeHex(remote_id->stringValue(), bytes);
            } catch (...) {
                // Decode failed
                if (!ignore_errors) {
                    throw;
                }
            }
            lease->remote_id_ = bytes;
        } else if (!ignore_errors) {
            isc_throw(BadValue, "remote-id entry is not a string");
        }
    }
}

bool
LeaseMgr::addExtendedInfo6(const Lease6Ptr& lease) {

    bool added = false;
    if (!lease) {
        return (added);
    }

    ConstElementPtr user_context = lease->getContext();
    if (!user_context || (user_context->getType() != Element::map) ||
        user_context->empty()) {
        return (added);
    }

    ConstElementPtr isc = user_context->get("ISC");
    if (!isc || (isc->getType() != Element::map) || isc->empty()) {
        return (added);
    }

    ConstElementPtr relay_info = isc->get("relay-info");
    if (!relay_info || (relay_info->getType() != Element::list) ||
        relay_info->empty()) {
        return (added);
    }

    for (unsigned i = 0; i < relay_info->size(); ++i) {
        ConstElementPtr relay = relay_info->get(i);
        if (!relay || (relay->getType() != Element::map) || relay->empty()) {
            continue;
        }
        try {
            ConstElementPtr relay_id = relay->get("relay-id");
            if (relay_id) {
                string relay_id_hex = relay_id->stringValue();
                vector<uint8_t> relay_id_data;
                encode::decodeHex(relay_id_hex, relay_id_data);
                if (relay_id_data.empty()) {
                    continue;
                }
                addRelayId6(lease->addr_, relay_id_data);
                added = true;
            }

            ConstElementPtr remote_id = relay->get("remote-id");
            if (remote_id) {
                string remote_id_hex = remote_id->stringValue();
                vector<uint8_t> remote_id_data;
                encode::decodeHex(remote_id_hex, remote_id_data);
                if (remote_id_data.empty()) {
                    continue;
                }
                addRemoteId6(lease->addr_, remote_id_data);
                added = true;
            }
        } catch (const exception&) {
            continue;
        }
    }
    return (added);
}

size_t
LeaseMgr::byRelayId6size() const {
    return (0);
}

size_t
LeaseMgr::byRemoteId6size() const {
    return (0);
}

} // namespace isc::dhcp
} // namespace isc
