// Copyright (C) 2014-2024 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 <asiolink/io_address.h>
#include <asiolink/addr_utilities.h>
#include <cryptolink/crypto_rng.h>
#include <dhcp/pkt4.h>
#include <dhcpsrv/host.h>
#include <exceptions/exceptions.h>

#include <util/encode/encode.h>
#include <util/str.h>

#include <boost/foreach.hpp>
#include <sstream>

using namespace isc::data;
using namespace isc::asiolink;

namespace isc {
namespace dhcp {

AuthKey::AuthKey(const std::vector<uint8_t>& key) {
    setAuthKey(key);
}

AuthKey::AuthKey(const std::string& key) {
    setAuthKey(key);
}

AuthKey::AuthKey() {
    authKey_ = AuthKey::getRandomKeyString();
}

std::vector<uint8_t>
AuthKey::getRandomKeyString() {
    return (isc::cryptolink::random(AUTH_KEY_LEN));
}

std::string
AuthKey::toText() const {
    if (authKey_.empty()) {
        return ("");
    }
    return (util::encode::encodeHex(authKey_));
}

void
AuthKey::setAuthKey(const std::vector<uint8_t>& key) {
    authKey_ = key;
    if (authKey_.size() > AUTH_KEY_LEN) {
        authKey_.resize(AUTH_KEY_LEN);
    }
}

void
AuthKey::setAuthKey(const std::string& key) {
    if (key.empty()) {
        authKey_.clear();
        return;
    }
    try {
        std::vector<uint8_t> bin;
        util::encode::decodeHex(key, bin);
        setAuthKey(bin);
    } catch (const std::exception& ex) {
        isc_throw(BadValue, "bad auth key: " << ex.what());
    }
}

bool
AuthKey::operator==(const AuthKey& other) const {
    return (authKey_ == other.authKey_);
}

bool
AuthKey::operator!=(const AuthKey& other) const {
    return (authKey_ != other.authKey_);
}

IPv6Resrv::IPv6Resrv(const Type& type,
                     const asiolink::IOAddress& prefix,
                     const uint8_t prefix_len)
    : type_(type), prefix_(asiolink::IOAddress("::")), prefix_len_(128),
      pd_exclude_option_() {
    // Validate and set the actual values.
    set(type, prefix, prefix_len);
}

void
IPv6Resrv::set(const Type& type, const asiolink::IOAddress& prefix,
               const uint8_t prefix_len) {
    if (!prefix.isV6() || prefix.isV6Multicast()) {
        isc_throw(isc::BadValue, "invalid prefix '" << prefix
                  << "' for new IPv6 reservation");

    } else if (prefix_len > 128) {
        isc_throw(isc::BadValue, "invalid prefix length '"
                  << static_cast<int>(prefix_len)
                  << "' for new IPv6 reservation");

    } else if ((type == TYPE_NA) && (prefix_len != 128)) {
        isc_throw(isc::BadValue, "invalid prefix length '"
                  << static_cast<int>(prefix_len)
                  << "' for reserved IPv6 address, expected 128");
    }

    type_ = type;
    prefix_ = prefix;
    prefix_len_ = prefix_len;
    pd_exclude_option_.reset();
}

void
IPv6Resrv::setPDExclude(const asiolink::IOAddress& excluded_prefix,
                        const uint8_t excluded_prefix_len) {
    if (excluded_prefix_len == 0) {
        pd_exclude_option_.reset();
    } else {
        pd_exclude_option_.reset(new Option6PDExclude(prefix_, prefix_len_,
                                                      excluded_prefix,
                                                      excluded_prefix_len));
    }
}

std::string
IPv6Resrv::toText(bool display_pd_exclude_option) const {
    std::ostringstream s;
    s << prefix_;
    // For PD, append prefix length.
    if (type_ == TYPE_PD) {
        s << "/" << static_cast<int>(prefix_len_);
        // If there is a Prefix Exclude option append it.
        if (display_pd_exclude_option && pd_exclude_option_) {
            s << " (excluded_prefix="
              << pd_exclude_option_->getExcludedPrefix(prefix_,
                                                       prefix_len_).toText()
              << "/"
              << static_cast<int>(pd_exclude_option_->getExcludedPrefixLength())
              << ")";
        }
    }
    return (s.str());
}

std::string
IPv6Resrv::PDExcludetoText() const {
    if (pd_exclude_option_) {
        std::ostringstream s;
        s << pd_exclude_option_->getExcludedPrefix(prefix_,
                                                   prefix_len_).toText()
          << "/"
          << static_cast<int>(pd_exclude_option_->getExcludedPrefixLength());
        return (s.str());
    } else {
        return ("");
    }
}

bool
IPv6Resrv::operator==(const IPv6Resrv& other) const {
    return (type_ == other.type_ &&
            prefix_ == other.prefix_ &&
            prefix_len_ == other.prefix_len_);
}

bool
IPv6Resrv::operator!=(const IPv6Resrv& other) const {
    return (!operator==(other));
}

Host::Host(const uint8_t* identifier, const size_t identifier_len,
           const IdentifierType& identifier_type,
           const SubnetID ipv4_subnet_id, const SubnetID ipv6_subnet_id,
           const asiolink::IOAddress& ipv4_reservation,
           const std::string& hostname,
           const std::string& dhcp4_client_classes,
           const std::string& dhcp6_client_classes,
           const asiolink::IOAddress& next_server,
           const std::string& server_host_name,
           const std::string& boot_file_name,
           const AuthKey& auth_key)

    : identifier_type_(identifier_type),
      identifier_value_(), ipv4_subnet_id_(ipv4_subnet_id),
      ipv6_subnet_id_(ipv6_subnet_id),
      ipv4_reservation_(asiolink::IOAddress::IPV4_ZERO_ADDRESS()),
      hostname_(hostname), dhcp4_client_classes_(dhcp4_client_classes),
      dhcp6_client_classes_(dhcp6_client_classes),
      next_server_(asiolink::IOAddress::IPV4_ZERO_ADDRESS()),
      server_host_name_(server_host_name), boot_file_name_(boot_file_name),
      host_id_(0), cfg_option4_(new CfgOption()),
      cfg_option6_(new CfgOption()), negative_(false),
      key_(auth_key) {

    // Initialize host identifier.
    setIdentifier(identifier, identifier_len, identifier_type);

    if (!ipv4_reservation.isV4Zero()) {
        // Validate and set IPv4 address reservation.
        setIPv4Reservation(ipv4_reservation);
    }

    if (!next_server.isV4Zero()) {
        // Validate and set next server address.
        setNextServer(next_server);
    }
}

Host::Host(const std::string& identifier, const std::string& identifier_name,
           const SubnetID ipv4_subnet_id, const SubnetID ipv6_subnet_id,
           const asiolink::IOAddress& ipv4_reservation,
           const std::string& hostname,
           const std::string& dhcp4_client_classes,
           const std::string& dhcp6_client_classes,
           const asiolink::IOAddress& next_server,
           const std::string& server_host_name,
           const std::string& boot_file_name,
           const AuthKey& auth_key)
    : identifier_type_(IDENT_HWADDR),
      identifier_value_(), ipv4_subnet_id_(ipv4_subnet_id),
      ipv6_subnet_id_(ipv6_subnet_id),
      ipv4_reservation_(asiolink::IOAddress::IPV4_ZERO_ADDRESS()),
      hostname_(hostname), dhcp4_client_classes_(dhcp4_client_classes),
      dhcp6_client_classes_(dhcp6_client_classes),
      next_server_(asiolink::IOAddress::IPV4_ZERO_ADDRESS()),
      server_host_name_(server_host_name), boot_file_name_(boot_file_name),
      host_id_(0), cfg_option4_(new CfgOption()),
      cfg_option6_(new CfgOption()), negative_(false),
      key_(auth_key) {

    // Initialize host identifier.
    setIdentifier(identifier, identifier_name);

    if (!ipv4_reservation.isV4Zero()) {
        // Validate and set IPv4 address reservation.
        setIPv4Reservation(ipv4_reservation);
    }

    if (!next_server.isV4Zero()) {
        // Validate and set next server address.
        setNextServer(next_server);
    }
}

size_t
Host::getIdentifierMaxLength(const IdentifierType& type) {
    switch (type) {
    case IDENT_HWADDR:
        return (HWAddr::MAX_HWADDR_LEN);
    case IDENT_DUID:
        return (DUID::MAX_DUID_LEN);
    case IDENT_CLIENT_ID:
        return (ClientId::MAX_CLIENT_ID_LEN);
    default:
        // In fact it is backend dependent but for compatibility we take
        // the lowest value.
        return (128);
    }
}

const std::vector<uint8_t>&
Host::getIdentifier() const {
    return (identifier_value_);
}

Host::IdentifierType
Host::getIdentifierType() const {
    return (identifier_type_);
}

Host::IdentifierType
Host::getIdentifierType(const std::string& identifier_name) {
    if (identifier_name == "hw-address") {
        return (IDENT_HWADDR);

    } else if (identifier_name == "duid") {
        return (IDENT_DUID);

    } else if (identifier_name == "circuit-id") {
        return (IDENT_CIRCUIT_ID);

    } else if (identifier_name == "client-id") {
        return (IDENT_CLIENT_ID);
    } else if (identifier_name == "flex-id") {
        return (IDENT_FLEX);
    } else {
        isc_throw(isc::BadValue, "invalid client identifier type '"
                  << identifier_name << "'");
    }
}

HWAddrPtr
Host::getHWAddress() const {
    return ((identifier_type_ == IDENT_HWADDR) ?
            HWAddrPtr(new HWAddr(identifier_value_, HTYPE_ETHER)) : HWAddrPtr());
}

DuidPtr
Host::getDuid() const {
    return ((identifier_type_ == IDENT_DUID) ?
            DuidPtr(new DUID(identifier_value_)) : DuidPtr());
}


std::string
Host::getIdentifierAsText() const {
    return (getIdentifierAsText(identifier_type_, &identifier_value_[0],
                                identifier_value_.size()));
}

std::string
Host::getIdentifierAsText(const IdentifierType& type, const uint8_t* value,
                          const size_t length) {
    // Convert identifier into <type>=<value> form.
    std::ostringstream s;
    switch (type) {
    case IDENT_HWADDR:
        s << "hwaddr";
        break;
    case IDENT_DUID:
        s << "duid";
        break;
    case IDENT_CIRCUIT_ID:
        s << "circuit-id";
        break;
    case IDENT_CLIENT_ID:
        s << "client-id";
        break;
    case IDENT_FLEX:
        s << "flex-id";
        break;
    default:
        // This should never happen actually, unless we add new identifier
        // and forget to add a case for it above.
        s << "(invalid-type)";
    }
    std::vector<uint8_t> vec(value, value + length);
    s << "=" << (length > 0 ? util::encode::encodeHex(vec) : "(null)");
    return (s.str());
}

std::string
Host::getIdentifierName(const IdentifierType& type) {
    switch (type) {
    case Host::IDENT_HWADDR:
        return ("hw-address");

    case Host::IDENT_DUID:
        return ("duid");

    case Host::IDENT_CIRCUIT_ID:
        return ("circuit-id");

    case Host::IDENT_CLIENT_ID:
        return ("client-id");

    case Host::IDENT_FLEX:
        return ("flex-id");

    default:
        ;
    }
    return ("(unknown)");
}


void
Host::setIdentifier(const uint8_t* identifier, const size_t len,
                    const IdentifierType& type) {
    if (len < 1) {
        isc_throw(BadValue, "invalid client identifier length 0");
    } else if (len > getIdentifierMaxLength(type)) {
        isc_throw(BadValue, "too long client identifier type "
                  << getIdentifierName(type)
                  << " length " << len);
    }

    identifier_type_ = type;
    identifier_value_.assign(identifier, identifier + len);
}

void
Host::setIdentifier(const std::string& identifier, const std::string& name) {
    // Empty identifier is not allowed.
    if (identifier.empty()) {
        isc_throw(isc::BadValue, "empty host identifier used");
    }

    // Set identifier type.
    identifier_type_ = getIdentifierType(name);

    // Identifier value can either be specified as string of hexadecimal
    // digits or a string in quotes. The latter is copied to a vector excluding
    // quote characters.

    // Try to convert the values in quotes into a vector of ASCII codes.
    // If the identifier lacks opening and closing quote, this will return
    // an empty value, in which case we'll try to decode it as a string of
    // hexadecimal digits.
    bool too_long = false;
    try {
        std::vector<uint8_t> binary = util::str::quotedStringToBinary(identifier);
        if (binary.empty()) {
            util::str::decodeFormattedHexString(identifier, binary);
        }

        size_t len = binary.size();
        if (len > getIdentifierMaxLength(identifier_type_)) {
            // Message does not matter as it will be replaced below...
            too_long = true;
            isc_throw(BadValue, "too long client identifier type " << name
                      << " length " << len);
        }

        // Successfully decoded the identifier, so let's use it.
        identifier_value_.swap(binary);

    } catch (...) {
        // The string doesn't match any known pattern, so we have to
        // report an error at this point.
        if (too_long) {
            throw;
        }
        isc_throw(isc::BadValue, "invalid host identifier value '"
                  << identifier << "'");
    }
}

void
Host::setIdentifierType(const IdentifierType& type) {
    identifier_type_ = type;
}

void
Host::setIPv4Reservation(const asiolink::IOAddress& address) {
    if (!address.isV4()) {
        isc_throw(isc::BadValue, "address '" << address << "' is not a valid"
                  " IPv4 address");
    } else if (address.isV4Zero() || address.isV4Bcast()) {
        isc_throw(isc::BadValue, "must not make reservation for the '"
                  << address << "' address");
    }
    ipv4_reservation_ = address;
}

void
Host::removeIPv4Reservation() {
    ipv4_reservation_ = asiolink::IOAddress::IPV4_ZERO_ADDRESS();
}

void
Host::addReservation(const IPv6Resrv& reservation) {
    // Check if it is not duplicating existing reservation.
    if (hasReservation(reservation)) {
        isc_throw(isc::InvalidOperation, "failed on attempt to add a duplicated"
                  " host reservation for " << reservation.toText());
    }
    // Add it.
    ipv6_reservations_.insert(IPv6ResrvTuple(reservation.getType(),
                                             reservation));
}

IPv6ResrvRange
Host::getIPv6Reservations(const IPv6Resrv::Type& type) const {
    return (ipv6_reservations_.equal_range(type));
}

IPv6ResrvRange
Host::getIPv6Reservations() const {
    return (IPv6ResrvRange(ipv6_reservations_.begin(),
                           ipv6_reservations_.end()));
}

bool
Host::hasIPv6Reservation() const {
    return (!ipv6_reservations_.empty());
}

bool
Host::hasReservation(const IPv6Resrv& reservation) const {
    IPv6ResrvRange reservations = getIPv6Reservations(reservation.getType());
    if (std::distance(reservations.first, reservations.second) > 0) {
        BOOST_FOREACH(auto const& it, reservations) {
            if (it.second == reservation) {
                return (true);
            }
        }
    }

    // No matching reservations found.
    return (false);
}

void
Host::addClientClass4(const std::string& class_name) {
    addClientClassInternal(dhcp4_client_classes_, class_name);
}


void
Host::addClientClass6(const std::string& class_name) {
    addClientClassInternal(dhcp6_client_classes_, class_name);
}

void
Host::addClientClassInternal(ClientClasses& classes,
                             const std::string& class_name) {
    std::string trimmed = util::str::trim(class_name);
    if (!trimmed.empty()) {
        classes.insert(ClientClass(trimmed));
    }
}

void
Host::setNextServer(const asiolink::IOAddress& next_server) {
    if (!next_server.isV4()) {
        isc_throw(isc::BadValue, "next server address '" << next_server
                  << "' is not a valid IPv4 address");
    } else if (next_server.isV4Bcast()) {
        isc_throw(isc::BadValue, "invalid next server address '"
                  << next_server << "'");
    }

    next_server_ = next_server;
}

void
Host::setServerHostname(const std::string& server_host_name) {
    if (server_host_name.size() > Pkt4::MAX_SNAME_LEN - 1) {
        isc_throw(isc::BadValue, "server hostname length must not exceed "
                  << (Pkt4::MAX_SNAME_LEN - 1));
    }
    server_host_name_ = server_host_name;
}

void
Host::setBootFileName(const std::string& boot_file_name) {
    if (boot_file_name.size() > Pkt4::MAX_FILE_LEN - 1) {
        isc_throw(isc::BadValue, "boot file length must not exceed "
                  << (Pkt4::MAX_FILE_LEN - 1));
    }
    boot_file_name_ = boot_file_name;
}

ElementPtr
Host::toElement4() const {

    // Prepare the map
    ElementPtr map = Element::createMap();
    // Set the user context
    contextToElement(map);
    // Set the identifier
    Host::IdentifierType id_type = getIdentifierType();
    if (id_type == Host::IDENT_HWADDR) {
        HWAddrPtr hwaddr = getHWAddress();
        map->set("hw-address", Element::create(hwaddr->toText(false)));
    } else if (id_type == Host::IDENT_DUID) {
        DuidPtr duid = getDuid();
        map->set("duid", Element::create(duid->toText()));
    } else if (id_type == Host::IDENT_CIRCUIT_ID) {
        const std::vector<uint8_t>& bin = getIdentifier();
        std::string circuit_id = util::encode::encodeHex(bin);
        map->set("circuit-id", Element::create(circuit_id));
    } else if (id_type == Host::IDENT_CLIENT_ID) {
        const std::vector<uint8_t>& bin = getIdentifier();
        std::string client_id = util::encode::encodeHex(bin);
        map->set("client-id", Element::create(client_id));
    } else if (id_type == Host::IDENT_FLEX) {
        const std::vector<uint8_t>& bin = getIdentifier();
        std::string flex = util::encode::encodeHex(bin);
        map->set("flex-id", Element::create(flex));
    } else {
        isc_throw(ToElementError, "invalid identifier type: " << id_type);
    }
    // Set the reservation (if not 0.0.0.0 which may not be re-read)
    const IOAddress& address = getIPv4Reservation();
    if (!address.isV4Zero()) {
        map->set("ip-address", Element::create(address.toText()));
    }
    // Set the hostname
    const std::string& hostname = getHostname();
    map->set("hostname", Element::create(hostname));
    // Set next-server
    const IOAddress& next_server = getNextServer();
    map->set("next-server", Element::create(next_server.toText()));
    // Set server-hostname
    const std::string& server_hostname = getServerHostname();
    map->set("server-hostname", Element::create(server_hostname));
    // Set boot-file-name
    const std::string& boot_file_name = getBootFileName();
    map->set("boot-file-name", Element::create(boot_file_name));
    // Set client-classes
    const ClientClasses& cclasses = getClientClasses4();
    ElementPtr classes = Element::createList();
    for (auto const& cclass : cclasses) {
        classes->add(Element::create(cclass));
    }
    map->set("client-classes", classes);
    // Set option-data
    ConstCfgOptionPtr opts = getCfgOption4();
    map->set("option-data", opts->toElement());

    return (map);
}

ElementPtr
Host::toElement6() const {
    // Prepare the map
    ElementPtr map = Element::createMap();
    // Set the user context
    contextToElement(map);
    // Set the identifier
    Host::IdentifierType id_type = getIdentifierType();
    if (id_type == Host::IDENT_HWADDR) {
        HWAddrPtr hwaddr = getHWAddress();
        map->set("hw-address", Element::create(hwaddr->toText(false)));
    } else if (id_type == Host::IDENT_DUID) {
        DuidPtr duid = getDuid();
        map->set("duid", Element::create(duid->toText()));
    } else if (id_type == Host::IDENT_CIRCUIT_ID) {
        isc_throw(ToElementError, "unexpected circuit-id DUID type");
    } else if (id_type == Host::IDENT_CLIENT_ID) {
        isc_throw(ToElementError, "unexpected client-id DUID type");
    } else if (id_type == Host::IDENT_FLEX) {
        const std::vector<uint8_t>& bin = getIdentifier();
        std::string flex = util::encode::encodeHex(bin);
        map->set("flex-id", Element::create(flex));
    } else {
        isc_throw(ToElementError, "invalid DUID type: " << id_type);
    }
    // Set reservations (ip-addresses)
    const IPv6ResrvRange& na_resv = getIPv6Reservations(IPv6Resrv::TYPE_NA);
    ElementPtr resvs = Element::createList();
    BOOST_FOREACH(auto const& resv, na_resv) {
        resvs->add(Element::create(resv.second.toText()));
    }
    map->set("ip-addresses", resvs);
    // Set reservations (prefixes)
    const IPv6ResrvRange& pd_resv = getIPv6Reservations(IPv6Resrv::TYPE_PD);
    resvs = Element::createList();
    bool has_pd_exclude_option(false);
    BOOST_FOREACH(auto const& resv, pd_resv) {
        if (resv.second.getPDExclude()) {
            has_pd_exclude_option = true;
        }
        resvs->add(Element::create(resv.second.toText(false)));
    }
    map->set("prefixes", resvs);
    // Set Prefix Exclude options.
    if (has_pd_exclude_option) {
        ElementPtr options = Element::createList();
        BOOST_FOREACH(auto const& resv, pd_resv) {
            options->add(Element::create(resv.second.PDExcludetoText()));
        }
        map->set("excluded-prefixes", options);
    }
    // Set the hostname
    const std::string& hostname = getHostname();
    map->set("hostname", Element::create(hostname));
    // Set client-classes
    const ClientClasses& cclasses = getClientClasses6();
    ElementPtr classes = Element::createList();
    for (auto const& cclass : cclasses) {
        classes->add(Element::create(cclass));
    }
    map->set("client-classes", classes);

    // Set option-data
    ConstCfgOptionPtr opts = getCfgOption6();
    map->set("option-data", opts->toElement());

    // Set auth key
    //@todo: uncomment once storing in configuration file is enabled
    //map->set("auth-key", Element::create(getKey().toText()));

    return (map);
}

void
Host::encapsulateOptions() const {
    if (!cfg_option4_->isEncapsulated()) {
        cfg_option4_->encapsulate();
    }
    if (!cfg_option6_->isEncapsulated()) {
        cfg_option6_->encapsulate();
    }
}

std::string
Host::toText() const {
    std::ostringstream s;

    // Add HW address or DUID.
    s << getIdentifierAsText();

    // Add IPv4 subnet id if exists.
    if (ipv4_subnet_id_ != SUBNET_ID_UNUSED) {
        s << " ipv4_subnet_id=" << ipv4_subnet_id_;
    }

    // Add IPv6 subnet id if exists.
    if (ipv6_subnet_id_ != SUBNET_ID_UNUSED) {
        s << " ipv6_subnet_id=" << ipv6_subnet_id_;
    }

    // Add hostname.
    s << " hostname=" << (hostname_.empty() ? "(empty)" : hostname_);

    // Add IPv4 reservation.
    s << " ipv4_reservation=" << (ipv4_reservation_.isV4Zero() ? "(no)" :
                                  ipv4_reservation_.toText());

    // Add next server.
    s << " siaddr=" << (next_server_.isV4Zero() ? "(no)" :
                             next_server_.toText());

    // Add server host name.
    s << " sname=" << (server_host_name_.empty() ? "(empty)" : server_host_name_);

    // Add boot file name.
    s << " file=" << (boot_file_name_.empty() ? "(empty)" : boot_file_name_);

    s << " key=" << (key_.toText().empty() ? "(empty)" : key_.toText());

    size_t count = 0;

    if (ipv6_reservations_.empty()) {
        s << " ipv6_reservations=(none)";

    } else {
        // Add all IPv6 reservations.
        count = 0;
        for (auto const& resrv : ipv6_reservations_) {
            s << " ipv6_reservation"
              << count++
              << "=" << resrv.second.toText();
        }
    }

    // Add DHCPv4 client classes.
    count = 0;
    for (auto const& cclass : dhcp4_client_classes_) {
        s << " dhcp4_class"
          << count++
          << "=" << cclass;
    }

    // Add DHCPv6 client classes.
    count = 0;
    for (auto const& cclass : dhcp6_client_classes_) {
        s << " dhcp6_class"
          << count++
          << "=" << cclass;
    }

    // Add negative cached.
    if (negative_) {
        s << " negative cached";
    }

    return (s.str());
}

} // end of namespace isc::dhcp
} // end of namespace isc
