// Copyright (C) 2017-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 <asiolink/io_address.h>
#include <asiolink/addr_utilities.h>
#include <cc/data.h>
#include <dhcp/hwaddr.h>
#include <dhcpsrv/lease.h>
#include <dhcpsrv/cfgmgr.h>
#include <dhcpsrv/cfg_consistency.h>
#include <dhcpsrv/lease_mgr.h>
#include <lease_cmds_exceptions.h>
#include <lease_parser.h>

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

namespace isc {
namespace lease_cmds {

Lease4Ptr
Lease4Parser::parse(ConstSrvConfigPtr& cfg,
                    const ConstElementPtr& lease_info,
                    bool& force_create) {
    if (!lease_info) {
        isc_throw(BadValue, "lease information missing");
    }

    // These are mandatory parameters.
    IOAddress addr = getAddress(lease_info, "ip-address");
    if (!addr.isV4()) {
        isc_throw(BadValue, "Non-IPv4 address specified: " << addr);
    }

    // Not a most straightforward conversion, but it works.
    string hwaddr_txt = getString(lease_info, "hw-address");
    HWAddr hwaddr = HWAddr::fromText(hwaddr_txt);
    HWAddrPtr hwaddr_ptr = HWAddrPtr(new HWAddr(hwaddr));

    // Now sort out the subnet-id. If specified, it must have correct value.
    // If not specified, Kea will try to sort it out.
    SubnetID subnet_id = 0;
    if (lease_info->contains("subnet-id")) {
        subnet_id = getUint32(lease_info, "subnet-id");
    }

    uint32_t pool_id = 0;
    if (lease_info->contains("pool-id")) {
        pool_id = getUint32(lease_info, "pool-id");
    }

    // Check if the subnet-id specified is sane.
    ConstSubnet4Ptr subnet;
    if (subnet_id) {
        // If subnet-id is specified, it has to match.
        subnet = cfg->getCfgSubnets4()->getBySubnetId(subnet_id);
        if (!subnet) {
            isc_throw(LeaseCmdsConflict, "Invalid subnet-id: No IPv4 subnet with subnet-id="
                      << subnet_id << " currently configured.");
        }

        // Check if the address specified really belongs to the subnet.
        if (!subnet->inRange(addr)) {
            isc_throw(LeaseCmdsConflict, "The address " << addr.toText() << " does not belong "
                      "to subnet " << subnet->toText() << ", subnet-id=" << subnet_id);
        }

    } else {
        // Subnet-id was not specified. Let's try to figure it out on our own.
        subnet = cfg->getCfgSubnets4()->selectSubnet(addr);
        if (!subnet) {
            isc_throw(LeaseCmdsConflict, "subnet-id not specified and failed to find a"
                      << " subnet for address " << addr);
        }
        subnet_id = subnet->getID();
    }

    // Client-id is optional.
    ClientIdPtr client_id;
    if (lease_info->contains("client-id")) {
        string txt = getString(lease_info, "client-id");
        client_id = ClientId::fromText(txt);
    }

    // These parameters are optional. If not specified, we'll derive them from
    // the current subnet configuration, if possible.
    uint32_t valid_lft = 0;
    if (lease_info->contains("valid-lft")) {
        valid_lft = getUint32(lease_info, "valid-lft");
    } else {
        valid_lft = subnet->getValid();
    }

    /// Let's calculate client last transmission time (cltt). If expiration
    /// timestamp is specified explicitly, we will use that. Note there are no
    /// checks whether this is in the past. There may be valid cases when user
    /// wants to insert expired leases, e.g. when migrating from one DHCP server
    /// to another and wants to migrate the database as is, without discarding
    /// any leases.
    time_t cltt;
    if (lease_info->contains("expire")) {
        int64_t expire_time = getInteger(lease_info, "expire");
        if (expire_time <= 0) {
            isc_throw(BadValue , "expiration time must be positive for address "
                      << addr);

        } else if (expire_time < valid_lft) {
            isc_throw(BadValue, "expiration time must be greater than valid lifetime"
                      " for address " << addr);
        }
        cltt = static_cast<time_t>(expire_time - valid_lft);
    } else {
        cltt = time(NULL);
    }

    bool fqdn_fwd = false;
    if (lease_info->contains("fqdn-fwd")) {
        fqdn_fwd = getBoolean(lease_info, "fqdn-fwd");
    }
    bool fqdn_rev = false;
    if (lease_info->contains("fqdn-rev")) {
        fqdn_rev = getBoolean(lease_info, "fqdn-rev");
    }
    string hostname;
    if (lease_info->contains("hostname")) {
        hostname = getString(lease_info, "hostname");
    }
    if (hostname.empty() && (fqdn_fwd || fqdn_rev)) {
        isc_throw(BadValue, "No hostname specified and either forward or reverse"
                  " fqdn was set to true.");
    }

    uint32_t state = 0;
    if (lease_info->contains("state")) {
        state = getUint8(lease_info, "state");
    }

    // Check if the state value is sane.
    if (state > Lease::STATE_REGISTERED) {
        isc_throw(BadValue, "Invalid state value: " << state << ", supported "
                  "values are: 0 (default), 1 (declined), 2 (expired-reclaimed),"
                  " 3 (released) and 4 (registered)");
    }

    // Handle user context.
    ConstElementPtr ctx = lease_info->get("user-context");
    if (ctx && (ctx->getType() != Element::map)) {
        isc_throw(BadValue, "Invalid user context '" << ctx->str()
                  << "' is not a JSON map.");
    }

    // Handle comment.
    ConstElementPtr comment = lease_info->get("comment");
    if (comment) {
        if (ctx && ctx->contains("comment")) {
            isc_throw(BadValue, "Duplicated comment entry '" << comment->str()
                      << "' in user context '" << ctx->str() << "'");
        }
        ElementPtr copied;
        if (ctx) {
            copied = copy(ctx, 0);
        } else {
            copied = Element::createMap();
        }
        copied->set("comment", comment);
        ctx = copied;
    }

    // Let's fabricate some data and we're ready to go.
    Lease4Ptr l(new Lease4(addr, hwaddr_ptr, client_id, valid_lft,
                           cltt, subnet_id,
                           fqdn_fwd, fqdn_rev, hostname));
    l->state_ = state;
    l->setContext(ctx);
    l->pool_id_ = pool_id;

    // Sanitize extended info.
    if (ctx) {
        auto check = cfg->getConsistency()->getExtendedInfoSanityCheck();
        LeaseMgr::upgradeLease4ExtendedInfo(l, check);
        LeaseMgr::extractLease4ExtendedInfo(l);
    }

    // Retrieve the optional flag indicating if the lease must be created when it
    // doesn't exist during the update.
    force_create = false;
    if (lease_info->contains("force-create")) {
        force_create = getBoolean(lease_info, "force-create");
    }

    return (l);
}

Lease6Ptr
Lease6Parser::parse(ConstSrvConfigPtr& cfg,
                    const ConstElementPtr& lease_info,
                    bool& force_create) {
    if (!lease_info) {
        isc_throw(BadValue, "lease information missing");
    }

    // These are mandatory parameters.
    IOAddress addr = getAddress(lease_info, "ip-address");
    if (addr.isV4()) {
        isc_throw(BadValue, "Non-IPv6 address specified: " << addr);
    }

    // Not a most straightforward conversion, but it works.
    string duid_txt = getString(lease_info, "duid");
    DUID duid = DUID::fromText(duid_txt);
    DuidPtr duid_ptr = DuidPtr(new DUID(duid));

    Lease::Type type = Lease::TYPE_NA;
    uint8_t prefix_len = 128;
    if (lease_info->contains("type")) {
        string txt = getString(lease_info, "type");
        if (txt == "IA_NA") {
            type = Lease::TYPE_NA;
        } else if (txt == "IA_TA") {
            type = Lease::TYPE_TA;
        } else if (txt == "IA_PD") {
            type = Lease::TYPE_PD;

            prefix_len = getUint8(lease_info, "prefix-len");
        } else {
            isc_throw(BadValue, "Incorrect lease type: " << txt << ", the only "
                      "supported values are: IA_NA and IA_PD");
        }
    }

    // Now sort out the subnet-id. If specified, it must have correct value.
    // If not specified, Kea will try to sort it out.
    SubnetID subnet_id = 0;
    if (lease_info->contains("subnet-id")) {
        subnet_id = getUint32(lease_info, "subnet-id");
    }

    uint32_t pool_id = 0;
    if (lease_info->contains("pool-id")) {
        pool_id = getUint32(lease_info, "pool-id");
    }

    // Check if the subnet-id specified is sane.
    ConstSubnet6Ptr subnet;
    if (subnet_id) {
        // If subnet-id is specified, it has to match.
        subnet = cfg->getCfgSubnets6()->getBySubnetId(subnet_id);
        if (!subnet) {
            isc_throw(LeaseCmdsConflict, "Invalid subnet-id: No IPv6 subnet with subnet-id="
                      << subnet_id << " currently configured.");
        }

        // Check if the address specified really belongs to the subnet.
        if ((type == Lease::TYPE_NA) && !subnet->inRange(addr)) {
            isc_throw(LeaseCmdsConflict, "The address " << addr.toText() << " does not belong "
                      "to subnet " << subnet->toText() << ", subnet-id=" << subnet_id);
        }

    } else {
        if (type != Lease::TYPE_NA) {
            isc_throw(BadValue, "Subnet-id is 0 or not specified. This is allowed for"
                      " address leases only, not prefix leases.");
        }
        // Subnet-id was not specified. Let's try to figure it out on our own.
        subnet = cfg->getCfgSubnets6()->selectSubnet(addr);
        if (!subnet) {
            isc_throw(LeaseCmdsConflict, "subnet-id not specified and failed to find a "
                      "subnet for address " << addr);
        }
        subnet_id = subnet->getID();
    }

    uint32_t iaid = getUint32(lease_info, "iaid");

    // Hw-address is optional in v6 leases.
    HWAddrPtr hwaddr_ptr;
    if (lease_info->contains("hw-address")) {
        string hwaddr_txt = getString(lease_info, "hw-address");
        HWAddr hwaddr = HWAddr::fromText(hwaddr_txt);
        hwaddr_ptr = HWAddrPtr(new HWAddr(hwaddr));
    }

    // These parameters are optional. If not specified, we'll derive them
    // from the current subnet configuration, if possible.
    uint32_t valid_lft = 0;
    if (lease_info->contains("valid-lft")) {
        valid_lft = getUint32(lease_info, "valid-lft");
    } else {
        valid_lft = subnet->getValid();
    }

    // These parameters are optional. If not specified, we'll derive them
    // from the current subnet configuration, if possible.
    uint32_t pref_lft = 0;
    if (lease_info->contains("preferred-lft")) {
        pref_lft = getUint32(lease_info, "preferred-lft");
    } else {
        pref_lft = subnet->getValid();
    }

    /// Let's calculate client last transmission time (cltt). If expiration
    /// timestamp is specified explicitly, we will use that. Note there are
    /// no checks whether this is in the past. There may be valid cases when
    /// user wants to insert expired leases, e.g. when migrating from one
    /// DHCP server to another and wants to migrate the database as is, without
    /// discarding any leases.
    time_t cltt;
    if (lease_info->contains("expire")) {
        int64_t expire_time = getInteger(lease_info, "expire");
        if (expire_time <= 0) {
            isc_throw(BadValue , "expiration time must be positive for address "
                      << addr);

        } else if (expire_time < valid_lft) {
            isc_throw(BadValue, "expiration time must be greater than valid lifetime"
                      " for address " << addr);
        }

        cltt = static_cast<time_t>(expire_time - valid_lft);
    } else {
        cltt = time(NULL);
    }

    bool fqdn_fwd = false;
    if (lease_info->contains("fqdn-fwd")) {
        fqdn_fwd = getBoolean(lease_info, "fqdn-fwd");
    }
    bool fqdn_rev = false;
    if (lease_info->contains("fqdn-rev")) {
        fqdn_rev = getBoolean(lease_info, "fqdn-rev");
    }
    string hostname;
    if (lease_info->contains("hostname")) {
        hostname = getString(lease_info, "hostname");
    }
    if (hostname.empty() && (fqdn_fwd || fqdn_rev)) {
        isc_throw(BadValue, "No hostname specified and either forward or reverse"
                  " fqdn was set to true.");
    }

    uint32_t state = 0;
    if (lease_info->contains("state")) {
        state = getUint8(lease_info, "state");
    }

    // Check if the state value is sane.
    if (state > Lease::STATE_REGISTERED) {
        isc_throw(BadValue, "Invalid state value: " << state << ", supported "
                  "values are: 0 (default), 1 (declined), 2 (expired-reclaimed),"
                  " 3 (released) and 4 (registered)");
    }

    if ((state == Lease::STATE_DECLINED) && (type == Lease::TYPE_PD)) {
        isc_throw(isc::InvalidOperation,
                  "Invalid declined state for PD prefix.");
    }

    // Handle user context.
    ConstElementPtr ctx = lease_info->get("user-context");
    if (ctx && (ctx->getType() != Element::map)) {
        isc_throw(BadValue, "Invalid user context '" << ctx->str()
                  << "' is not a JSON map.");
    }

    // Handle comment.
    ConstElementPtr comment = lease_info->get("comment");
    if (comment) {
        if (ctx && ctx->contains("comment")) {
            isc_throw(BadValue, "Duplicated comment entry '" << comment->str()
                      << "' in user context '" << ctx->str() << "'");
        }
        ElementPtr copied;
        if (ctx) {
            copied = copy(ctx, 0);
        } else {
            copied = Element::createMap();
        }
        copied->set("comment", comment);
        ctx = copied;
    }

    // Check if the prefix length is sane
    if (prefix_len == 0 || prefix_len > 128) {
        isc_throw(BadValue, "Invalid prefix length: "
                  << static_cast<unsigned>(prefix_len));
    }

    if (prefix_len != 128) {
        IOAddress first_address = firstAddrInPrefix(addr, prefix_len);
        if (first_address != addr) {
            isc_throw(BadValue, "Prefix address: " << addr
                      << " exceeds prefix/prefix-len pair: " << first_address
                      << "/" << static_cast<uint32_t>(prefix_len));
        }
    }

    // Let's fabricate some data and we're ready to go.
    Lease6Ptr l(new Lease6(type, addr, duid_ptr, iaid, pref_lft, valid_lft,
                           subnet_id, fqdn_fwd, fqdn_rev, hostname,
                           hwaddr_ptr, prefix_len));
    l->cltt_ = cltt;
    l->state_ = state;
    l->setContext(ctx);
    l->pool_id_ = pool_id;

    // Sanitize extended info.
    if (ctx) {
        auto check = cfg->getConsistency()->getExtendedInfoSanityCheck();
        LeaseMgr::upgradeLease6ExtendedInfo(l, check);
    }

    // Retrieve the optional flag indicating if the lease must be created when it
    // doesn't exist during the update.
    force_create = false;
    if (lease_info->contains("force-create")) {
        force_create = getBoolean(lease_info, "force-create");
    }

    return (l);
}

} // end of namespace lease_cmds
} // end of namespace isc
