from __future__ import annotations

import ipaddress
from dataclasses import dataclass
from typing import TYPE_CHECKING

from infrahub.core.constants import InfrahubKind
from infrahub.core.graph.schema import GraphAttributeIPHostNode, GraphAttributeIPNetworkNode
from infrahub.core.ipam.constants import AllIPTypes, IPAddressType, IPNetworkType
from infrahub.core.query import Query, QueryResult, QueryType
from infrahub.core.registry import registry
from infrahub.core.utils import convert_ip_to_binary_str

if TYPE_CHECKING:
    from uuid import UUID

    from infrahub.core.node import Node
    from infrahub.database import InfrahubDatabase


PREFIX_ATTRIBUTE_LABEL = GraphAttributeIPNetworkNode.get_default_label()
ADDRESS_ATTRIBUTE_LABEL = GraphAttributeIPHostNode.get_default_label()


@dataclass
class IPPrefixData:
    id: UUID
    prefix: IPNetworkType


@dataclass
class IPAddressData:
    id: UUID
    address: IPAddressType


@dataclass(frozen=True)
class IPPrefixFreeData:
    free_start: int

    @classmethod
    def from_db(cls, result: QueryResult) -> IPPrefixFreeData:
        return cls(
            free_start=result.get_as_type("free_start", return_type=int),
        )


@dataclass(frozen=True)
class IPv6PrefixFreeData:
    free_start_bin: str

    @classmethod
    def from_db(cls, result: QueryResult) -> IPv6PrefixFreeData:
        return cls(
            free_start_bin=result.get_as_type("free_start_bin", return_type=str),
        )


@dataclass(frozen=True)
class IPAddressFreeData:
    free_addr: int
    is_free: bool
    is_last: bool

    @classmethod
    def from_db(cls, result: QueryResult) -> IPAddressFreeData:
        return cls(
            free_addr=result.get_as_type("free_addr", return_type=int),
            is_free=result.get_as_type("is_free", return_type=bool),
            is_last=result.get_as_type("is_last", return_type=bool),
        )


@dataclass(frozen=True)
class IPv6AddressFreeData:
    free_addr_bin: str
    is_free: bool
    is_last: bool

    @classmethod
    def from_db(cls, result: QueryResult) -> IPv6AddressFreeData:
        return cls(
            free_addr_bin=result.get_as_type("free_addr_bin", return_type=str),
            is_free=result.get_as_type("is_free", return_type=bool),
            is_last=result.get_as_type("is_last", return_type=bool),
        )


def _get_namespace_id(
    namespace: Node | str | None = None,
) -> str:
    if namespace and isinstance(namespace, str):
        return namespace
    if namespace and hasattr(namespace, "id"):
        return namespace.id
    return registry.default_ipnamespace


class IPPrefixSubnetFetch(Query):
    name = "ipprefix_subnet_fetch"
    type = QueryType.READ

    def __init__(
        self,
        obj: IPNetworkType,
        namespace: Node | str | None = None,
        **kwargs,
    ) -> None:
        self.obj = obj
        self.namespace_id = _get_namespace_id(namespace)

        super().__init__(**kwargs)

    async def query_init(self, db: InfrahubDatabase, **kwargs) -> None:  # noqa: ARG002
        self.params["ns_id"] = self.namespace_id

        prefix_bin = convert_ip_to_binary_str(self.obj)[: self.obj.prefixlen]
        self.params["prefix_binary"] = prefix_bin
        self.params["maxprefixlen"] = self.obj.prefixlen
        self.params["ip_version"] = self.obj.version

        branch_filter, branch_params = self.branch.get_query_filter_path(
            at=self.at.to_string(), branch_agnostic=self.branch_agnostic
        )
        self.params.update(branch_params)

        # ruff: noqa: E501
        query = """
        // First match on IPNAMESPACE
        MATCH (ns:%(ns_label)s)
        WHERE ns.uuid = $ns_id
        CALL (ns) {
            MATCH (ns)-[r:IS_PART_OF]-(root:Root)
            WHERE %(branch_filter)s
            RETURN r
            ORDER BY r.branch_level DESC, r.from DESC
            LIMIT 1
        }
        WITH ns, r
        WHERE r.status = "active"
        WITH ns
        // MATCH all prefixes that are IN SCOPE
        MATCH path2 = (ns)-[:IS_RELATED]-(ns_rel:Relationship)-[:IS_RELATED]-(pfx:%(node_label)s)-[:HAS_ATTRIBUTE]-(an:Attribute {name: "prefix"})-[:HAS_VALUE]-(av:AttributeIPNetwork)
        WHERE ns_rel.name = "ip_namespace__ip_prefix"
            AND av.binary_address STARTS WITH $prefix_binary
            AND av.prefixlen > $maxprefixlen
            AND av.version = $ip_version
            AND all(r IN relationships(path2) WHERE (%(branch_filter)s) and r.status = "active")
        WITH
            collect([pfx, av]) as all_prefixes_and_value,
            collect(pfx) as all_prefixes
        // ---
        // FIND ALL CHILDREN OF THESE PREFIXES
        // ---
        CALL (all_prefixes) {
            UNWIND all_prefixes as prefix
            OPTIONAL MATCH (prefix)<-[:IS_RELATED]-(ch_rel:Relationship)<-[:IS_RELATED]-(children:BuiltinIPPrefix)
            WHERE ch_rel.name = "parent__child"
            RETURN children
        }
        WITH collect( distinct children ) AS all_children, all_prefixes_and_value
        UNWIND all_prefixes_and_value as prefixes_to_check
        WITH prefixes_to_check, all_children
        WHERE not prefixes_to_check[0] in all_children
        """ % {
            "ns_label": InfrahubKind.IPNAMESPACE,
            "node_label": InfrahubKind.IPPREFIX,
            "branch_filter": branch_filter,
        }

        self.add_to_query(query)
        self.return_labels = ["prefixes_to_check[0] as pfx", "prefixes_to_check[1] as av"]
        self.order_by = ["av.binary_address"]

    def get_subnets(self) -> list[IPPrefixData]:
        """Return a list of all subnets fitting in the prefix."""
        subnets: list[IPPrefixData] = []

        for result in self.get_results():
            subnet = IPPrefixData(
                id=result.get("pfx").get("uuid"), prefix=ipaddress.ip_network(result.get("av").get("value"))
            )
            subnets.append(subnet)

        return subnets


class IPPrefixSubnetFetchFree(Query):
    name = "ipprefix_subnet_fetch_free"
    type = QueryType.READ
    raise_error_if_empty = False

    def __init__(
        self,
        obj: IPNetworkType,
        target_prefixlen: int,
        namespace: Node | str | None = None,
        **kwargs,
    ) -> None:
        self.obj = obj
        self.target_prefixlen = target_prefixlen
        self.namespace_id = _get_namespace_id(namespace)

        super().__init__(**kwargs)

    async def query_init(self, db: InfrahubDatabase, **kwargs) -> None:  # noqa: ARG002
        self.params["ns_id"] = self.namespace_id

        prefix_bin = convert_ip_to_binary_str(self.obj)[: self.obj.prefixlen]
        self.params["prefix_binary"] = prefix_bin
        self.params["maxprefixlen"] = self.obj.prefixlen
        self.params["ip_version"] = self.obj.version
        self.params["parent_start"] = int(self.obj.network_address)
        self.params["parent_end"] = int(self.obj.broadcast_address)
        self.params["block_size"] = 1 << (32 - self.target_prefixlen)

        branch_filter, branch_params = self.branch.get_query_filter_path(
            at=self.at.to_string(), branch_agnostic=self.branch_agnostic
        )
        self.params.update(branch_params)

        # ruff: noqa: E501
        query = """
        // First match on IPNAMESPACE
        MATCH (ns:%(ns_label)s)
        WHERE ns.uuid = $ns_id
        CALL (ns) {
            MATCH (ns)-[r:IS_PART_OF]-(root:Root)
            WHERE %(branch_filter)s
            RETURN r
            ORDER BY r.branch_level DESC, r.from DESC
            LIMIT 1
        }
        WITH ns, r
        WHERE r.status = "active"
        WITH ns
        OPTIONAL MATCH path2 = (ns)-[:IS_RELATED]-(ns_rel:Relationship)-[:IS_RELATED]-(pfx:%(node_label)s)-[:HAS_ATTRIBUTE]-(an:Attribute {name: "prefix"})-[:HAS_VALUE]-(av:AttributeIPNetwork)
        WHERE ns_rel.name = "ip_namespace__ip_prefix"
            AND av.binary_address STARTS WITH $prefix_binary
            AND av.prefixlen > $maxprefixlen
            AND av.version = $ip_version
            AND all(r IN relationships(path2) WHERE (%(branch_filter)s) AND r.status = "active")
        WITH collect({binary: av.binary_address, prefixlen: av.prefixlen}) AS ranges_raw
        // Convert binary strings to integer ranges for gap detection
        // - start: Convert binary string to decimal by iterating through each bit
        //          and accumulating (dec * 2 + bit_value), which is binary-to-decimal conversion
        // - end: start + (2^host_bits - 1), where host_bits = 32 - prefixlen
        //        This gives the broadcast address (last IP) of the prefix
        WITH [r IN ranges_raw |
                {
                    start: reduce(dec = 0, b IN split(r.binary, "") | dec * 2 + toInteger(b)),
                    end: reduce(dec = 0, b IN split(r.binary, "") | dec * 2 + toInteger(b)) + toInteger(2 ^ (32 - r.prefixlen)) - 1
                }
            ] AS ranges
        UNWIND CASE WHEN size(ranges) = 0 THEN [{start: null, end: null}] ELSE ranges END AS r
        WITH r
        ORDER BY r.start ASC
        WITH collect(r) AS ranges_sorted_raw
        WITH [r IN ranges_sorted_raw WHERE r.start IS NOT NULL] AS ranges_sorted
        // Gap detection algorithm using reduce to scan through sorted ranges
        // acc.cursor: current candidate position for a free block (always block-aligned)
        // acc.found: set when a gap is found, stops further iteration
        WITH reduce(acc = {cursor: $parent_start, found: null}, r IN ranges_sorted |
                CASE
                    // Already found a gap, preserve result
                    WHEN acc.found IS NOT NULL THEN acc
                    // Range ends before cursor, skip (range already passed)
                    WHEN r.end < acc.cursor THEN acc
                    // Gap found: cursor + block_size - 1 < range start means there's room before this range
                    WHEN acc.cursor + $block_size - 1 < r.start THEN {cursor: acc.cursor, found: acc.cursor}
                    // No gap: advance cursor past this range using ceiling division
                    // Formula: ceil((r.end + 1) / block_size) * block_size
                    // This aligns the cursor to the next block boundary after the range
                    ELSE
                        {
                            cursor: CASE
                                WHEN toInteger((r.end + 1 + $block_size - 1) / $block_size) * $block_size > $parent_end + 1
                                THEN $parent_end + 1
                                ELSE toInteger((r.end + 1 + $block_size - 1) / $block_size) * $block_size
                            END,
                            found: null
                        }
                END
            ) AS res
        WITH CASE
            WHEN res.found IS NOT NULL THEN res.found
            WHEN res.cursor + $block_size - 1 <= $parent_end THEN res.cursor
            ELSE NULL
        END AS free_start
        WHERE free_start IS NOT NULL
        """ % {
            "ns_label": InfrahubKind.IPNAMESPACE,
            "node_label": InfrahubKind.IPPREFIX,
            "branch_filter": branch_filter,
        }

        self.add_to_query(query)
        self.return_labels = ["free_start"]
        self.limit = 1

    def get_prefix_data(self) -> IPPrefixFreeData | None:
        result = self.get_result()
        if not result:
            return None

        return IPPrefixFreeData.from_db(result=result)


class IPv6PrefixSubnetFetchFree(Query):
    """Query to find the next free IPv6 prefix within a parent prefix.

    This query uses binary string operations to handle IPv6's 128-bit address space,
    as the integer values would overflow Neo4j's 64-bit integer type.
    """

    name = "ipv6prefix_subnet_fetch_free"
    type = QueryType.READ
    raise_error_if_empty = False

    def __init__(
        self,
        obj: IPNetworkType,
        target_prefixlen: int,
        namespace: Node | str | None = None,
        **kwargs,
    ) -> None:
        self.obj = obj
        self.target_prefixlen = target_prefixlen
        self.namespace_id = _get_namespace_id(namespace)

        super().__init__(**kwargs)

    async def query_init(self, db: InfrahubDatabase, **kwargs) -> None:  # noqa: ARG002
        self.params["ns_id"] = self.namespace_id

        prefix_bin = convert_ip_to_binary_str(self.obj)[: self.obj.prefixlen]
        self.params["prefix_binary"] = prefix_bin
        self.params["maxprefixlen"] = self.obj.prefixlen
        self.params["ip_version"] = self.obj.version
        self.params["target_prefixlen"] = self.target_prefixlen
        # Binary representation of parent network and broadcast addresses
        self.params["parent_start_bin"] = convert_ip_to_binary_str(self.obj)
        self.params["parent_end_bin"] = format(int(self.obj.broadcast_address), "0128b")

        branch_filter, branch_params = self.branch.get_query_filter_path(
            at=self.at.to_string(), branch_agnostic=self.branch_agnostic
        )
        self.params.update(branch_params)

        # ruff: noqa: E501
        query = """
        // First match on IPNAMESPACE
        MATCH (ns:%(ns_label)s)
        WHERE ns.uuid = $ns_id
        CALL (ns) {
            MATCH (ns)-[r:IS_PART_OF]-(root:Root)
            WHERE %(branch_filter)s
            RETURN r
            ORDER BY r.branch_level DESC, r.from DESC
            LIMIT 1
        }
        WITH ns, r
        WHERE r.status = "active"
        WITH ns
        OPTIONAL MATCH path2 = (ns)-[:IS_RELATED]-(ns_rel:Relationship)-[:IS_RELATED]-(pfx:%(node_label)s)-[:HAS_ATTRIBUTE]-(an:Attribute {name: "prefix"})-[:HAS_VALUE]-(av:AttributeIPNetwork)
        WHERE ns_rel.name = "ip_namespace__ip_prefix"
            AND av.binary_address STARTS WITH $prefix_binary
            AND av.prefixlen > $maxprefixlen
            AND av.version = $ip_version
            AND all(r IN relationships(path2) WHERE (%(branch_filter)s) AND r.status = "active")
        WITH collect({binary: av.binary_address, prefixlen: av.prefixlen}) AS ranges_raw
        // Create ranges with start (block-aligned) and end_block (the block containing the end address)
        WITH [r IN ranges_raw WHERE r.binary IS NOT NULL |
                {
                    // Start block: first target_prefixlen bits of network address
                    start_block: left(r.binary, $target_prefixlen),
                    // End block: first target_prefixlen bits of broadcast address
                    // For a prefix, broadcast = network with all host bits set to 1
                    end_block: left(left(r.binary, r.prefixlen) + reduce(s = "", i IN range(1, 128 - r.prefixlen) | s + "1"), $target_prefixlen)
                }
            ] AS ranges
        UNWIND CASE WHEN size(ranges) = 0 THEN [{start_block: null, end_block: null}] ELSE ranges END AS r
        WITH r
        ORDER BY r.start_block ASC
        WITH collect(r) AS ranges_sorted_raw
        WITH [r IN ranges_sorted_raw WHERE r.start_block IS NOT NULL] AS ranges_sorted
        // Find first available slot using binary string comparison
        // We track cursor as the current candidate block (target_prefixlen bits)
        WITH reduce(acc = {cursor: left($parent_start_bin, $target_prefixlen), found: null}, r IN ranges_sorted |
                CASE
                    WHEN acc.found IS NOT NULL THEN acc
                    // Range ends before cursor, skip it
                    WHEN r.end_block < acc.cursor THEN acc
                    // Gap found: cursor is before this range starts
                    WHEN acc.cursor < r.start_block THEN {cursor: acc.cursor, found: acc.cursor}
                    // Cursor overlaps with range: advance cursor past this range
                    // Binary string increment: add 1 to end_block to get the next block
                    // Algorithm: process bits right-to-left with carry propagation
                    // - Start with carry=1 (we're adding 1)
                    // - For each bit: if carry=0, keep bit unchanged
                    //   if carry=1 and bit="1", output "0" and carry remains 1
                    //   if carry=1 and bit="0", output "1" and carry becomes 0
                    // Note: if all bits are "1", result will be all "0"s with carry=1 (overflow)
                    // We track the final carry and set cursor to null if overflow occurred
                    ELSE
                        {
                            cursor: CASE
                                // Check for overflow: if final carry is 1, return null
                                WHEN reduce(
                                    inc = {bits: split(r.end_block, ""), carry: 1, result: []},
                                    idx IN reverse(range(0, $target_prefixlen - 1)) |
                                    {
                                        bits: inc.bits,
                                        carry: CASE
                                            WHEN inc.carry = 0 THEN 0
                                            WHEN inc.bits[idx] = "1" THEN 1
                                            ELSE 0
                                        END,
                                        result: CASE
                                            WHEN inc.carry = 0 THEN [inc.bits[idx]] + inc.result
                                            WHEN inc.bits[idx] = "1" THEN ["0"] + inc.result
                                            ELSE ["1"] + inc.result
                                        END
                                    }
                                ).carry = 1 THEN null
                                ELSE reduce(s = "", c IN reduce(
                                    inc = {bits: split(r.end_block, ""), carry: 1, result: []},
                                    idx IN reverse(range(0, $target_prefixlen - 1)) |
                                    {
                                        bits: inc.bits,
                                        carry: CASE
                                            WHEN inc.carry = 0 THEN 0
                                            WHEN inc.bits[idx] = "1" THEN 1
                                            ELSE 0
                                        END,
                                        result: CASE
                                            WHEN inc.carry = 0 THEN [inc.bits[idx]] + inc.result
                                            WHEN inc.bits[idx] = "1" THEN ["0"] + inc.result
                                            ELSE ["1"] + inc.result
                                        END
                                    }
                                ).result | s + c)
                            END,
                            found: null
                        }
                END
            ) AS res
        // Handle overflow case
        WITH
            CASE
                WHEN res.found IS NOT NULL THEN res.found
                ELSE res.cursor
            END AS cursor_str,
            res.found AS found
        // Check if we found a slot or if there's space after all ranges
        WITH CASE
            WHEN found IS NOT NULL THEN found
            // Check cursor is valid (not overflowed) and within parent range
            WHEN size(cursor_str) = $target_prefixlen AND cursor_str <= left($parent_end_bin, $target_prefixlen) THEN cursor_str
            ELSE NULL
        END AS free_start_partial
        WHERE free_start_partial IS NOT NULL
        // Pad the partial binary to full 128 bits
        WITH free_start_partial + reduce(s = "", i IN range(1, 128 - $target_prefixlen) | s + "0") AS free_start_bin
        """ % {
            "ns_label": InfrahubKind.IPNAMESPACE,
            "node_label": InfrahubKind.IPPREFIX,
            "branch_filter": branch_filter,
        }

        self.add_to_query(query)
        self.return_labels = ["free_start_bin"]
        self.limit = 1

    def get_prefix_data(self) -> IPv6PrefixFreeData | None:
        result = self.get_result()
        if not result:
            return None

        return IPv6PrefixFreeData.from_db(result=result)


class IPPrefixIPAddressFetch(Query):
    name = "ipprefix_ipaddress_fetch"
    type = QueryType.READ

    def __init__(
        self,
        obj: IPNetworkType,
        namespace: Node | str | None = None,
        **kwargs,
    ) -> None:
        self.obj = obj
        self.namespace_id = _get_namespace_id(namespace)

        super().__init__(**kwargs)

    async def query_init(self, db: InfrahubDatabase, **kwargs) -> None:  # noqa: ARG002
        self.params["ns_id"] = self.namespace_id

        prefix_bin = convert_ip_to_binary_str(self.obj)[: self.obj.prefixlen]
        self.params["prefix_binary"] = prefix_bin
        self.params["maxprefixlen"] = self.obj.prefixlen
        self.params["ip_version"] = self.obj.version

        branch_filter, branch_params = self.branch.get_query_filter_path(
            at=self.at.to_string(), branch_agnostic=self.branch_agnostic
        )
        self.params.update(branch_params)

        # ruff: noqa: E501
        query = """
        // First match on IPNAMESPACE
        MATCH (ns:%(ns_label)s)
        WHERE ns.uuid = $ns_id
        CALL (ns) {
            MATCH (ns)-[r:IS_PART_OF]-(root:Root)
            WHERE %(branch_filter)s
            RETURN r
            ORDER BY r.branch_level DESC, r.from DESC
            LIMIT 1
        }
        WITH ns, r
        WHERE r.status = "active"
        WITH ns
        // MATCH all IPAddress that are IN SCOPE
        MATCH path2 = (ns)-[:IS_RELATED]-(ns_rel:Relationship)-[:IS_RELATED]-(addr:%(node_label)s)-[:HAS_ATTRIBUTE]-(an:Attribute {name: "address"})-[:HAS_VALUE]-(av:AttributeIPHost)
        WHERE ns_rel.name = "ip_namespace__ip_address"
            AND av.binary_address STARTS WITH $prefix_binary
            AND av.prefixlen >= $maxprefixlen
            AND av.version = $ip_version
            AND all(r IN relationships(path2) WHERE (%(branch_filter)s) and r.status = "active")
        """ % {
            "ns_label": InfrahubKind.IPNAMESPACE,
            "node_label": InfrahubKind.IPADDRESS,
            "branch_filter": branch_filter,
        }

        self.add_to_query(query)
        self.return_labels = ["addr", "av"]
        self.order_by = ["av.binary_address"]

    def get_addresses(self) -> list[IPAddressData]:
        """Return a list of all addresses fitting in the prefix."""
        addresses: list[IPAddressData] = []

        for result in self.get_results():
            address = IPAddressData(
                id=result.get("addr").get("uuid"), address=ipaddress.ip_interface(result.get("av").get("value"))
            )
            addresses.append(address)

        return addresses


class IPPrefixIPAddressFetchFree(Query):
    name = "ipprefix_ipaddress_fetch_free"
    type = QueryType.READ

    def __init__(
        self,
        obj: IPNetworkType,
        is_pool: bool,
        namespace: Node | str | None = None,
        **kwargs,
    ) -> None:
        self.obj = obj
        self.namespace_id = _get_namespace_id(namespace)
        self.is_pool = is_pool

        super().__init__(**kwargs)

    async def query_init(self, db: InfrahubDatabase, **kwargs) -> None:  # noqa: ARG002
        self.params["ns_id"] = self.namespace_id

        prefix_bin = convert_ip_to_binary_str(self.obj)[: self.obj.prefixlen]
        self.params["prefix_binary"] = prefix_bin
        self.params["start_range"] = int(self.obj.network_address)
        self.params["end_range"] = int(self.obj.broadcast_address)
        if not self.is_pool:
            self.params["start_range"] += 1
            self.params["end_range"] -= 1

        self.params["maxprefixlen"] = self.obj.prefixlen
        self.params["ip_version"] = self.obj.version
        self.limit = 1  # Query only works at returning a single, free entry

        branch_filter, branch_params = self.branch.get_query_filter_path(
            at=self.at.to_string(), branch_agnostic=self.branch_agnostic
        )
        self.params.update(branch_params)

        # ruff: noqa: E501
        query = """
        // First match on IPNAMESPACE
        MATCH (ns:%(ns_label)s)
        WHERE ns.uuid = $ns_id
        CALL (ns) {
            MATCH (ns)-[r:IS_PART_OF]-(root:Root)
            WHERE %(branch_filter)s
            RETURN r
            ORDER BY r.branch_level DESC, r.from DESC
            LIMIT 1
        }
        WITH ns, r
        WHERE r.status = "active"
        WITH ns
        // MATCH all IPAddress that are IN SCOPE
        MATCH path2 = (ns)-[:IS_RELATED]-(ns_rel:Relationship)-[:IS_RELATED]-(addr:%(node_label)s)-[:HAS_ATTRIBUTE]-(an:Attribute {name: "address"})-[:HAS_VALUE]-(av:AttributeIPHost)
        WHERE ns_rel.name = "ip_namespace__ip_address"
            AND av.binary_address STARTS WITH $prefix_binary
            AND av.prefixlen >= $maxprefixlen
            AND av.version = $ip_version
            AND all(r IN relationships(path2) WHERE (%(branch_filter)s) and r.status = "active")
        ORDER BY av.binary_address
        // Gap detection algorithm: collect used addresses and find first available slot
        // Each used_address is a single binary string representing an allocated IP
        WITH DISTINCT av.binary_address AS used_address
        // Convert binary string to integer for comparison
        WITH [x IN split(used_address, "") | toInteger(x)] AS bits
        // Build array: [start_range - 1] prepended to all used addresses as integers
        // This creates a baseline for gap detection starting from the range beginning
        WITH [$start_range - 1] + collect(reduce(dec = 0, b IN bits | dec * 2 + b)) AS nums
        UNWIND range(0, size(nums) - 1) AS idx
        CALL (nums, idx) {
            // Compare expected sequential address with actual address at this position
            // If they differ, we found a gap (is_free = true)
            WITH nums[idx] AS curr, idx - 1 + $start_range AS expected
            RETURN expected AS addr, expected <> curr AS is_free, idx = size(nums) - 1 AS is_last
        }
        WITH addr, is_free, is_last
        WHERE is_free = true OR is_last = true
        WITH addr AS free_addr, is_free, is_last
        """ % {
            "ns_label": InfrahubKind.IPNAMESPACE,
            "node_label": InfrahubKind.IPADDRESS,
            "branch_filter": branch_filter,
        }

        self.add_to_query(query)
        self.return_labels = ["free_addr", "is_free", "is_last"]
        self.order_by = ["free_addr"]

    def get_address_data(self) -> IPAddressFreeData | None:
        if not self.results:
            return None

        return IPAddressFreeData.from_db(result=self.results[0])

    def get_address(self) -> IPAddressType | None:
        """Return the next free address fitting in the prefix."""
        result_data = self.get_address_data()
        if result_data is None:
            return None

        if result_data.is_free:
            return ipaddress.ip_interface(result_data.free_addr)
        # When is_last=True and is_free=False, we've reached the end of all allocated addresses
        # without finding a gap. The next available address is one past the last allocated address,
        # but only if it doesn't exceed the end of the valid range (end_range).
        if result_data.is_last and result_data.free_addr < self.params["end_range"]:
            return ipaddress.ip_interface(result_data.free_addr + 1)

        return None


class IPv6PrefixIPAddressFetchFree(Query):
    """Query to find the next free IPv6 address within a prefix.

    This query uses binary string operations to handle IPv6's 128-bit address space,
    as the integer values would overflow Neo4j's 64-bit integer type.
    """

    name = "ipv6prefix_ipaddress_fetch_free"
    type = QueryType.READ

    def __init__(
        self,
        obj: IPNetworkType,
        is_pool: bool,
        namespace: Node | str | None = None,
        **kwargs,
    ) -> None:
        self.obj = obj
        self.namespace_id = _get_namespace_id(namespace)
        self.is_pool = is_pool

        super().__init__(**kwargs)

    async def query_init(self, db: InfrahubDatabase, **kwargs) -> None:  # noqa: ARG002
        self.params["ns_id"] = self.namespace_id

        prefix_bin = convert_ip_to_binary_str(self.obj)[: self.obj.prefixlen]
        self.params["prefix_binary"] = prefix_bin
        self.params["maxprefixlen"] = self.obj.prefixlen
        self.params["ip_version"] = self.obj.version

        # Binary representation of start and end of valid range
        start_addr = self.obj.network_address
        end_addr = self.obj.broadcast_address
        if not self.is_pool:
            start_addr += 1
            end_addr -= 1
        self.params["start_range_bin"] = format(int(start_addr), "0128b")
        self.params["end_range_bin"] = format(int(end_addr), "0128b")

        self.limit = 1  # Query only works at returning a single, free entry

        branch_filter, branch_params = self.branch.get_query_filter_path(
            at=self.at.to_string(), branch_agnostic=self.branch_agnostic
        )
        self.params.update(branch_params)

        # ruff: noqa: E501
        query = """
        // First match on IPNAMESPACE
        MATCH (ns:%(ns_label)s)
        WHERE ns.uuid = $ns_id
        CALL (ns) {
            MATCH (ns)-[r:IS_PART_OF]-(root:Root)
            WHERE %(branch_filter)s
            RETURN r
            ORDER BY r.branch_level DESC, r.from DESC
            LIMIT 1
        }
        WITH ns, r
        WHERE r.status = "active"
        WITH ns
        // MATCH all IPAddress that are IN SCOPE
        OPTIONAL MATCH path2 = (ns)-[:IS_RELATED]-(ns_rel:Relationship)-[:IS_RELATED]-(addr:%(node_label)s)-[:HAS_ATTRIBUTE]-(an:Attribute {name: "address"})-[:HAS_VALUE]-(av:AttributeIPHost)
        WHERE ns_rel.name = "ip_namespace__ip_address"
            AND av.binary_address STARTS WITH $prefix_binary
            AND av.prefixlen >= $maxprefixlen
            AND av.version = $ip_version
            AND all(r IN relationships(path2) WHERE (%(branch_filter)s) and r.status = "active")
        WITH DISTINCT av.binary_address AS used_address
        ORDER BY used_address
        // Collect used addresses as binary strings, prepend a sentinel value before start_range
        WITH collect(used_address) AS used_addrs_raw
        WITH [addr IN used_addrs_raw WHERE addr IS NOT NULL AND addr >= $start_range_bin AND addr <= $end_range_bin] AS used_addrs
        // Gap detection using binary string comparison
        // We iterate through used addresses and find the first gap
        WITH reduce(acc = {cursor: $start_range_bin, found: null, is_last: false}, addr IN used_addrs |
                CASE
                    // Already found a gap, preserve result
                    WHEN acc.found IS NOT NULL THEN acc
                    // Gap found: cursor is less than this address
                    WHEN acc.cursor < addr THEN {cursor: acc.cursor, found: acc.cursor, is_last: false}
                    // No gap: advance cursor past this address using binary increment
                    ELSE
                        {
                            cursor: CASE
                                // Check for overflow: if final carry is 1, return a marker value
                                WHEN reduce(
                                    inc = {bits: split(addr, ""), carry: 1, result: []},
                                    idx IN reverse(range(0, 127)) |
                                    {
                                        bits: inc.bits,
                                        carry: CASE
                                            WHEN inc.carry = 0 THEN 0
                                            WHEN inc.bits[idx] = "1" THEN 1
                                            ELSE 0
                                        END,
                                        result: CASE
                                            WHEN inc.carry = 0 THEN [inc.bits[idx]] + inc.result
                                            WHEN inc.bits[idx] = "1" THEN ["0"] + inc.result
                                            ELSE ["1"] + inc.result
                                        END
                                    }
                                ).carry = 1 THEN null
                                ELSE reduce(s = "", c IN reduce(
                                    inc = {bits: split(addr, ""), carry: 1, result: []},
                                    idx IN reverse(range(0, 127)) |
                                    {
                                        bits: inc.bits,
                                        carry: CASE
                                            WHEN inc.carry = 0 THEN 0
                                            WHEN inc.bits[idx] = "1" THEN 1
                                            ELSE 0
                                        END,
                                        result: CASE
                                            WHEN inc.carry = 0 THEN [inc.bits[idx]] + inc.result
                                            WHEN inc.bits[idx] = "1" THEN ["0"] + inc.result
                                            ELSE ["1"] + inc.result
                                        END
                                    }
                                ).result | s + c)
                            END,
                            found: null,
                            is_last: false
                        }
                END
            ) AS res
        // Determine final result
        WITH CASE
            // Gap was found during iteration
            WHEN res.found IS NOT NULL THEN {addr: res.found, is_free: true, is_last: false}
            // No gap found, check if cursor is still valid and within range
            WHEN res.cursor IS NOT NULL AND res.cursor <= $end_range_bin THEN {addr: res.cursor, is_free: true, is_last: true}
            // No addresses available
            ELSE {addr: null, is_free: false, is_last: true}
        END AS result
        WHERE result.addr IS NOT NULL
        WITH result.addr AS free_addr_bin, result.is_free AS is_free, result.is_last AS is_last
        """ % {
            "ns_label": InfrahubKind.IPNAMESPACE,
            "node_label": InfrahubKind.IPADDRESS,
            "branch_filter": branch_filter,
        }

        self.add_to_query(query)
        self.return_labels = ["free_addr_bin", "is_free", "is_last"]

    def get_address_data(self) -> IPv6AddressFreeData | None:
        if not self.results:
            return None

        return IPv6AddressFreeData.from_db(result=self.results[0])

    def get_address(self) -> IPAddressType | None:
        """Return the next free IPv6 address fitting in the prefix."""
        result_data = self.get_address_data()
        if result_data is None:
            return None

        if result_data.is_free:
            # Convert binary string to IPv6 address
            addr_int = int(result_data.free_addr_bin, 2)
            return ipaddress.ip_interface(ipaddress.IPv6Address(addr_int))

        return None


@dataclass(frozen=True)
class IPPrefixUtilizationResult:
    """Result from IPPrefixUtilization containing prefix child allocation data."""

    prefix_uuid: str
    """UUID of the parent prefix node."""

    child_uuid: str
    """UUID of the child node (prefix or address)."""

    child_kind: str
    """Kind/type of the child node."""

    child_labels: tuple[str, ...]
    """Labels of the child node (used to determine if IPADDRESS or IPPREFIX)."""

    ip_value: str
    """IP value (address or prefix) of the child."""

    prefixlen: int
    """Prefix length of the child IP value."""

    branch: str
    """Branch name where this allocation exists."""

    @classmethod
    def from_db(cls, result: QueryResult) -> IPPrefixUtilizationResult:
        """Convert raw QueryResult to typed dataclass."""
        pfx = result.get_node("pfx")
        child = result.get_node("child")
        av = result.get_node("av")
        return cls(
            prefix_uuid=str(pfx.get("uuid")),
            child_uuid=str(child.get("uuid")),
            child_kind=child.get("kind"),
            child_labels=tuple(child.labels),
            ip_value=av.get("value"),
            prefixlen=av.get("prefixlen"),
            branch=str(result.get("branch")),
        )


class IPPrefixUtilization(Query):
    name = "ipprefix_utilization_prefix"
    type = QueryType.READ

    def __init__(self, ip_prefixes: list[str], allocated_kinds: list[str], **kwargs) -> None:
        self.ip_prefixes = ip_prefixes
        self.allocated_kinds: list[str] = []
        self.allocated_kinds_rel: list[str] = []

        for kind in sorted(allocated_kinds):
            self.allocated_kinds.append(f'"{kind}"')
            self.allocated_kinds_rel.append(
                {InfrahubKind.IPADDRESS: '"ip_prefix__ip_address"', InfrahubKind.IPPREFIX: '"parent__child"'}[kind]
            )

        super().__init__(**kwargs)

    async def query_init(self, db: InfrahubDatabase, **kwargs) -> None:  # noqa: ARG002
        self.params["ids"] = [p.get_id() for p in self.ip_prefixes]
        self.params["time_at"] = self.at.to_string()

        def rel_filter(rel_name: str) -> str:
            return f"{rel_name}.from <= $time_at AND ({rel_name}.to IS NULL OR {rel_name}.to >= $time_at)"

        query = f"""
        MATCH (pfx:Node)
        WHERE pfx.uuid IN $ids
        CALL (pfx) {{
            MATCH (pfx)-[r_rel1:IS_RELATED]-(rl:Relationship)<-[r_rel2:IS_RELATED]-(child:Node)
            WHERE rl.name IN [{", ".join(self.allocated_kinds_rel)}]
            AND any(l IN labels(child) WHERE l IN [{", ".join(self.allocated_kinds)}])
            AND ({rel_filter("r_rel1")})
            AND ({rel_filter("r_rel2")})
            RETURN r_rel1, rl, r_rel2, child
        }}
        WITH pfx, r_rel1, rl, r_rel2, child
        MATCH path = (
            (pfx)-[r_1:IS_RELATED]-(rl:Relationship)-[r_2:IS_RELATED]-(child:Node)
            -[r_attr:HAS_ATTRIBUTE]->(attr:Attribute)
            -[r_attr_val:HAS_VALUE]->(av:{PREFIX_ATTRIBUTE_LABEL}|{ADDRESS_ATTRIBUTE_LABEL})
        )
        WHERE %(id_func)s(r_1) = %(id_func)s(r_rel1)
        AND %(id_func)s(r_2) = %(id_func)s(r_rel2)
        AND ({rel_filter("r_attr")})
        AND ({rel_filter("r_attr_val")})
        AND attr.name IN ["prefix", "address"]
        WITH
            path,
            pfx,
            child,
            av,
            reduce(br_lvl = 0, r in relationships(path) | br_lvl + r.branch_level) AS sum_branch_level,
            all(r in relationships(path) WHERE r.status = "active") AS is_active,
            [r_attr_val.from, r_attr.from, r_2.from, r_1.from] AS from_times,
            reduce(
                b_details = [0, null], r in relationships(path) |
                CASE WHEN r.branch_level > b_details[0] THEN [r.branch_level, r.branch] ELSE b_details END
            ) as deepest_branch_details
        ORDER BY pfx.uuid, child.uuid, av.uuid, sum_branch_level DESC, from_times[3] DESC, from_times[2] DESC, from_times[1] DESC, from_times[0] DESC
        WITH
            pfx,
            child,
            av,
            deepest_branch_details[0] AS branch_level,
            deepest_branch_details[1] AS branch,
            head(collect(is_active)) AS is_latest_active
        WHERE is_latest_active = TRUE
        """ % {
            "id_func": db.get_id_function_name(),
        }
        self.return_labels = ["pfx", "child", "av", "branch_level", "branch"]
        self.add_to_query(query)

    def get_data(self) -> list[IPPrefixUtilizationResult]:
        """Return results as typed dataclass instances.

        Returns:
            List of IPPrefixUtilizationResult containing prefix child allocation data.
        """
        return [IPPrefixUtilizationResult.from_db(result) for result in self.get_results()]


@dataclass(frozen=True)
class IPPrefixReconcileQueryResult:
    """Result from IPPrefixReconcileQuery containing IP reconciliation data."""

    ip_node_uuid: str | None
    """UUID of the IP node being reconciled, or None if not found."""

    current_parent_uuid: str | None
    """UUID of the current parent prefix, or None if no parent exists."""

    calculated_parent_uuid: str | None
    """UUID of the calculated correct parent prefix, or None if should be top-level."""

    current_children_uuids: tuple[str, ...]
    """UUIDs of current child prefixes/addresses."""

    calculated_children_uuids: tuple[str, ...]
    """UUIDs of calculated correct child prefixes/addresses."""

    @classmethod
    def from_db(cls, result: QueryResult) -> IPPrefixReconcileQueryResult:
        """Convert raw QueryResult to typed dataclass."""

        def get_optional_node_uuid(label: str) -> str | None:
            """Extract UUID from an optional node (may be None from OPTIONAL MATCH)."""
            node = result.get(label)
            return str(node.get("uuid")) if node and node.get("uuid") else None

        def get_collection_uuids(label: str) -> tuple[str, ...]:
            """Extract UUIDs from a node collection (may contain None from COLLECT)."""
            return tuple(str(n.get("uuid")) for n in result.get_node_collection(label) if n and n.get("uuid"))

        return cls(
            ip_node_uuid=get_optional_node_uuid("ip_node"),
            current_parent_uuid=get_optional_node_uuid("current_parent"),
            calculated_parent_uuid=get_optional_node_uuid("new_parent"),
            current_children_uuids=get_collection_uuids("current_children"),
            calculated_children_uuids=get_collection_uuids("new_children"),
        )


class IPPrefixReconcileQuery(Query):
    name = "ip_prefix_reconcile"
    type = QueryType.READ

    def __init__(
        self,
        ip_value: AllIPTypes,
        namespace: Node | str | None = None,
        node_uuid: str | None = None,
        **kwargs,
    ) -> None:
        self.ip_value = ip_value
        self.ip_uuid = node_uuid
        self.namespace_id = _get_namespace_id(namespace)
        super().__init__(**kwargs)

    async def query_init(self, db: InfrahubDatabase, **kwargs) -> None:  # noqa: ARG002
        branch_filter, branch_params = self.branch.get_query_filter_path(at=self.at.to_string())
        self.params.update(branch_params)
        self.params["namespace_kind"] = InfrahubKind.IPNAMESPACE
        self.params["namespace_id"] = self.namespace_id
        self.params["ip_prefix_kind"] = InfrahubKind.IPPREFIX
        self.params["ip_address_kind"] = InfrahubKind.IPADDRESS
        self.params["ip_prefix_attribute_kind"] = PREFIX_ATTRIBUTE_LABEL
        self.params["ip_address_attribute_kind"] = ADDRESS_ATTRIBUTE_LABEL

        if isinstance(self.ip_value, IPAddressType):
            is_address = True
            prefixlen = self.ip_value.network.prefixlen
        else:
            is_address = False
            prefixlen = self.ip_value.prefixlen
        self.params["is_prefix"] = not is_address
        self.params["prefixlen"] = prefixlen
        prefix_bin = convert_ip_to_binary_str(self.ip_value)
        prefix_bin_host = prefix_bin[:prefixlen]
        self.params["prefix_binary_full"] = prefix_bin
        self.params["prefix_binary_host"] = prefix_bin_host
        self.params["ip_version"] = self.ip_value.version
        # possible prefix: highest possible prefix length for a match
        possible_prefix_map: dict[str, int] = {}
        start_prefixlen = prefixlen if is_address else prefixlen - 1
        for max_prefix_len in range(start_prefixlen, -1, -1):
            tmp_prefix = prefix_bin_host[:max_prefix_len]
            possible_prefix = tmp_prefix.ljust(self.ip_value.max_prefixlen, "0")
            if possible_prefix not in possible_prefix_map:
                possible_prefix_map[possible_prefix] = max_prefix_len
        self.params["possible_prefix_and_length_list"] = []
        self.params["possible_prefix_list"] = []
        for possible_prefix, max_length in possible_prefix_map.items():
            self.params["possible_prefix_and_length_list"].append([possible_prefix, max_length])
            self.params["possible_prefix_list"].append(possible_prefix)

        namespace_query = """
        // ------------------
        // Get IP Namespace
        // ------------------
        MATCH (ip_namespace:%(namespace_kind)s {uuid: $namespace_id})-[r:IS_PART_OF]->(root:Root)
        WHERE %(branch_filter)s
        ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
        LIMIT 1
        WITH ip_namespace
        WHERE r.status = "active"
        """ % {"branch_filter": branch_filter, "namespace_kind": self.params["namespace_kind"]}
        self.add_to_query(namespace_query)

        if self.ip_uuid:
            self.params["node_uuid"] = self.ip_uuid
            get_node_by_id_query = """
            // ------------------
            // Get IP Prefix node by UUID
            // ------------------
            OPTIONAL MATCH (ip_node:%(ip_kind)s {uuid: $node_uuid})-[r:IS_PART_OF]->(:Root)
            WHERE %(branch_filter)s
            ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
            LIMIT 1
            WITH ip_namespace, ip_node
            """ % {
                "branch_filter": branch_filter,
                "ip_kind": InfrahubKind.IPADDRESS
                if isinstance(self.ip_value, IPAddressType)
                else InfrahubKind.IPPREFIX,
            }
            self.add_to_query(get_node_by_id_query)

        else:
            get_node_by_prefix_query = """
            // ------------------
            // Get IP node with the correct value on this branch
            // ------------------
            OPTIONAL MATCH (:Root)<-[r1:IS_PART_OF]-(ip_node:%(ip_kind)s)
                -[r2:HAS_ATTRIBUTE]->(a:Attribute)-[r3:HAS_VALUE]->(aipn:%(ip_attribute_kind)s)
            WHERE aipn.binary_address = $prefix_binary_full
            AND aipn.prefixlen = $prefixlen
            AND aipn.version = $ip_version
            AND all(r IN [r1, r2, r3] WHERE (%(branch_filter)s))
            ORDER BY r3.branch_level DESC, r3.from DESC, r3.status ASC,
                r2.branch_level DESC, r2.from DESC, r2.status ASC,
                r1.branch_level DESC, r1.from DESC, r1.status ASC
            LIMIT 1
            WITH ip_namespace, CASE
                WHEN ip_node IS NOT NULL AND r1.status = "active" AND r2.status = "active" AND r3.status = "active" THEN ip_node
                ELSE NULL
            END AS ip_node
            // ------------------
            // Filter to only those that are in the correct namespace
            // ------------------
            OPTIONAL MATCH (ip_namespace)-[r1:IS_RELATED]-(nsr:Relationship)-[r2:IS_RELATED]-(ip_node)
            WHERE nsr.name IN ["ip_namespace__ip_prefix", "ip_namespace__ip_address"]
            AND all(r IN [r1, r2] WHERE (%(branch_filter)s))
            ORDER BY r1.branch_level DESC, r1.from DESC, r1.status ASC,
                r2.branch_level DESC, r2.from DESC, r2.status ASC
            LIMIT 1
            WITH ip_namespace, CASE
                WHEN ip_node IS NOT NULL AND r1.status = "active" AND r2.status = "active" THEN ip_node
                ELSE NULL
            END as ip_node
            """ % {
                "branch_filter": branch_filter,
                "ip_kind": InfrahubKind.IPADDRESS
                if isinstance(self.ip_value, IPAddressType)
                else InfrahubKind.IPPREFIX,
                "ip_attribute_kind": ADDRESS_ATTRIBUTE_LABEL
                if isinstance(self.ip_value, IPAddressType)
                else PREFIX_ATTRIBUTE_LABEL,
            }
            self.add_to_query(get_node_by_prefix_query)

        get_current_parent_query = """
        // ------------------
        // Get prefix node's current parent, if it exists
        // ------------------
        CALL (ip_node) {
            OPTIONAL MATCH parent_prefix_path = (ip_node)-[r1:IS_RELATED]->(:Relationship {name: "parent__child"})-[r2:IS_RELATED]->(current_parent:%(ip_prefix_kind)s)
            WHERE $is_prefix = TRUE
            AND all(r IN relationships(parent_prefix_path) WHERE (%(branch_filter)s))
            RETURN current_parent, (r1.status = "active" AND r2.status = "active") AS parent_is_active
            ORDER BY r1.branch_level DESC, r1.from DESC, r1.status ASC, r2.branch_level DESC, r2.from DESC, r2.status ASC
            LIMIT 1
        }
        WITH ip_namespace, ip_node, CASE WHEN parent_is_active THEN current_parent ELSE NULL END as prefix_parent
        CALL (ip_node) {
            OPTIONAL MATCH parent_prefix_path = (ip_node)-[r1:IS_RELATED]->(:Relationship {name: "ip_prefix__ip_address"})<-[r2:IS_RELATED]-(current_parent:%(ip_prefix_kind)s)
            WHERE $is_prefix = FALSE
            AND all(r IN relationships(parent_prefix_path) WHERE (%(branch_filter)s))
            RETURN current_parent, (r1.status = "active" AND r2.status = "active") AS parent_is_active
            ORDER BY r1.branch_level DESC, r1.from DESC, r1.status ASC, r2.branch_level DESC, r2.from DESC, r2.status ASC
            LIMIT 1
        }
        WITH ip_namespace, ip_node, prefix_parent, CASE WHEN parent_is_active THEN current_parent ELSE NULL END as address_parent
        WITH ip_namespace, ip_node, COALESCE(prefix_parent, address_parent) AS current_parent
        """ % {
            "branch_filter": branch_filter,
            "ip_prefix_kind": InfrahubKind.IPPREFIX,
        }
        self.add_to_query(get_current_parent_query)

        get_current_children_query = """
        // ------------------
        // Get prefix node's current prefix children, if any exist
        // ------------------
        CALL (ip_node) {
            OPTIONAL MATCH child_prefix_path = (ip_node:%(ip_prefix_kind)s)<-[r1:IS_RELATED]-(:Relationship {name: "parent__child"})<-[r2:IS_RELATED]-(current_prefix_child:%(ip_prefix_kind)s)
            WHERE all(r IN relationships(child_prefix_path) WHERE (%(branch_filter)s))
            WITH current_prefix_child, (r1.status = "active" AND r2.status = "active") AS is_active
            ORDER BY current_prefix_child.uuid, r1.branch_level DESC, r1.from DESC, r2.branch_level DESC, r2.from DESC
            RETURN current_prefix_child, head(collect(is_active)) AS prefix_child_is_active
        }
        WITH ip_namespace, ip_node, current_parent, CASE WHEN prefix_child_is_active THEN current_prefix_child ELSE NULL END as current_prefix_child
        WITH ip_namespace, ip_node, current_parent, collect(current_prefix_child) AS current_prefix_children
        // ------------------
        // Get prefix node's current address children, if any exist
        // ------------------
        CALL (ip_node) {
            OPTIONAL MATCH child_address_path = (ip_node:%(ip_prefix_kind)s)-[r1:IS_RELATED]->(:Relationship {name: "ip_prefix__ip_address"})<-[r2:IS_RELATED]-(current_address_child:%(ip_address_kind)s)
            WHERE all(r IN relationships(child_address_path) WHERE (%(branch_filter)s))
            WITH current_address_child, (r1.status = "active" AND r2.status = "active") AS is_active
            ORDER BY current_address_child.uuid, r1.branch_level DESC, r1.from DESC, r2.branch_level DESC, r2.from DESC
            RETURN current_address_child, head(collect(is_active)) AS address_child_is_active

        }
        WITH ip_namespace, ip_node, current_parent, current_prefix_children, CASE WHEN address_child_is_active THEN current_address_child ELSE NULL END as current_address_child
        WITH ip_namespace, ip_node, current_parent, current_prefix_children, collect(current_address_child) AS current_address_children
        WITH ip_namespace, ip_node, current_parent, current_prefix_children + current_address_children AS current_children
        """ % {
            "branch_filter": branch_filter,
            "ip_prefix_kind": InfrahubKind.IPPREFIX,
            "ip_address_kind": InfrahubKind.IPADDRESS,
        }
        self.add_to_query(get_current_children_query)

        get_new_parent_query = """
        // ------------------
        // Identify the correct parent, if any, for the prefix node
        // ------------------
        CALL (ip_namespace) {
            // ------------------
            // start with just the AttributeValue vertices b/c we have an index on them
            // ------------------
            OPTIONAL MATCH (av:%(ip_prefix_attribute_kind)s)
            WHERE av.version = $ip_version
            AND av.binary_address IN $possible_prefix_list
            AND any(prefix_and_length IN $possible_prefix_and_length_list WHERE av.binary_address = prefix_and_length[0] AND av.prefixlen <= prefix_and_length[1])
            // ------------------
            // now get all the possible IPPrefix nodes for these AttributeValues
            // ------------------
            OPTIONAL MATCH parent_path = (ip_namespace)-[:IS_RELATED]-(:Relationship {name: "ip_namespace__ip_prefix"})
            -[:IS_RELATED]-(maybe_new_parent:%(ip_prefix_kind)s)
            -[:HAS_ATTRIBUTE]->(:Attribute {name: "prefix"})
            -[:HAS_VALUE]->(av:AttributeValue)
            WHERE all(r IN relationships(parent_path) WHERE (%(branch_filter)s))
            RETURN DISTINCT maybe_new_parent
        }
        CALL (ip_namespace, maybe_new_parent) {
            // ------------------
            // filter to only active maybe_new_parent Nodes in the correct namespace
            // ------------------
            OPTIONAL MATCH (ip_namespace)-[r1:IS_RELATED]-(:Relationship {name: "ip_namespace__ip_prefix"})-[r2:IS_RELATED]-(maybe_new_parent)
            WHERE all(r IN [r1, r2] WHERE (%(branch_filter)s))
            WITH maybe_new_parent, r1, r2, r1.status = "active" AND r2.status = "active" AS is_active
            ORDER BY elementId(maybe_new_parent), r1.branch_level DESC, r1.from DESC, r1.status ASC, r2.branch_level DESC, r2.from DESC, r2.status ASC
            WITH maybe_new_parent, head(collect(is_active)) AS is_active
            RETURN is_active = TRUE AS parent_in_namespace
        }
        CALL (maybe_new_parent) {
            // ------------------
            // filter to only active maybe_new_parent Nodes currently linked to one of the allowed AttributeValues
            // ------------------
            OPTIONAL MATCH (maybe_new_parent)-[r1:HAS_ATTRIBUTE]->(:Attribute {name: "prefix"})-[r2:HAS_VALUE]->(av:AttributeValue)
            WHERE all(r IN [r1, r2] WHERE (%(branch_filter)s))
            WITH maybe_new_parent, av, r1, r2, r1.status = "active" AND r2.status = "active" AS is_active
            ORDER BY elementId(maybe_new_parent), r1.branch_level DESC, r1.from DESC, r1.status ASC, r2.branch_level DESC, r2.from DESC, r2.status ASC
            // ------------------
            // get the latest active attribute value for the maybe_new_parent
            // ------------------
            WITH maybe_new_parent, head(collect([av, is_active])) AS av_is_active
            WITH
                av_is_active[0] AS av,
                av_is_active[1] AS is_active
            // ------------------
            // return NULL if the value is not allowed or if it is not active
            // ------------------
            WITH av, is_active, (
                av.version = $ip_version
                AND av.binary_address IN $possible_prefix_list
                AND any(prefix_and_length IN $possible_prefix_and_length_list WHERE av.binary_address = prefix_and_length[0] AND av.prefixlen <= prefix_and_length[1])
            ) AS is_allowed_value
            RETURN CASE WHEN is_active = TRUE AND is_allowed_value = TRUE THEN av ELSE NULL END AS allowed_av
        }
        // ------------------
        // set inactive maybe_new_parents to NULL
        // ------------------
        WITH ip_namespace, ip_node, current_parent, current_children, allowed_av.prefixlen AS mnp_prefixlen,
            CASE WHEN parent_in_namespace = TRUE AND allowed_av IS NOT NULL
                THEN maybe_new_parent ELSE NULL
            END AS maybe_new_parent
        WITH ip_namespace, ip_node, current_parent, current_children, maybe_new_parent, mnp_prefixlen
        ORDER BY ip_node.uuid, mnp_prefixlen DESC
        WITH ip_namespace, ip_node, current_parent, current_children, head(collect(maybe_new_parent)) as new_parent
        """ % {
            "branch_filter": branch_filter,
            "ip_prefix_kind": InfrahubKind.IPPREFIX,
            "ip_prefix_attribute_kind": PREFIX_ATTRIBUTE_LABEL,
        }
        self.add_to_query(get_new_parent_query)

        get_new_children_query = """
        // ------------------
        // Identify the correct children, if any, for the prefix node
        // ------------------
        CALL (ip_namespace, ip_node) {
            // ------------------
            // Get ALL possible children for the prefix node
            // ------------------
            OPTIONAL MATCH (
                 (ip_namespace)-[:IS_RELATED]-(ns_rel:Relationship)-[:IS_RELATED]
                 -(maybe_new_child:%(ip_prefix_kind)s|%(ip_address_kind)s)-[:HAS_ATTRIBUTE]
                 ->(a:Attribute)-[:HAS_VALUE]->(av:%(ip_prefix_attribute_kind)s|%(ip_address_attribute_kind)s)
            )
            USING INDEX av:%(ip_prefix_attribute_kind)s(binary_address)
            USING INDEX av:%(ip_address_attribute_kind)s(binary_address)
            WHERE $is_prefix  // only prefix nodes can have children
            AND ns_rel.name IN ["ip_namespace__ip_prefix", "ip_namespace__ip_address"]
            AND a.name in ["prefix", "address"]
            AND (ip_node IS NULL OR maybe_new_child.uuid <> ip_node.uuid)
            AND (
                ($ip_prefix_kind IN labels(maybe_new_child) AND av.prefixlen > $prefixlen)
                OR ($ip_address_kind IN labels(maybe_new_child) AND av.prefixlen >= $prefixlen)
            )
            AND av.version = $ip_version
            AND av.binary_address STARTS WITH $prefix_binary_host
            RETURN DISTINCT maybe_new_child
        }
        CALL (ip_namespace, maybe_new_child) {
            // ------------------
            // filter to only active maybe_new_child Nodes in the correct namespace
            // ------------------
            OPTIONAL MATCH (ip_namespace)-[r1:IS_RELATED]-(ns_rel:Relationship)-[r2:IS_RELATED]-(maybe_new_child)
            WHERE ns_rel.name IN ["ip_namespace__ip_prefix", "ip_namespace__ip_address"]
            AND all(r IN [r1, r2] WHERE (%(branch_filter)s))
            WITH maybe_new_child, r1, r2, r1.status = "active" AND r2.status = "active" AS is_active
            ORDER BY elementId(maybe_new_child), r1.branch_level DESC, r1.from DESC, r1.status ASC, r2.branch_level DESC, r2.from DESC, r2.status ASC
            WITH maybe_new_child, head(collect(is_active)) AS is_active
            RETURN is_active = TRUE AS child_in_namespace
        }
        CALL (maybe_new_child) {
            // ------------------
            // filter to only active maybe_new_child Nodes currently linked to a possible child AttributeValue
            // ------------------
            OPTIONAL MATCH (maybe_new_child:%(ip_prefix_kind)s|%(ip_address_kind)s)-[r1:HAS_ATTRIBUTE]->(a:Attribute)-[r2:HAS_VALUE]->(av:AttributeValue)
            WHERE a.name in ["prefix", "address"]
            AND all(r IN [r1, r2] WHERE (%(branch_filter)s))
            WITH maybe_new_child, av, r1, r2, r1.status = "active" AND r2.status = "active" AS is_active
            ORDER BY elementId(maybe_new_child), r1.branch_level DESC, r1.from DESC, r1.status ASC, r2.branch_level DESC, r2.from DESC, r2.status ASC
            // ------------------
            // get the latest active attribute value for the maybe_new_child
            // ------------------
            WITH maybe_new_child, head(collect([av, is_active])) AS av_is_active
            WITH
                av_is_active[0] AS av,
                av_is_active[1] AS is_active
            // ------------------
            // return NULL if the value is not allowed or if it is not active
            // ------------------
            WITH av, is_active, (
                (
                    ($ip_prefix_kind IN labels(maybe_new_child) AND av.prefixlen > $prefixlen)
                    OR ($ip_address_kind IN labels(maybe_new_child) AND av.prefixlen >= $prefixlen)
                )
                AND av.version = $ip_version
                AND av.binary_address STARTS WITH $prefix_binary_host
            ) AS is_allowed_value
            RETURN CASE WHEN is_active = TRUE AND is_allowed_value = TRUE THEN av ELSE NULL END AS latest_mnc_attribute
        }
        // ------------------
        // set inactive/illegal value/wrong namespace maybe_new_children to NULL
        // ------------------
        WITH ip_namespace, ip_node, current_parent, current_children, new_parent,
            CASE
                WHEN child_in_namespace = TRUE AND latest_mnc_attribute IS NOT NULL THEN maybe_new_child
                ELSE NULL
            END AS maybe_new_child,
            CASE WHEN child_in_namespace = TRUE THEN latest_mnc_attribute ELSE NULL END AS latest_mnc_attribute
        WITH ip_namespace, ip_node, current_parent, current_children, new_parent, collect([maybe_new_child, latest_mnc_attribute]) AS maybe_children_ips
        WITH ip_namespace, ip_node, current_parent, current_children, new_parent, maybe_children_ips, range(0, size(maybe_children_ips) - 1) AS child_indices
        UNWIND child_indices as ind
        CALL (ind, maybe_children_ips) {
            // ------------------
            // Filter all possible children to remove those that have a more-specific parent
            // among the list of all possible children
            // ------------------
            WITH ind, maybe_children_ips AS ips
            RETURN REDUCE(
                has_more_specific_parent = FALSE, potential_parent IN ips |
                CASE
                    WHEN has_more_specific_parent THEN has_more_specific_parent  // keep it True once set
                    WHEN potential_parent IS NULL OR ips[ind][0] IS NULL THEN has_more_specific_parent
                    WHEN potential_parent[0] = ips[ind][0] THEN has_more_specific_parent  // skip comparison to self
                    WHEN $ip_address_kind in labels(potential_parent[0]) THEN has_more_specific_parent  // address cannot be a parent
                    WHEN $ip_prefix_attribute_kind IN labels(ips[ind][1]) AND (potential_parent[1]).prefixlen >= (ips[ind][1]).prefixlen THEN has_more_specific_parent  // prefix with same or greater prefixlen for prefix cannot be parent
                    WHEN $ip_address_attribute_kind IN labels(ips[ind][1]) AND (potential_parent[1]).prefixlen > (ips[ind][1]).prefixlen THEN has_more_specific_parent  // prefix with greater prefixlen for address cannot be parent
                    WHEN (ips[ind][1]).binary_address STARTS WITH SUBSTRING((potential_parent[1]).binary_address, 0, (potential_parent[1]).prefixlen) THEN TRUE  // we found a parent
                    ELSE has_more_specific_parent
                END
            ) as has_parent_among_maybe_children
        }
        WITH ip_namespace, ip_node, current_parent, current_children, new_parent, maybe_children_ips[ind][0] AS new_child, has_parent_among_maybe_children
        WHERE has_parent_among_maybe_children = FALSE
        WITH
            ip_namespace,
            ip_node,
            current_parent,
            current_children,
            new_parent,
            collect(new_child) as new_children
        """ % {
            "ip_prefix_kind": InfrahubKind.IPPREFIX,
            "ip_address_kind": InfrahubKind.IPADDRESS,
            "branch_filter": branch_filter,
            "ip_prefix_attribute_kind": PREFIX_ATTRIBUTE_LABEL,
            "ip_address_attribute_kind": ADDRESS_ATTRIBUTE_LABEL,
        }
        self.add_to_query(get_new_children_query)
        self.order_by = ["ip_node.uuid"]
        self.return_labels = ["ip_node", "current_parent", "current_children", "new_parent", "new_children"]

    def get_data(self) -> IPPrefixReconcileQueryResult | None:
        """Return single result as typed dataclass instance.

        Returns:
            IPPrefixReconcileQueryResult containing reconciliation data,
            or None if no results found.
        """
        results = list(self.get_results())
        if not results:
            return None
        return IPPrefixReconcileQueryResult.from_db(results[0])
