# Copyright (C) 2026 Linuxfabrik <info@linuxfabrik.ch>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# On Debian systems, the complete text of the GNU General Public License
# version 2 can be found in /usr/share/common-licenses/GPL-2.
#
# SPDX-License-Identifier: GPL-2.0-or-later

"""Generic rule processors shared across all compilers.

Corresponds to the generic processor classes from fwbuilder's compiler.py,
rewritten for CompRule dataclasses.
"""

from __future__ import annotations

import ipaddress as _ipa

import sqlalchemy

from firewallfabrik.compiler._comp_rule import CompRule, expand_group
from firewallfabrik.compiler._rule_processor import BasicRuleProcessor
from firewallfabrik.core._util import SLOT_VALUES
from firewallfabrik.core.objects import (
    Address,
    AddressRange,
    Direction,
    Group,
    ICMP6Service,
    ICMPService,
    Interface,
    IPService,
    MultiAddress,
    Network,
    NetworkIPv6,
    PolicyAction,
    Service,
    TCPService,
    TCPUDPService,
    group_membership,
)


def _is_runtime(obj: MultiAddress) -> bool:
    """Return True if the MultiAddress is marked for run-time resolution."""
    return bool((obj.data or {}).get('run_time', False))


def _get_group_members(session, group: Group) -> list:
    """Return the direct members of a group (without recursive expansion).

    Uses the ``group_membership`` association table to look up member IDs,
    then resolves them to model objects.  Needed by
    :class:`RecursiveGroupsInRE` to walk the group tree.
    """
    member_ids = (
        session.execute(
            sqlalchemy.select(group_membership.c.member_id).where(
                group_membership.c.group_id == group.id,
            ),
        )
        .scalars()
        .all()
    )
    if not member_ids:
        return []

    from firewallfabrik.compiler._comp_rule import _resolve_objects

    obj_map = _resolve_objects(session, set(member_ids))
    return [obj_map[mid] for mid in member_ids if mid in obj_map]


class Begin(BasicRuleProcessor):
    """Injects CompRules from the compiler's rules list into the pipeline."""

    def __init__(self, name: str = 'Begin') -> None:
        super().__init__(name)
        self._init = False

    def process_next(self) -> bool:
        if not self._init:
            for rule in self.compiler.rules:
                if rule.disabled:
                    continue
                self.tmp_queue.append(rule)
            self._init = True
            return bool(self.tmp_queue)
        return False


class PrintTotalNumberOfRules(BasicRuleProcessor):
    """Counts total rules (uses slurp). Passes all rules through."""

    def __init__(self, name: str = 'Print total number of rules') -> None:
        super().__init__(name)

    def process_next(self) -> bool:
        if self.slurp():
            return True
        return bool(self.tmp_queue)


class SimplePrintProgress(BasicRuleProcessor):
    """Passes rules through, optionally printing progress."""

    def __init__(self, name: str = 'Progress') -> None:
        super().__init__(name)

    def process_next(self) -> bool:

        rule = self.prev_processor.get_next_rule()
        if rule is not None:
            self.tmp_queue.append(rule)
            return True
        return False


class SingleRuleFilter(BasicRuleProcessor):
    """Filter to single rule in single-rule compile mode."""

    def __init__(self, name: str = 'Single rule filter') -> None:
        super().__init__(name)

    def process_next(self) -> bool:

        rule = self.prev_processor.get_next_rule()
        if rule is None:
            return False
        if (
            self.compiler.single_rule_compile_mode
            and str(rule.id) != self.compiler.single_rule_id
        ):
            return True  # skip, try next
        self.tmp_queue.append(rule)
        return True


class SkipDisabledRules(BasicRuleProcessor):
    """Remove disabled rules from the pipeline."""

    def __init__(self, name: str = 'Skip disabled rules') -> None:
        super().__init__(name)

    def process_next(self) -> bool:

        rule = self.prev_processor.get_next_rule()
        if rule is None:
            return False
        if not rule.disabled:
            self.tmp_queue.append(rule)
        return True


class ResolveMultiAddress(BasicRuleProcessor):
    """Resolve compile-time MultiAddress objects in all rule element slots.

    Corresponds to C++ ``Preprocessor::convertObject()`` which calls
    ``MultiAddress::loadFromSource()`` before compilation.  In C++ this
    runs as a separate ``Preprocessor`` pass over the entire object tree;
    here we do it per-rule in the processor pipeline, but *before*
    ``EmptyGroupsInRE`` so that the empty-group check can see whether
    resolution produced any addresses.

    - **Compile-time** MultiAddress (DNSName, AddressTable): resolved
      and replaced with the resulting Address objects in the slot.
    - **Runtime** MultiAddress: kept as-is.
    """

    def process_next(self) -> bool:
        rule = self.prev_processor.get_next_rule()
        if rule is None:
            return False

        for slot in SLOT_VALUES:
            elements = getattr(rule, slot)
            if not elements:
                continue
            new_elements = []
            changed = False
            for obj in elements:
                if isinstance(obj, MultiAddress) and not _is_runtime(obj):
                    new_elements.extend(
                        self.compiler._resolve_multi_address(obj),
                    )
                    changed = True
                else:
                    new_elements.append(obj)
            if changed:
                setattr(rule, slot, new_elements)

        self.tmp_queue.append(rule)
        return True


class EmptyGroupsInRE(BasicRuleProcessor):
    """Check for empty groups in a specific rule element slot.

    Corresponds to C++ ``Compiler::emptyGroupsInRE``.  Runs **after**
    ``ResolveMultiAddress`` and **before** ``ExpandGroups``.  For each
    Group in the slot that has zero effective members (recursively
    counting through nested groups):

    - If ``ignore_empty_groups`` is **true**: remove the empty group from
      the element and warn.  If the element becomes "any" (empty) after
      all removals, drop the rule.
    - If ``ignore_empty_groups`` is **false** (default): abort compilation.

    Runtime MultiAddress objects are skipped (their content is unknown at
    compile time).

    Each platform compiler adds one instance per slot it cares about
    (C++: src, dst, srv, itf for policy; osrc, odst, osrv, tsrc, tdst,
    tsrv for NAT).
    """

    def __init__(self, name: str, slot: str) -> None:
        super().__init__(name)
        self._slot = slot

    @staticmethod
    def _count_children(session, obj) -> int:
        """Count effective leaf members of a group recursively.

        Matches C++ ``Compiler::emptyGroupsInRE::countChildren``.
        Runtime MultiAddress objects count as 1 (their content is
        unknown at compile time).
        """
        if not isinstance(obj, Group):
            return 1
        if isinstance(obj, MultiAddress) and _is_runtime(obj):
            return 1
        members = expand_group(session, obj)
        return len(members)

    def process_next(self) -> bool:
        rule = self.prev_processor.get_next_rule()
        if rule is None:
            return False

        elements = getattr(rule, self._slot)
        if not elements:
            # Element is "any" — nothing to check
            self.tmp_queue.append(rule)
            return True

        # Find empty groups in this slot.  Skip runtime MultiAddress
        # objects — their content is unknown at compile time.  Matches
        # C++ Compiler::emptyGroupsInRE::processNext().
        empty_groups: list = []
        for obj in elements:
            if isinstance(obj, MultiAddress) and _is_runtime(obj):
                continue
            if (
                isinstance(obj, Group)
                and self._count_children(self.compiler.session, obj) == 0
            ):
                empty_groups.append(obj)

        if not empty_groups:
            self.tmp_queue.append(rule)
            return True

        if self.compiler.fw.get_option('ignore_empty_groups'):
            # Remove empty groups and warn
            for obj in empty_groups:
                name = getattr(obj, 'name', str(obj))
                self.compiler.warning(
                    rule,
                    f"Empty group or address table object '{name}'",
                )
            remaining = [o for o in elements if o not in empty_groups]
            setattr(rule, self._slot, remaining)
            if not remaining:
                # Element became "any" after removal — drop the rule
                self.compiler.warning(
                    rule,
                    f'After removal of all empty groups rule element'
                    f" {self._slot} becomes 'any'; dropping rule"
                    f' {rule.label} because option'
                    f" 'Ignore rules with empty groups' is in effect",
                )
                return True  # drop rule
        else:
            names = ', '.join(getattr(o, 'name', str(o)) for o in empty_groups)
            self.compiler.abort(
                rule,
                f"Empty group or address table object '{names}'"
                f' is used in the rule but option'
                f" 'Ignore rules with empty groups' is off",
            )
            return True  # abort was set

        self.tmp_queue.append(rule)
        return True


class RecursiveGroupsInRE(BasicRuleProcessor):
    """Check for recursive (self-referencing) groups in a rule element slot.

    Corresponds to C++ ``Compiler::recursiveGroupsInRE``.  Runs **before**
    ``EmptyGroupsInRE`` and ``ExpandGroups``.  For each Group in the slot,
    recursively checks whether any nested group references itself or an
    ancestor, aborting compilation if it does.

    Each platform compiler adds one instance per slot it cares about
    (e.g. src, dst, srv for policy; osrc, odst, osrv for NAT).
    """

    def __init__(self, name: str, slot: str) -> None:
        super().__init__(name)
        self._slot = slot

    def _is_recursive_group(self, grid, obj) -> None:
        """Recursively check whether *obj* contains a reference back to *grid*.

        Matches C++ ``Compiler::recursiveGroupsInRE::isRecursiveGroup``.
        """
        if not isinstance(obj, Group):
            return

        members = _get_group_members(self.compiler.session, obj)
        for member in members:
            if not isinstance(member, Group):
                continue
            if member.id == grid or obj.id == member.id:
                name = getattr(member, 'name', str(member))
                self.compiler.abort(
                    None,
                    f"Group '{name}' references itself recursively",
                )
                return
            self._is_recursive_group(grid, member)
            self._is_recursive_group(member.id, member)

    def process_next(self) -> bool:
        rule = self.prev_processor.get_next_rule()
        if rule is None:
            return False

        elements = getattr(rule, self._slot)
        if not elements:
            self.tmp_queue.append(rule)
            return True

        for obj in elements:
            if isinstance(obj, Group):
                self._is_recursive_group(obj.id, obj)

        self.tmp_queue.append(rule)
        return True


class ExpandGroups(BasicRuleProcessor):
    """Expand group references in all rule element slots.

    Replaces Group objects with their leaf member objects.
    """

    def __init__(self, name: str = 'Expand groups') -> None:
        super().__init__(name)

    def process_next(self) -> bool:

        rule = self.prev_processor.get_next_rule()
        if rule is None:
            return False

        for slot in SLOT_VALUES:
            elements = getattr(rule, slot)
            if not elements:
                continue
            self.compiler.expand_groups_in_element(rule, slot)

        self.tmp_queue.append(rule)
        return True


class ConvertToAtomic(BasicRuleProcessor):
    """Split rules with multiple objects in Src/Dst/Srv into separate
    atomic rules (one object per element).

    Creates the Cartesian product of Src x Dst x Srv.
    """

    def __init__(self, name: str = 'Convert to atomic') -> None:
        super().__init__(name)

    def process_next(self) -> bool:

        rule = self.prev_processor.get_next_rule()
        if rule is None:
            return False

        src = rule.src or [None]
        dst = rule.dst or [None]
        srv = rule.srv or [None]

        if len(src) <= 1 and len(dst) <= 1 and len(srv) <= 1:
            self.tmp_queue.append(rule)
            return True

        for s in src:
            for d in dst:
                for v in srv:
                    r = rule.clone()
                    r.src = [s] if s is not None else []
                    r.dst = [d] if d is not None else []
                    r.srv = [v] if v is not None else []
                    self.tmp_queue.append(r)

        return True


class ConvertToAtomicForAddresses(BasicRuleProcessor):
    """Split rules with multiple address objects in Src/Dst only.

    Unlike ConvertToAtomic, this preserves multi-service rules
    (used after service grouping in iptables compiler).
    """

    def __init__(self, name: str = 'Convert to atomic for addresses') -> None:
        super().__init__(name)

    def process_next(self) -> bool:

        rule = self.prev_processor.get_next_rule()
        if rule is None:
            return False

        src = rule.src or [None]
        dst = rule.dst or [None]

        if len(src) <= 1 and len(dst) <= 1:
            self.tmp_queue.append(rule)
            return True

        for s in src:
            for d in dst:
                r = rule.clone()
                r.src = [s] if s is not None else []
                r.dst = [d] if d is not None else []
                self.tmp_queue.append(r)

        return True


class ConvertToAtomicForInterfaces(BasicRuleProcessor):
    """Split rules with multiple interfaces into separate rules."""

    def __init__(self, name: str = 'Convert to atomic for interfaces') -> None:
        super().__init__(name)

    def process_next(self) -> bool:

        rule = self.prev_processor.get_next_rule()
        if rule is None:
            return False

        if len(rule.itf) <= 1:
            self.tmp_queue.append(rule)
            return True

        for itf_obj in rule.itf:
            r = rule.clone()
            r.itf = [itf_obj]
            self.tmp_queue.append(r)

        return True


class DropRulesByAddressFamily(BasicRuleProcessor):
    """Base class for dropping rules by address family.

    Removes addresses of the wrong family from rule element slots.
    If a slot becomes empty (was non-empty before), drops the entire rule.
    """

    def __init__(self, name: str = '', drop_ipv6: bool = True) -> None:
        super().__init__(name)
        self._drop_ipv6 = drop_ipv6

    def _should_drop(self, obj) -> bool:
        """Return True if this address object should be dropped."""
        if not isinstance(obj, Address):
            return False
        addr_str = obj.get_address()
        if not addr_str:
            return False
        if self._drop_ipv6 and obj.is_v6():
            return True
        return bool(not self._drop_ipv6 and obj.is_v4())

    def _filter_slot(self, rule: CompRule, slot: str) -> bool:
        """Filter address objects in a slot. Returns True if rule should be dropped."""
        elements = getattr(rule, slot)
        if not elements:
            return False  # empty = "any", leave as-is

        new_elements = [obj for obj in elements if not self._should_drop(obj)]
        if elements and not new_elements:
            return True  # slot became empty, drop rule
        setattr(rule, slot, new_elements)
        return False

    def _filter_srv_slot(self, rule: CompRule, slot: str) -> bool:
        """Filter ICMP services by address family. Returns True if rule should be dropped."""
        elements = getattr(rule, slot)
        if not elements:
            return False

        new_elements = []
        for obj in elements:
            if isinstance(obj, ICMPService) and not isinstance(obj, ICMP6Service):
                if not self._drop_ipv6:
                    continue  # drop ICMPv4 when compiling for IPv6
            elif isinstance(obj, ICMP6Service) and self._drop_ipv6:
                continue  # drop ICMPv6 when compiling for IPv4
            new_elements.append(obj)

        if elements and not new_elements:
            return True
        setattr(rule, slot, new_elements)
        return False

    def process_next(self) -> bool:

        rule = self.prev_processor.get_next_rule()
        if rule is None:
            return False

        # Filter address elements
        for slot in ('src', 'dst', 'osrc', 'odst', 'tsrc', 'tdst', 'rdst'):
            if self._filter_slot(rule, slot):
                return True  # drop rule

        # Filter service elements for ICMP
        for slot in ('srv', 'osrv', 'tsrv'):
            if self._filter_srv_slot(rule, slot):
                return True  # drop rule

        self.tmp_queue.append(rule)
        return True


class DropIPv4Rules(DropRulesByAddressFamily):
    """Drop rules that contain only IPv4 addresses (for IPv6 compilation)."""

    def __init__(self, name: str = 'Drop IPv4 rules') -> None:
        super().__init__(name, drop_ipv6=False)


class DropIPv6Rules(DropRulesByAddressFamily):
    """Drop rules that contain only IPv6 addresses (for IPv4 compilation)."""

    def __init__(self, name: str = 'Drop IPv6 rules') -> None:
        super().__init__(name, drop_ipv6=True)


class DropRuleWithEmptyRE(BasicRuleProcessor):
    """Drop rules where a required rule element became empty (size==0).

    Corresponds to C++ ``Compiler::dropRuleWithEmptyRE``.
    After group expansion and address processing, checks if src or dst
    (for policy rules) became literally empty.  An empty element is
    different from "any" — "any" is the initial default, while an empty
    element results from all objects being removed by earlier processors.
    """

    def __init__(self, name: str = 'Drop rules with empty RE') -> None:
        super().__init__(name)

    def process_next(self) -> bool:

        rule = self.prev_processor.get_next_rule()
        if rule is None:
            return False

        # Check if any required element became empty after processing.
        # In our CompRule model, [] means "any" initially, but if a slot
        # was non-empty and became empty due to filtering (address family,
        # etc.), it should be dropped.  The has_empty_re flag is set by
        # processors that remove objects from elements.
        if getattr(rule, 'has_empty_re', False):
            return True  # drop

        self.tmp_queue.append(rule)
        return True


class EliminateDuplicatesInSRC(BasicRuleProcessor):
    """Remove duplicate objects from the src element."""

    def __init__(self, name: str = 'Eliminate duplicates in SRC') -> None:
        super().__init__(name)

    def process_next(self) -> bool:

        rule = self.prev_processor.get_next_rule()
        if rule is None:
            return False
        self.compiler.eliminate_duplicates_in_element(rule, 'src')
        self.tmp_queue.append(rule)
        return True


class EliminateDuplicatesInDST(BasicRuleProcessor):
    """Remove duplicate objects from the dst element."""

    def __init__(self, name: str = 'Eliminate duplicates in DST') -> None:
        super().__init__(name)

    def process_next(self) -> bool:

        rule = self.prev_processor.get_next_rule()
        if rule is None:
            return False
        self.compiler.eliminate_duplicates_in_element(rule, 'dst')
        self.tmp_queue.append(rule)
        return True


class EliminateDuplicatesInSRV(BasicRuleProcessor):
    """Remove duplicate objects from the srv element."""

    def __init__(self, name: str = 'Eliminate duplicates in SRV') -> None:
        super().__init__(name)

    def process_next(self) -> bool:

        rule = self.prev_processor.get_next_rule()
        if rule is None:
            return False
        self.compiler.eliminate_duplicates_in_element(rule, 'srv')
        self.tmp_queue.append(rule)
        return True


class DetectShadowing(BasicRuleProcessor):
    """Detect rule shadowing — abort if an earlier rule completely covers a later one.

    Corresponds to C++ PolicyCompiler::DetectShadowing.
    Accumulates rules and for each new rule checks whether any previously
    seen rule is more general (shadows it).  Aborts compilation on the
    first match.

    Rules with negation, Branch/Continue/Return/Accounting actions,
    fallback, or hidden flags are excluded from the check.
    """

    def __init__(self, name: str = 'Detect shadowing') -> None:
        super().__init__(name)
        self._rules_seen: list[CompRule] = []

    def process_next(self) -> bool:
        rule = self.prev_processor.get_next_rule()
        if rule is None:
            return False

        self.tmp_queue.append(rule)

        # Skip rules that shouldn't participate in shadowing checks
        if rule.fallback or rule.hidden:
            return True
        if rule.get_neg('src') or rule.get_neg('dst') or rule.get_neg('srv'):
            return True
        if rule.action in (
            PolicyAction.Branch,
            PolicyAction.Continue,
            PolicyAction.Return,
            PolicyAction.Accounting,
        ):
            return True

        for prev in self._rules_seen:
            if prev.abs_rule_number == rule.abs_rule_number:
                continue
            if self._rule_shadows(prev, rule):
                self.compiler.warning(
                    f'Rule {prev.position} shadows Rule {rule.position} below it',
                )
                break

        self._rules_seen.append(rule)
        return True

    def _rule_shadows(self, r1: CompRule, r2: CompRule) -> bool:
        """Return True if r1 is more general than r2 (r1 shadows r2)."""
        # Skip r1 candidates with special properties
        if r1.get_neg('src') or r1.get_neg('dst') or r1.get_neg('srv'):
            return False
        if r1.action in (
            PolicyAction.Branch,
            PolicyAction.Continue,
            PolicyAction.Return,
            PolicyAction.Accounting,
        ):
            return False
        # Routing rules may or may not be terminal — skip
        if r1.get_option('routing', False) or r2.get_option('routing', False):
            return False
        # r2 with Continue action is non-terminating, can't be shadowed
        if r2.action == PolicyAction.Continue:
            return False

        # Chain check: rules in different chains can't shadow each other
        if r1.ipt_chain and r2.ipt_chain and r1.ipt_chain != r2.ipt_chain:
            return False

        # Interface check (matches C++ checkInterfacesForShadowing):
        # r1=above, r2=below. If above is "any" interface → can shadow.
        # If above is specific and below is "any" → can't shadow.
        # If both specific → must match.
        r1_itf = r1.itf[0] if r1.itf else None  # above rule
        r2_itf = r2.itf[0] if r2.itf else None  # below rule
        if r1_itf is None:
            pass  # r1 is "any" interface → can shadow anything
        elif r2_itf is None:
            return False  # r1 specific, r2 "any" → can't shadow
        else:
            r1_id = getattr(r1_itf, 'id', None)
            r2_id = getattr(r2_itf, 'id', None)
            if r1_id is not None and r2_id is not None and r1_id != r2_id:
                return False

        # Direction check: normalize Both to match the other rule's direction
        # (matches C++ PolicyCompiler::checkForShadowing)
        d1 = r1.direction or Direction.Both
        d2 = r2.direction or Direction.Both
        if d1 == Direction.Both:
            d1 = d2
        if d2 == Direction.Both:
            d2 = d1
        if d1 != d2:
            return False

        # All three rule elements must satisfy containment
        return (
            self._element_shadows(r1.src, r2.src)
            and self._element_shadows(r1.dst, r2.dst)
            and self._srv_element_shadows(r1.srv, r2.srv)
        )

    @staticmethod
    def _element_shadows(e1: list, e2: list) -> bool:
        """Return True if address element e1 is a superset of e2.

        e1 shadows e2 when every object in e2 is "contained by" at
        least one object in e1.  An empty element (= "any") contains
        everything.
        """
        if not e1:  # e1 is "any" → contains everything
            return True
        if not e2:  # e2 is "any" → only contained by "any"
            return False
        return all(any(_addr_contains(a1, a2) for a1 in e1) for a2 in e2)

    @staticmethod
    def _srv_element_shadows(e1: list, e2: list) -> bool:
        """Return True if service element e1 is a superset of e2."""
        if not e1 and not e2:  # both "any" → no shadowing (C++ semantics)
            return False
        if not e1:  # e1 is "any" → contains everything
            return True
        if not e2:  # e2 is "any" → only contained by "any"
            return False
        return all(any(_srv_contains(s1, s2) for s1 in e1) for s2 in e2)


def _addr_contains(a1, a2) -> bool:
    """Return True if address a1 contains (is a superset of) a2.

    Uses the ipaddress module for network/host containment checks.
    """
    if a1 is a2 or a1.id == a2.id:
        return True

    # "any" address contains everything (AddressRange has no is_any)
    if isinstance(a1, Address) and not isinstance(a1, AddressRange) and a1.is_any():
        return True
    if isinstance(a2, Address) and not isinstance(a2, AddressRange) and a2.is_any():
        return False

    try:
        r1 = _addr_range(a1)
        r2 = _addr_range(a2)
    except (ValueError, TypeError):
        return False

    if r1 is None or r2 is None:
        return False

    return r1[0] <= r2[0] and r2[1] <= r1[1]


def _addr_range(obj) -> tuple | None:
    """Return (first_addr, last_addr) for an address object."""
    if isinstance(obj, AddressRange):
        start = obj.get_start_address()
        end = obj.get_end_address()
        if start and end:
            return (_ipa.ip_address(start), _ipa.ip_address(end))
        return None

    if isinstance(obj, (Network, NetworkIPv6)):
        addr_s = obj.get_address()
        mask_s = obj.get_netmask()
        if addr_s and mask_s:
            try:
                net = _ipa.ip_network(f'{addr_s}/{mask_s}', strict=False)
                return (net.network_address, net.broadcast_address)
            except ValueError:
                return None
        return None

    # Single host / IPv4 / IPv6 / Interface address
    if isinstance(obj, Address):
        addr_s = obj.get_address()
        if addr_s:
            addr = _ipa.ip_address(addr_s)
            return (addr, addr)
    return None


def _srv_data_val(srv, key: str) -> str:
    """Get a service data-dict value as a string for comparison.

    Matches C++ ``FWObject::getStr()`` semantics: missing/None → empty string.
    """
    if not srv.data:
        return ''
    val = srv.data.get(key)
    if val is None or val == '':
        return ''
    return str(val)


_IP_FLAGS = ('fragm', 'short_fragm', 'lsrr', 'ssrr', 'rr', 'ts')


def _srv_contains(s1, s2) -> bool:
    """Return True if service s1 contains (is a superset of) s2."""
    if s1 is s2 or s1.id == s2.id:
        return True

    s1_any = isinstance(s1, Service) and s1.is_any()
    s2_any = isinstance(s2, Service) and s2.is_any()
    # C++: both "any" → false (no shadowing between identical "any" services)
    if s1_any and s2_any:
        return False
    # "any" service contains specific
    if s1_any:
        return True
    # specific cannot contain "any"
    if s2_any:
        return False

    # IPService: check IP flags + TOS/DSCP before protocol comparison
    # (C++ Compiler_ops.cpp:373-400)
    if isinstance(s1, IPService) and isinstance(s2, IPService):
        # All six IP option flags must match
        for flag in _IP_FLAGS:
            if _srv_data_val(s1, flag) != _srv_data_val(s2, flag):
                return False
        # TOS and DSCP codes must match
        if _srv_data_val(s1, 'tos') != _srv_data_val(s2, 'tos'):
            return False
        if _srv_data_val(s1, 'dscp') != _srv_data_val(s2, 'dscp'):
            return False
        p1 = s1.get_protocol_number()
        p2 = s2.get_protocol_number()
        if p1 == p2:
            return True
        # proto 0 (any IP) in s1 shadows specific proto in s2
        return p1 == 0

    # TCP/UDP port range containment
    if isinstance(s1, TCPUDPService) and isinstance(s2, TCPUDPService):
        if s1.get_protocol_number() != s2.get_protocol_number():
            return False
        # TCP flag check: flags and masks must match (C++ Compiler_ops.cpp:406-415)
        if isinstance(s1, TCPService) and isinstance(s2, TCPService):
            if (s1.tcp_flags or {}) != (s2.tcp_flags or {}):
                return False
            if (s1.tcp_flags_masks or {}) != (s2.tcp_flags_masks or {}):
                return False
        srs1 = s1.src_range_start or 0
        sre1 = s1.src_range_end or 0
        drs1 = s1.dst_range_start or 0
        dre1 = s1.dst_range_end or 0
        srs2 = s2.src_range_start or 0
        sre2 = s2.src_range_end or 0
        drs2 = s2.dst_range_start or 0
        dre2 = s2.dst_range_end or 0
        # Normalize: 0 means "any" → full range (C++ uses 65536)
        if srs1 == 0 and sre1 == 0:
            srs1, sre1 = 0, 65536
        if drs1 == 0 and dre1 == 0:
            drs1, dre1 = 0, 65536
        if srs2 == 0 and sre2 == 0:
            srs2, sre2 = 0, 65536
        if drs2 == 0 and dre2 == 0:
            drs2, dre2 = 0, 65536
        return srs1 <= srs2 and sre2 <= sre1 and drs1 <= drs2 and dre2 <= dre1

    # ICMP: type -1 (any) in s1 shadows specific type in s2
    # C++: returns (o1.type != -1 && o2.type == -1) where o1=below, o2=above
    # Python: s1=above, s2=below, so: (s2.type != -1 && s1.type == -1)
    if isinstance(s1, ICMPService) and isinstance(s2, ICMPService):
        if type(s1) is not type(s2):  # ICMPv4 vs ICMPv6
            return False
        codes1 = s1.codes or {}
        codes2 = s2.codes or {}
        t1 = codes1.get('type', -1)
        t2 = codes2.get('type', -1)
        return t2 != -1 and t1 == -1

    # Cross-type: IPService with proto=0 and all IP flags cleared
    # can shadow any other service type (C++ Compiler_ops.cpp:453-474)
    if isinstance(s1, IPService) and not isinstance(s2, IPService):
        if s1.get_protocol_number() != 0:
            return False
        data = s1.data or {}
        for flag in _IP_FLAGS:
            val = data.get(flag)
            if val is not None and str(val) == 'True':
                return False
        return True

    return False


class CheckForTCPEstablished(BasicRuleProcessor):
    """Check for TCPService with deprecated 'established' flag.

    The 'established' option is not supported by iptables (or nftables).
    Use stateful rules instead.

    Corresponds to C++ ``Compiler::CheckForTCPEstablished``.
    """

    def process_next(self) -> bool:
        rule = self.prev_processor.get_next_rule()
        if rule is None:
            return False

        # Check srv for policy rules, osrv for NAT rules
        srv_slot = rule.srv if rule.type == 'PolicyRule' else getattr(rule, 'osrv', [])
        for srv in srv_slot:
            if isinstance(srv, TCPService):
                established = (srv.data or {}).get('established', False)
                if str(established).lower() in ('true', '1'):
                    self.compiler.abort(
                        rule,
                        f'TCPService object with option "established" is not '
                        f'supported by firewall platform '
                        f'"{self.compiler.my_platform_name()}". '
                        f'Use stateful rule instead.',
                    )

        self.tmp_queue.append(rule)
        return True


class ReplaceClusterInterfaceInItfRE(BasicRuleProcessor):
    """Replace cluster failover interfaces with member firewall interfaces.

    If a rule references a cluster interface (failover interface), replace
    it with the corresponding interface of the member firewall being compiled.

    Corresponds to C++ Compiler::replaceClusterInterfaceInItfRE.
    """

    def __init__(self, name: str, slot: str) -> None:
        super().__init__(name)
        self._slot = slot

    def process_next(self) -> bool:
        rule = self.prev_processor.get_next_rule()
        if rule is None:
            return False

        from firewallfabrik.core.objects import FailoverClusterGroup, Interface

        elements = getattr(rule, self._slot)
        if not elements:
            self.tmp_queue.append(rule)
            return True

        new_elements = []
        for obj in elements:
            if not isinstance(obj, Interface):
                new_elements.append(obj)
                continue

            # Check if this is a failover interface
            member_iface = None
            if hasattr(obj, 'subinterfaces'):
                for sub in obj.subinterfaces or []:
                    if isinstance(sub, FailoverClusterGroup):
                        member_iface = self._get_member_interface(sub)
                        break

            if member_iface is not None:
                new_elements.append(member_iface)
            else:
                new_elements.append(obj)

        setattr(rule, self._slot, new_elements)
        self.tmp_queue.append(rule)
        return True

    def _get_member_interface(self, fg) -> Interface | None:
        """Find the interface for the member firewall in a failover group."""
        # The FailoverClusterGroup maps cluster interfaces to member interfaces
        fw = self.compiler.fw
        if hasattr(fg, 'get_interface_for_member'):
            return fg.get_interface_for_member(fw)
        return None


class AssignUniqueRuleId(BasicRuleProcessor):
    """Assign sequential abs_rule_number to each rule."""

    def __init__(self, name: str = 'Assign unique rule ID') -> None:
        super().__init__(name)
        self._counter = 0

    def process_next(self) -> bool:

        rule = self.prev_processor.get_next_rule()
        if rule is None:
            return False
        rule.abs_rule_number = self._counter
        self._counter += 1
        self.tmp_queue.append(rule)
        return True
