/*
 * Copyright (c) 2025, Canonical, Ltd.
 * Copyright (c) 2025, STACKIT GmbH & Co. KG
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at:
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <config.h>

#include <net/if.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include "vswitch-idl.h"
#include "openvswitch/hmap.h"
#include "openvswitch/vlog.h"
#include "openvswitch/ofp-parse.h"

#include "lib/ovn-sb-idl.h"
#include "lib/ovn-util.h"
#include "lib/uuidset.h"

#include "binding.h"
#include "ha-chassis.h"
#include "local_data.h"
#include "route.h"

#include "route-table.h"

VLOG_DEFINE_THIS_MODULE(exchange);

#define PRIORITY_DEFAULT 1000
#define PRIORITY_LOCAL_BOUND 100

/* Discover the veth peer interface name of 'iface' using the
 * status:peer_ifindex value that OVS populates for veth devices.
 *
 * Returns the peer interface name, or NULL if 'iface' is not a veth device
 * or if the peer ifindex does not resolve to an interface in this
 * namespace.
 *
 * Caller must free the returned string.
 */
static char *
find_veth_peer(const struct ovsrec_interface *iface)
{
    if (!iface) {
        return NULL;
    }

    /* Only veth devices have status:peer_ifindex set. */
    const char *peer_ifindex_str = smap_get(&iface->status, "peer_ifindex");
    if (!peer_ifindex_str) {
        return NULL;
    }

    unsigned int peer_ifindex;
    if (!str_to_uint(peer_ifindex_str, 10, &peer_ifindex) || !peer_ifindex) {
        return NULL;
    }

    /* Resolve the peer ifindex in ovn-controller namespace. */
    char peer_ifname[IFNAMSIZ];
    if (!if_indextoname(peer_ifindex, peer_ifname)) {
        return NULL;
    }

    return xstrdup(peer_ifname);
}

/* Per-candidate state for an LB-derived Advertised_Route that is eligible for
 * Service_Monitor gating. */
struct lb_route_gate {
    struct hmap_node node;
    const struct sbrec_advertised_route *route;
    bool seen_monitor;
    bool any_online;
};

static bool
route_has_health_checks(const struct sbrec_advertised_route *route)
{
    return smap_get(&route->external_ids, OVN_AR_HEALTH_CHECKS_KEY) != NULL;
}

static bool
route_is_distributed_lb(const struct sbrec_advertised_route *route)
{
    const char *source = smap_get(&route->external_ids, OVN_AR_SOURCE_ID);
    return source && !strcmp(source, "lb") &&
           smap_get_bool(&route->external_ids,
                         OVN_AR_DISTRIBUTED_LB_ID, false);
}

static bool
route_advertising_port_is_local(
    const struct sbrec_advertised_route *route,
    struct ovsdb_idl_index *sbrec_port_binding_by_name,
    const struct sbrec_chassis *chassis)
{
    return lport_is_local(sbrec_port_binding_by_name, chassis,
                          route->logical_port->logical_port);
}

static bool
route_is_local_only_eligible(
    const struct sbrec_advertised_route *route,
    struct ovsdb_idl_index *sbrec_port_binding_by_name,
    const struct sbrec_chassis *chassis, bool *tracked_port_local)
{
    if (!route->tracked_port) {
        *tracked_port_local = false;
        return true;
    }

    *tracked_port_local = lport_is_local(
        sbrec_port_binding_by_name, chassis,
        route->tracked_port->logical_port);
    return *tracked_port_local ||
           !smap_get_bool(&route->logical_port->options,
                          "dynamic-routing-redistribute-local-only", false);
}

/* Build gates only for routes that northd annotated with health selectors. */
static void
build_lb_route_gates(struct hmap *gates,
                     const struct sbrec_advertised_route_table *ar_table,
                     struct hmap *announce_routes,
                     struct ovsdb_idl_index *sbrec_port_binding_by_name,
                     const struct sbrec_chassis *chassis,
                     struct sset *health_check_ports)
{
    const struct sbrec_advertised_route *route;
    SBREC_ADVERTISED_ROUTE_TABLE_FOR_EACH (route, ar_table) {
        struct advertise_datapath_entry *ad =
            advertise_datapath_find(announce_routes, route->datapath);
        if (!ad) {
            continue;
        }
        if (!route_is_distributed_lb(route)) {
            continue;
        }
        if (!route->tracked_port || !route_has_health_checks(route)) {
            continue;
        }
        if (!route_advertising_port_is_local(
                route, sbrec_port_binding_by_name, chassis)) {
            continue;
        }

        bool tracked_port_local;
        if (!route_is_local_only_eligible(
                route, sbrec_port_binding_by_name, chassis,
                &tracked_port_local)) {
            continue;
        }

        struct lb_route_gate *g = xmalloc(sizeof *g);
        *g = (struct lb_route_gate) {
            .route = route,
            .seen_monitor = false,
            .any_online = false,
        };
        hmap_insert(gates, &g->node, hash_pointer(route, 0));
        sset_add(health_check_ports, route->tracked_port->logical_port);
    }
}

static void
destroy_lb_route_gates(struct hmap *gates)
{
    struct lb_route_gate *g;
    HMAP_FOR_EACH_POP (g, node, gates) {
        free(g);
    }
    hmap_destroy(gates);
}

/* Resolve northd-provided protocol,backend-IP,backend-port selectors against
 * Service_Monitor rows with the same logical port, port and protocol.  IP is
 * compared separately in binary form so equivalent IPv6 spellings match. */
static void
evaluate_lb_route_gates(struct hmap *gates,
                        struct ovsdb_idl_index *service_monitor_by_selector,
                        struct uuidset *relevant_service_monitors)
{
    struct lb_route_gate *g;
    HMAP_FOR_EACH (g, node, gates) {
        const char *checks = smap_get(&g->route->external_ids,
                                      OVN_AR_HEALTH_CHECKS_KEY);
        if (!checks) {
            continue;
        }

        char *buf = xstrdup(checks);
        char *save_ptr = NULL;
        char *selector;
        for (selector = strtok_r(buf, ";", &save_ptr);
             selector; selector = strtok_r(NULL, ";", &save_ptr)) {

            char *protocol = selector;
            char *backend_ip = strchr(protocol, ',');
            if (!backend_ip) {
                continue;
            }
            *backend_ip++ = '\0';
            char *port_s = strchr(backend_ip, ',');
            if (!port_s) {
                continue;
            }
            *port_s++ = '\0';

            unsigned int backend_port;
            if (!str_to_uint(port_s, 10, &backend_port) ||
                backend_port > UINT16_MAX) {
                continue;
            }

            struct in6_addr backend_ip_addr;
            if (!ip46_parse(backend_ip, &backend_ip_addr)) {
                continue;
            }

            const char *tracked_lp = g->route->tracked_port->logical_port;
            struct sbrec_service_monitor *filter =
                sbrec_service_monitor_index_init_row(
                    service_monitor_by_selector);
            sbrec_service_monitor_index_set_logical_port(filter, tracked_lp);
            sbrec_service_monitor_index_set_type(filter, "load-balancer");
            sbrec_service_monitor_index_set_protocol(filter, protocol);
            sbrec_service_monitor_index_set_port(filter, backend_port);

            const struct sbrec_service_monitor *monitor;
            SBREC_SERVICE_MONITOR_FOR_EACH_EQUAL (
                monitor, filter, service_monitor_by_selector) {
                struct in6_addr sm_ip;
                if (!ip46_parse(monitor->ip, &sm_ip) ||
                    !ipv6_addr_equals(&sm_ip, &backend_ip_addr)) {
                    continue;
                }

                uuidset_insert(relevant_service_monitors,
                               &monitor->header_.uuid);
                g->seen_monitor = true;
                g->any_online |= monitor->status &&
                                 !strcmp(monitor->status, "online");
            }

            sbrec_service_monitor_index_destroy_row(filter);

            if (g->any_online) {
                break;
            }
        }
        free(buf);
    }
}

/* Look up the gate decision for a specific route. Returns:
 *  -1 if no gate exists (route is not LB-derived)
 *   0 if gate says withdraw (seen_monitor && !any_online)
 *   1 if gate says install */
static int
lb_route_gate_decision(const struct hmap *gates,
                       const struct sbrec_advertised_route *route)
{
    const struct lb_route_gate *g;
    HMAP_FOR_EACH_WITH_HASH (g, node, hash_pointer(route, 0), gates) {
        if (g->route == route) {
            if (g->seen_monitor && !g->any_online) {
                return 0;
            }
            return 1;
        }
    }
    return -1;
}

static bool
route_exchange_relevant_port(const struct sbrec_port_binding *pb)
{
    return pb && smap_get_bool(&pb->options, "dynamic-routing", false);
}

uint32_t
advertise_route_hash(const struct in6_addr *dst,
                     const struct in6_addr *nexthop, unsigned int plen)
{
    uint32_t hash = hash_add_in6_addr(0, dst);
    hash = hash_add_in6_addr(hash, nexthop);
    return hash_int(plen, hash);
}

struct advertise_route_entry
advertise_route_from_route_data(const struct route_data *rd)
{
    struct advertise_route_entry re = (struct advertise_route_entry) {
        .addr = rd->rta_dst,
        .plen = rd->rtm_dst_len,
        .priority = rd->rta_priority,
    };

    if (!ovs_list_is_empty(&rd->nexthops)) {
        const struct route_data_nexthop *rdnh =
            CONTAINER_OF(ovs_list_front(&rd->nexthops),
                         const struct route_data_nexthop, nexthop_node);
        re.nexthop = rdnh->addr;
    }

    return re;
}

const struct sbrec_port_binding*
route_exchange_find_port(struct ovsdb_idl_index *sbrec_port_binding_by_name,
                         const struct sbrec_chassis *chassis,
                         const struct sbrec_port_binding *pb,
                         const char **dynamic_routing_port_name)
{
    if (dynamic_routing_port_name) {
        *dynamic_routing_port_name = NULL;
    }

    if (!pb) {
        return NULL;
    }
    if (route_exchange_relevant_port(pb)) {
        if (dynamic_routing_port_name) {
            *dynamic_routing_port_name =
                smap_get(&pb->options, "dynamic-routing-port-name");
        }
        return pb;
    }

    const struct sbrec_port_binding *cr_pb =
        lport_get_cr_port(sbrec_port_binding_by_name, pb, NULL);

    if (!cr_pb) {
        return NULL;
    }

    if (dynamic_routing_port_name) {
        *dynamic_routing_port_name =
            smap_get(&cr_pb->options, "dynamic-routing-port-name");
    }

    if (!lport_pb_is_chassis_resident(chassis, cr_pb)) {
        return NULL;
    }

    if (route_exchange_relevant_port(cr_pb)) {
        return cr_pb;
    }
    return NULL;
}

struct advertise_datapath_entry *
advertise_datapath_find(const struct hmap *datapaths,
                        const struct sbrec_datapath_binding *db)
{
    struct advertise_datapath_entry *ade;
    HMAP_FOR_EACH_WITH_HASH (ade, node, db->tunnel_key, datapaths) {
        if (ade->db == db) {
            return ade;
        }
    }
    return NULL;
}

static void
build_port_mapping(struct smap *mapping, const char *port_mapping)
{
    if (!port_mapping) {
        return;
    }

    char *tokstr, *orig, *key, *value;

    orig = tokstr = xstrdup(port_mapping);
    while (ofputil_parse_key_value(&tokstr, &key, &value)) {
        if (!*value) {
          static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
          VLOG_WARN_RL(&rl, "dynamic-routing-port-mapping setting '%s' is "
                            "not valid.", tokstr);
          break;
        }
        smap_add(mapping, key, value);
    }
    free(orig);
}

/* Looks up the OVS interface locally bound to logical port 'port_name'.
 * Returns NULL if 'port_name' has no local binding on this chassis or
 * if the port binding is not resident on 'chassis'. */
static const struct ovsrec_interface *
local_iface_for_port_name(struct shash *local_bindings,
                          const struct sbrec_chassis *chassis,
                          const char *port_name)
{
    const struct binding_lport *b_lport =
        local_binding_get_primary_lport(local_binding_find(local_bindings,
                                                           port_name));

    if (!b_lport || !lport_pb_is_chassis_resident(chassis, b_lport->pb)) {
        return NULL;
    }

    return b_lport->lbinding->iface;
}

static const char *
ifname_from_port_name(const struct smap *port_mapping,
                      struct shash *local_bindings,
                      const struct sbrec_chassis *chassis,
                      const char *port_name)
{
    const char *iface = smap_get(port_mapping, port_name);
    if (iface) {
        return iface;
    }

    const struct ovsrec_interface *ovs_iface =
        local_iface_for_port_name(local_bindings, chassis, port_name);

    return ovs_iface ? ovs_iface->name : NULL;
}

/* Resolves the veth peer interface name for the Logical Switch Port referred
 * to by the LRP 'routing-protocol-redirect' option ('redirect_port').
 * Returns NULL, without logging, if 'redirect_port' is not bound locally,
 * since some other ovn-controller is expected to handle it. Returns NULL,
 * after logging, if 'redirect_port' is bound locally but its interface is
 * not a veth device or its peer cannot be resolved.
 *
 * Caller must free the returned string.
 */
static char *
find_veth_peer_for_redirect_port(struct shash *local_bindings,
                                 const struct sbrec_chassis *chassis,
                                 const char *redirect_port)
{
    const struct ovsrec_interface *iface =
        local_iface_for_port_name(local_bindings, chassis, redirect_port);
    if (!iface) {
        return NULL;
    }

    char *peer_iface = find_veth_peer(iface);

    static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 20);
    if (peer_iface) {
        VLOG_DBG_RL(&rl, "Auto-discovered veth peer '%s' for port '%s' "
                     "(bound to '%s')", peer_iface, redirect_port,
                     iface->name);
    } else {
        VLOG_DBG_RL(&rl, "Cannot auto-discover veth peer for port '%s' "
                    "(bound to '%s'), falling back to learning routes "
                    "from all ports", redirect_port, iface->name);
    }

    return peer_iface;
}

/* Determines which local interface, if any, route learning for local_peer
 * should be scoped to, and records it in ad->bound_ports (keyed by the
 * peer logical port).  The value is:
 *  - the resolved interface name, if learning must be filtered to a
 *    specific interface (an explicit dynamic-routing-port-name, or a veth
 *    peer auto-discovered from routing-protocol-redirect); or
 *  - NULL, if routes should be learned from all interfaces on this LRP.
 *
 * A port referenced via dynamic-routing-port-name or via
 * routing-protocol-redirect is added to filtered_ports so that changes to
 * its binding trigger a recompute.
 *
 * Returns true if a specific interface filter was applied for this peer
 * (i.e. the value stored in bound_ports is non-NULL), false otherwise. */
static bool
route_add_bound_port(struct route_ctx_in *r_ctx_in,
                     struct route_ctx_out *r_ctx_out,
                     struct advertise_datapath_entry *ad,
                     const struct sbrec_port_binding *repb,
                     const struct sbrec_port_binding *local_peer,
                     const char *port_name,
                     const struct smap *port_mapping)
{
    /* An explicit dynamic-routing-port-name filters learning to the name
     * from the port-mapping or the interface name of the local binding. If
     * the port is neither in the port-mapping nor bound locally we will not
     * learn routes for this port. */
    if (port_name) {
        const char *ifname = ifname_from_port_name(
            port_mapping, r_ctx_in->local_bindings,
            r_ctx_in->chassis, port_name);
        if (ifname) {
            smap_add(&ad->bound_ports, local_peer->logical_port, ifname);
        }
        sset_add(r_ctx_out->filtered_ports, port_name);
        return true;
    }

    /* No explicit port-name. If routing-protocol-redirect points to a LSP
     * that is bound locally and connected through a veth pair, auto-discover
     * its peer interface and use it to scope route learning, without
     * requiring dynamic-routing-port-name/port-mapping to be configured
     * manually. */
    char *peer_iface = NULL;
    const char *redirect_port = smap_get(&repb->options,
                                         "routing-protocol-redirect");
    if (redirect_port) {
        /* Track redirect_port so that we recompute if its binding changes. */
        sset_add(r_ctx_out->filtered_ports, redirect_port);

        peer_iface = find_veth_peer_for_redirect_port(
            r_ctx_in->local_bindings, r_ctx_in->chassis, redirect_port);
    }

    /* If auto-discovery succeeded, peer_iface scopes learning to that
     * interface (like an explicit dynamic-routing-port-name would).
     * Otherwise peer_iface is NULL and we learn routes from all
     * interfaces on this LRP.  Either way bound_ports takes ownership of
     * peer_iface. */
    smap_add_nocopy(&ad->bound_ports, xstrdup(local_peer->logical_port),
                    peer_iface);
    return peer_iface != NULL;
}

static void
advertise_datapath_cleanup(struct advertise_datapath_entry *ad)
{
    struct advertise_route_entry *ar;
    HMAP_FOR_EACH_SAFE (ar, node, &ad->routes) {
        hmap_remove(&ad->routes, &ar->node);
        free(ar);
    }
    hmap_destroy(&ad->routes);
    smap_destroy(&ad->bound_ports);
    free(ad);
}

static struct advertise_datapath_entry *
advertised_datapath_alloc(const struct sbrec_datapath_binding *datapath)
{
    struct advertise_datapath_entry *ad = xmalloc(sizeof *ad);
    *ad = (struct advertise_datapath_entry) {
        .db = datapath,
        .routes = HMAP_INITIALIZER(&ad->routes),
        .bound_ports = SMAP_INITIALIZER(&ad->bound_ports),
    };

    static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 10);

    const char *nh4 =
        smap_get(&datapath->external_ids, "dynamic-routing-v4-prefix-nexthop");
    if (nh4 && !ip46_parse(nh4, &ad->ipv4_nexthop)) {
        memset(&ad->ipv4_nexthop, 0, sizeof ad->ipv4_nexthop);
        VLOG_WARN_RL(&rl, "Couldn't parse IPv4 prefix nexthop %s, "
                     "routes will be installed as blackhole.", nh4);
    }

    const char *nh6 =
        smap_get(&datapath->external_ids, "dynamic-routing-v6-prefix-nexthop");
    if (nh6 && !ipv6_parse(nh6, &ad->ipv6_nexthop)) {
        memset(&ad->ipv6_nexthop, 0, sizeof ad->ipv6_nexthop);
        VLOG_WARN_RL(&rl, "Couldn't parse IPv6 prefix nexthop %s, "
                     "routes will be installed as blackhole.", nh6);
    }

    return ad;
}

static void
route_record_status(struct route_ctx_out *r_ctx_out,
                    const struct sbrec_advertised_route *route,
                    const char *desired_status,
                    const char *withdrawal_reason,
                    const char *withdrawal_reason_value)
{
    struct advertised_route_status status = {
        .route = route,
        .desired_status = desired_status,
        .withdrawal_reason = withdrawal_reason,
        .withdrawal_reason_value = withdrawal_reason_value,
    };
    vector_push(r_ctx_out->advertised_route_status, &status);
}

void
advertised_route_status_clear(struct vector *statuses)
{
    vector_clear(statuses);
}

static int
advertised_route_status_cmp(const void *a_, const void *b_)
{
    const struct advertised_route_status *a = a_;
    const struct advertised_route_status *b = b_;

    return uuid_compare_3way(&a->route->header_.uuid,
                             &b->route->header_.uuid);
}

void
advertised_route_status_sort(struct vector *statuses)
{
    vector_qsort(statuses, advertised_route_status_cmp);
}

const struct advertised_route_status *
advertised_route_status_find(const struct vector *statuses,
                             const struct sbrec_advertised_route *route)
{
    if (!route) {
        return NULL;
    }

    const struct advertised_route_status key = {
        .route = route,
    };
    return vector_bsearch(statuses, &key, advertised_route_status_cmp);
}

void
route_run(struct route_ctx_in *r_ctx_in,
          struct route_ctx_out *r_ctx_out)
{
    const struct local_datapath *ld;
    struct smap port_mapping = SMAP_INITIALIZER(&port_mapping);

    build_port_mapping(&port_mapping, r_ctx_in->dynamic_routing_port_mapping);

    HMAP_FOR_EACH (ld, hmap_node, r_ctx_in->local_datapaths) {
        if (vector_is_empty(&ld->peer_ports) || ld->is_switch) {
            continue;
        }
        struct advertise_datapath_entry *ad = NULL;
        bool lr_has_port_name_filter = false;

        /* This is a LR datapath, find LRPs with route exchange options
         * that are bound locally. */
        const struct peer_ports *peers;
        VECTOR_FOR_EACH_PTR (&ld->peer_ports, peers) {
            const struct sbrec_port_binding *local_peer = peers->local;
            const char *port_name;

            const struct sbrec_port_binding *repb =
                route_exchange_find_port(r_ctx_in->sbrec_port_binding_by_name,
                                         r_ctx_in->chassis,
                                         local_peer, &port_name);
            if (port_name) {
                lr_has_port_name_filter = true;
            }
            if (!repb) {
                continue;
            }

            if (!ad) {
                ad = advertised_datapath_alloc(ld->datapath);
            }

            ad->maintain_vrf |=
                smap_get_bool(&repb->options,
                              "dynamic-routing-maintain-vrf",
                              false);

            const char *vrf_name = smap_get(&repb->options,
                                            "dynamic-routing-vrf-name");
            if (vrf_name && strlen(vrf_name) >= IFNAMSIZ) {
                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 20);
                VLOG_WARN_RL(&rl, "Ignoring vrf name %s, since it is too long."
                             "Maximum length is %d characters", vrf_name,
                             IFNAMSIZ);
                vrf_name = NULL;
            }
            if (vrf_name) {
                memcpy(ad->vrf_name, vrf_name, strlen(vrf_name) + 1);
            } else {
                snprintf(ad->vrf_name, sizeof ad->vrf_name, "ovnvrf%"PRIu32,
                         route_get_table_id(ad->db));
            }

            lr_has_port_name_filter |= route_add_bound_port(
                r_ctx_in, r_ctx_out, ad, repb, local_peer, port_name,
                &port_mapping);
        }

        if (ad) {
            /* If at least one bound port has dynamic-routing-port-name
             * configured, ignore the ones that don't. */
            if (lr_has_port_name_filter) {
                struct smap_node *node;

                SMAP_FOR_EACH_SAFE (node, &ad->bound_ports) {
                    if (!node->value) {
                        smap_remove_node(&ad->bound_ports, node);
                    }
                }
            }

            tracked_datapath_add(ld->datapath, TRACKED_RESOURCE_NEW,
                                 r_ctx_out->tracked_re_datapaths);
            hmap_insert(r_ctx_out->announce_routes, &ad->node,
                        ad->db->tunnel_key);
        }
    }

    struct hmap lb_route_gates = HMAP_INITIALIZER(&lb_route_gates);
    if (!hmap_is_empty(r_ctx_out->announce_routes)) {
        build_lb_route_gates(&lb_route_gates,
                             r_ctx_in->advertised_route_table,
                             r_ctx_out->announce_routes,
                             r_ctx_in->sbrec_port_binding_by_name,
                             r_ctx_in->chassis,
                             r_ctx_out->health_check_ports);
    }

    if (!hmap_is_empty(&lb_route_gates)) {
        evaluate_lb_route_gates(&lb_route_gates,
                                r_ctx_in->service_monitor_by_selector,
                                r_ctx_out->relevant_service_monitors);
    }

    const struct sbrec_advertised_route *route;
    SBREC_ADVERTISED_ROUTE_TABLE_FOR_EACH (route,
                                           r_ctx_in->advertised_route_table) {
        struct advertise_datapath_entry *ad =
            advertise_datapath_find(r_ctx_out->announce_routes,
                                    route->datapath);
        if (!ad) {
            continue;
        }

        struct in6_addr prefix;
        unsigned int plen;
        if (!ip46_parse_cidr(route->ip_prefix, &prefix, &plen)) {
            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 20);
            VLOG_WARN_RL(&rl, "bad 'ip_prefix' %s in route "
                         UUID_FMT, route->ip_prefix,
                         UUID_ARGS(&route->header_.uuid));
            continue;
        }

        if (!route_advertising_port_is_local(
                route, r_ctx_in->sbrec_port_binding_by_name,
                r_ctx_in->chassis)) {
            sset_add(r_ctx_out->tracked_ports_remote,
                     route->logical_port->logical_port);
            continue;
        }
        sset_add(r_ctx_out->tracked_ports_local,
                 route->logical_port->logical_port);

        bool distributed_lb = route_is_distributed_lb(route);

        unsigned int priority = PRIORITY_DEFAULT;
        if (route->tracked_port) {
            bool tracked_port_local;
            bool local_only_eligible = route_is_local_only_eligible(
                route, r_ctx_in->sbrec_port_binding_by_name,
                r_ctx_in->chassis, &tracked_port_local);
            if (tracked_port_local) {
                priority = PRIORITY_LOCAL_BOUND;
                sset_add(r_ctx_out->tracked_ports_local,
                         route->tracked_port->logical_port);
            } else {
                sset_add(r_ctx_out->tracked_ports_remote,
                         route->tracked_port->logical_port);
            }

            /* A route not selected for this chassis must not get a
             * status row: evaluate local-only eligibility before any
             * administrative or health withdrawal so the documented
             * "selected for its chassis" meaning holds. */
            if (!local_only_eligible) {
                continue;
            }
        }

        if (distributed_lb &&
            !smap_get_bool(&route->external_ids, "enabled", true)) {
            route_record_status(r_ctx_out, route, "withdrawn",
                                "administrative-policy", "disabled");
            continue;
        }

        int gate = lb_route_gate_decision(&lb_route_gates, route);
        if (gate == 0) {
            if (distributed_lb) {
                route_record_status(r_ctx_out, route,
                                    "withdrawn", "service-monitor",
                                    "no-online-backend");
            }
            continue;
        }

        if (distributed_lb) {
            route_record_status(r_ctx_out, route, "advertised", NULL, NULL);
        }

        struct in6_addr nexthop = IN6_IS_ADDR_V4MAPPED(&prefix)
                ? ad->ipv4_nexthop : ad->ipv6_nexthop;
        if (advertise_route_find(priority, &prefix, plen, &nexthop,
                                 &ad->routes)) {
            continue;
        }

        struct advertise_route_entry *ar = xmalloc(sizeof(*ar));
        *ar = (struct advertise_route_entry) {
            .addr = prefix,
            .plen = plen,
            .priority = priority,
            .nexthop = nexthop,
        };
        hmap_insert(&ad->routes, &ar->node,
                    advertise_route_hash(&ar->addr, &ar->nexthop, plen));
    }

    destroy_lb_route_gates(&lb_route_gates);

    smap_destroy(&port_mapping);
}

void
route_cleanup(struct hmap *announce_routes)
{
    struct advertise_datapath_entry *ad;
    HMAP_FOR_EACH_POP (ad, node, announce_routes) {
        advertise_datapath_cleanup(ad);
    }
}

uint32_t
route_get_table_id(const struct sbrec_datapath_binding *dp)
{
    int64_t vrf_id = ovn_smap_get_llong(&dp->external_ids,
                                        "dynamic-routing-vrf-id", -1);
    return (vrf_id >= 1 && vrf_id <= UINT32_MAX) ? vrf_id : dp->tunnel_key;
}

struct advertise_route_entry *
advertise_route_find(unsigned int priority, const struct in6_addr *prefix,
                     unsigned int plen, const struct in6_addr *nexthop,
                     const struct hmap *advertised_routes)
{
    uint32_t hash = advertise_route_hash(prefix, nexthop, plen);
    struct advertise_route_entry *ar;
    HMAP_FOR_EACH_WITH_HASH (ar, node, hash, advertised_routes) {
        if (ipv6_addr_equals(&ar->addr, prefix) &&
            ipv6_addr_equals(&ar->nexthop, nexthop) &&
            ar->plen == plen &&
            ar->priority == priority) {
            return ar;
        }
    }
    return NULL;
}
