"""FlowSpec route configuration parsers.

Parsers for FlowSpec (RFC 5575) route configuration elements:
match conditions, traffic actions, and redirects.

Match conditions:
    source/destination: IP prefix filters
    protocol/next_header: IP protocol filters
    any_port/source_port/destination_port: Port filters
    tcp_flags/fragment/dscp: Packet header filters

Traffic actions:
    accept/discard: Allow or drop traffic
    rate_limit: Limit traffic rate (bytes/sec or packets/sec)
    redirect: Redirect to VRF or next-hop
    mark: Set DSCP marking
    action: Sample and/or terminal flags
"""

from __future__ import annotations

from typing import Generator, Type, TypeVar, TYPE_CHECKING, cast

if TYPE_CHECKING:
    from exabgp.configuration.core.parser import Tokeniser

from exabgp.bgp.message.open.asn import ASN
from exabgp.bgp.message.open.capability.asn4 import ASN4
from exabgp.bgp.message.update.attribute import AttributeCollection
from exabgp.bgp.message.update.attribute.community.extended import (
    ExtendedCommunities,
    ExtendedCommunitiesIPv6,
    InterfaceSet,
    TrafficAction,
    TrafficMark,
    TrafficNextHopIPv4IETF,
    TrafficNextHopIPv6IETF,
    TrafficNextHopSimpson,
    TrafficRate,
    TrafficRatePackets,
    TrafficRedirect,
    TrafficRedirectASN4,
    TrafficRedirectIPv6,
)
from exabgp.bgp.message.update.nlri import (
    Flow,
)
from exabgp.bgp.message.update.nlri.flow import (
    BinaryOperator,
    Flow4Destination,
    Flow4Source,
    Flow6Destination,
    Flow6Source,
    FlowAnyPort,
    FlowDestinationPort,
    FlowDSCP,
    FlowFlowLabel,
    FlowFragment,
    FlowICMPCode,
    FlowICMPType,
    FlowIPProtocol,
    FlowIPv4,
    FlowIPv6,
    FlowNextHeader,
    FlowPacketLength,
    FlowSourcePort,
    FlowTCPFlag,
    FlowTrafficClass,
    NumericOperator,
)
from exabgp.logger import log, lazymsg
from exabgp.protocol.family import (
    AFI,
)
from exabgp.protocol.ip import IP, IPSelf, IPv4, IPv6
from exabgp.rib.route import Route

# TypeVar for flow condition classes
FlowConditionT = TypeVar(
    'FlowConditionT',
    FlowIPProtocol,
    FlowNextHeader,
    FlowAnyPort,
    FlowSourcePort,
    FlowDestinationPort,
    FlowICMPType,
    FlowICMPCode,
    FlowTCPFlag,
    FlowPacketLength,
    FlowDSCP,
    FlowTrafficClass,
    FlowFragment,
    FlowFlowLabel,
)

SINGLE_SLASH = 1  # Format with single slash (IP/prefix)
DOUBLE_SLASH = 2  # IPv6 format with offset (IP/prefix/offset)

# Bit width constants for local administrator field validation
LOCAL_ADMIN_16_BITS = 16  # 16-bit local administrator field
LOCAL_ADMIN_32_BITS = 32  # 32-bit local administrator field

# Interface set direction values
DIRECTION_INPUT = 1
DIRECTION_OUTPUT = 2
DIRECTION_INPUT_OUTPUT = 3

# Colon count for interface set format validation
INTERFACE_SET_COLON_COUNT = 3  # Format: transitive:direction:asn:route_target

# Traffic rate limiting constants
MIN_RATE_LIMIT_BPS = 9600  # Minimum rate limit in bytes per second
MAX_RATE_LIMIT_BPS = 1000000000000  # Maximum rate limit (1 terabyte/s)

# DSCP (Differentiated Services Code Point) value range
DSCP_MAX_VALUE = 0b111111  # DSCP is a 6-bit field (0-63)


def flow() -> Route:
    from exabgp.protocol.ip import IP

    nlri = Flow.make_flow()
    # Create with explicit nexthop=NoNextHop; will be updated via with_nexthop() when parsed
    return Route(nlri, AttributeCollection(), nexthop=IP.NoNextHop)


def source(tokeniser: 'Tokeniser') -> Generator[Flow4Source | Flow6Source, None, None]:
    """Update source to handle both IPv4 and IPv6 flows."""
    data: str = tokeniser()
    # Check if it's IPv4
    if data.count('.') == IPv4.DOT_COUNT and data.count(':') == 0:
        ip: str
        netmask: str
        ip, netmask = data.split('/')
        raw: bytes = b''.join(bytes([int(_)]) for _ in ip.split('.'))
        yield Flow4Source.make_prefix4(raw, int(netmask))
    # Check if it's IPv6 without an offset
    elif data.count(':') >= IPv6.COLON_MIN and data.count('/') == SINGLE_SLASH:
        ip, netmask = data.split('/')
        yield Flow6Source.make_prefix6(IP.pton(ip), int(netmask), 0)
    # Check if it's IPv6 with an offset
    elif data.count(':') >= IPv6.COLON_MIN and data.count('/') == DOUBLE_SLASH:
        offset: str
        ip, netmask, offset = data.split('/')
        yield Flow6Source.make_prefix6(IP.pton(ip), int(netmask), int(offset))


def destination(tokeniser: 'Tokeniser') -> Generator[Flow4Destination | Flow6Destination, None, None]:
    """Update destination to handle both IPv4 and IPv6 flows."""
    data: str = tokeniser()
    # Check if it's IPv4
    if data.count('.') == IPv4.DOT_COUNT and data.count(':') == 0:
        ip: str
        netmask: str
        ip, netmask = data.split('/')
        raw: bytes = b''.join(bytes([int(_)]) for _ in ip.split('.'))
        yield Flow4Destination.make_prefix4(raw, int(netmask))
    # Check if it's IPv6 without an offset
    elif data.count(':') >= IPv6.COLON_MIN and data.count('/') == SINGLE_SLASH:
        ip, netmask = data.split('/')
        yield Flow6Destination.make_prefix6(IP.pton(ip), int(netmask), 0)
    # Check if it's IPv6 with an offset
    elif data.count(':') >= IPv6.COLON_MIN and data.count('/') == DOUBLE_SLASH:
        offset: str
        ip, netmask, offset = data.split('/')
        yield Flow6Destination.make_prefix6(IP.pton(ip), int(netmask), int(offset))


# Expressions


def _operator_numeric(string: str) -> tuple[int, str]:
    try:
        char: str = string[0].lower()
        if char == '=':
            return NumericOperator.EQ, string[1:]
        operator: int
        if char == '>':
            operator = NumericOperator.GT
        elif char == '<':
            operator = NumericOperator.LT
        elif char == '!':
            if string.startswith('!='):
                return NumericOperator.NEQ, string[2:]
            raise ValueError('invalid operator syntax {}'.format(string))
        elif char == 't' and string.lower().startswith('true'):
            return NumericOperator.TRUE, string[4:]
        elif char == 'f' and string.lower().startswith('false'):
            return NumericOperator.FALSE, string[5:]
        else:
            return NumericOperator.EQ, string
        if string[1] == '=':
            operator += NumericOperator.EQ
            return operator, string[2:]
        return operator, string[1:]
    except IndexError:
        raise ValueError('Invalid expression (too short) {}'.format(string)) from None


def _operator_binary(string: str) -> tuple[int, str]:
    try:
        if string[0] == '=':
            return BinaryOperator.MATCH, string[1:]
        if string[0] == '!':
            if string.startswith('!='):
                return BinaryOperator.DIFF, string[2:]
            return BinaryOperator.NOT, string[1:]
        return BinaryOperator.INCLUDE, string
    except IndexError:
        raise ValueError('Invalid expression (too short) {}'.format(string)) from None


def _value(string: str) -> tuple[str, str]:
    ls: int = 0
    for c in string:
        if c not in [
            '&',
        ]:
            ls += 1
            continue
        break
    return string[:ls], string[ls:]


# parse [ content1 content2 content3 ]
# parse =80 or >80 or <25 or &>10<20
def _generic_condition(tokeniser: 'Tokeniser', klass: Type[FlowConditionT]) -> Generator[FlowConditionT, None, None]:
    # Validate that the flow rule component is valid for the current address family
    afi = tokeniser.afi
    if afi == AFI.ipv4 and not issubclass(klass, FlowIPv4):
        raise ValueError(f"'{klass.__name__}' is not valid for IPv4 flow routes (IPv6-only component)")
    if afi == AFI.ipv6 and not issubclass(klass, FlowIPv6):
        raise ValueError(f"'{klass.__name__}' is not valid for IPv6 flow routes (IPv4-only component)")

    _operator = _operator_binary if klass.OPERATION == 'binary' else _operator_numeric
    data: str = tokeniser()
    AND: int = BinaryOperator.NOP
    if data == '[':
        data = tokeniser()
        while True:
            if data == ']':
                break
            operator: int
            _: str
            operator, _ = _operator(data)
            value: str
            value, data = _value(_)
            yield klass(AND | operator, klass.converter(value))
            if data:
                if data[0] != '&':
                    raise ValueError('Unknown binary operator {}'.format(data[0]))
                AND = BinaryOperator.AND
                data = data[1:]
                if not data:
                    raise ValueError('Can not finish an expresion on an &')
            else:
                AND = BinaryOperator.NOP
                data = tokeniser()
    else:
        while data:
            operator, _ = _operator(data)
            value, data = _value(_)
            yield klass(operator | AND, klass.converter(value))
            if data:
                if data[0] != '&':
                    raise ValueError('Unknown binary operator {}'.format(data[0]))
                AND = BinaryOperator.AND
                data = data[1:]


def any_port(tokeniser: 'Tokeniser') -> Generator[FlowAnyPort, None, None]:
    yield from _generic_condition(tokeniser, FlowAnyPort)


def source_port(tokeniser: 'Tokeniser') -> Generator[FlowSourcePort, None, None]:
    yield from _generic_condition(tokeniser, FlowSourcePort)


def destination_port(tokeniser: 'Tokeniser') -> Generator[FlowDestinationPort, None, None]:
    yield from _generic_condition(tokeniser, FlowDestinationPort)


def packet_length(tokeniser: 'Tokeniser') -> Generator[FlowPacketLength, None, None]:
    yield from _generic_condition(tokeniser, FlowPacketLength)


def tcp_flags(tokeniser: 'Tokeniser') -> Generator[FlowTCPFlag, None, None]:
    yield from _generic_condition(tokeniser, FlowTCPFlag)


def protocol(tokeniser: 'Tokeniser') -> Generator[FlowIPProtocol, None, None]:
    yield from _generic_condition(tokeniser, FlowIPProtocol)


def next_header(tokeniser: 'Tokeniser') -> Generator[FlowNextHeader, None, None]:
    yield from _generic_condition(tokeniser, FlowNextHeader)


def icmp_type(tokeniser: 'Tokeniser') -> Generator[FlowICMPType, None, None]:
    yield from _generic_condition(tokeniser, FlowICMPType)


def icmp_code(tokeniser: 'Tokeniser') -> Generator[FlowICMPCode, None, None]:
    yield from _generic_condition(tokeniser, FlowICMPCode)


def fragment(tokeniser: 'Tokeniser') -> Generator[FlowFragment, None, None]:
    yield from _generic_condition(tokeniser, FlowFragment)


def dscp(tokeniser: 'Tokeniser') -> Generator[FlowDSCP, None, None]:
    yield from _generic_condition(tokeniser, FlowDSCP)


def traffic_class(tokeniser: 'Tokeniser') -> Generator[FlowTrafficClass, None, None]:
    yield from _generic_condition(tokeniser, FlowTrafficClass)


def flow_label(tokeniser: 'Tokeniser') -> Generator[FlowFlowLabel, None, None]:
    yield from _generic_condition(tokeniser, FlowFlowLabel)


def next_hop(tokeniser: 'Tokeniser') -> IP:
    """Parse next-hop for FlowSpec routes.

    Returns IP (not NextHop attribute) since Flow NLRI nexthop field
    stores IP addresses, not BGP attributes. IPSelf is a subclass of IP.
    """
    value: str = tokeniser()

    if value.lower() == 'self':
        return IPSelf(AFI.ipv4)
    return IP.from_string(value)


def accept(tokeniser: 'Tokeniser') -> None:
    return


def discard(tokeniser: 'Tokeniser') -> ExtendedCommunities:
    # README: We are setting the ASN as zero as that what Juniper (and Arbor) did when we created a local flow route
    return ExtendedCommunities().add(TrafficRate.make_traffic_rate(ASN(0), 0))


def rate_limit(tokeniser: 'Tokeniser') -> ExtendedCommunities:
    # README: We are setting the ASN as zero as that what Juniper (and Arbor) did when we created a local flow route
    speed: int = int(tokeniser())
    unit = tokeniser.peek()
    if unit in ('bytes', 'packets'):
        tokeniser()
    else:
        unit = 'bytes'

    if unit == 'packets':
        return ExtendedCommunities().add(TrafficRatePackets.make_traffic_rate_packets(ASN(0), speed))

    if speed < MIN_RATE_LIMIT_BPS and speed != 0:
        log.warning(
            lazymsg('flow.rate_limit.warning reason=too_low min_bps={min_bps}', min_bps=MIN_RATE_LIMIT_BPS),
            'configuration',
        )
    if speed > MAX_RATE_LIMIT_BPS:
        speed = MAX_RATE_LIMIT_BPS
        log.warning(
            lazymsg(
                'flow.rate_limit.warning reason=too_high max_bps={max_bps} requested={speed}',
                max_bps=MAX_RATE_LIMIT_BPS,
                speed=speed,
            ),
            'configuration',
        )
    return ExtendedCommunities().add(TrafficRate.make_traffic_rate(ASN(0), speed))


def redirect(tokeniser: 'Tokeniser') -> tuple[IP, ExtendedCommunities]:
    data: str = tokeniser()
    count: int = data.count(':')

    # the redirect is an IPv4 or an IPv6 nexthop
    if count == 0 or (count > 1 and '[' not in data and ']' not in data):
        return IP.from_string(data), ExtendedCommunities().add(
            TrafficNextHopSimpson.make_traffic_nexthop_simpson(False)
        )

    # the redirect is an IPv6 nexthop using [] notation
    if data.startswith('[') and data.endswith(']'):
        return IP.from_string(data[1:-1]), ExtendedCommunities().add(
            TrafficNextHopSimpson.make_traffic_nexthop_simpson(False)
        )

    # the redirect is an ipv6:NN route-target using []: notation
    if count > 1:
        if ']:' not in data:
            try:
                ip: IP = IP.from_string(data)
                return ip, ExtendedCommunities().add(TrafficNextHopSimpson.make_traffic_nexthop_simpson(False))
            except (OSError, ValueError):
                raise ValueError('it looks like you tried to use an IPv6 but did not enclose it in []') from None

        nn: str
        ip_str: str
        ip_str, nn = data.split(']:')
        ip_str = ip_str.replace('[', '', 1)

        if int(nn) >= pow(2, LOCAL_ADMIN_16_BITS):
            raise ValueError('Local administrator field is a 16 bits number, value too large {}'.format(nn))
        return IP.from_string(ip_str), ExtendedCommunities().add(
            TrafficRedirectIPv6.make_traffic_redirect_ipv6(ip_str, int(nn))
        )

    # the redirect is an ASN:NN route-target
    if True:  # count == 1:
        prefix: str
        suffix: str
        prefix, suffix = data.split(':', 1)

        if prefix.count('.'):
            raise ValueError(
                'this format has been deprecated as it does not make sense and it is not supported by other vendors',
            )

        asn: int = int(prefix)
        nn_int: int = int(suffix)

        if not ASN4.validate(asn):
            raise ValueError(f'asn is invalid, must be 0 to {ASN.MAX_4BYTE} (32 bits): {asn}')

        if asn > ASN.MAX_2BYTE:
            if nn_int >= pow(2, LOCAL_ADMIN_16_BITS):
                raise ValueError(
                    'asn is a 32 bits number, local administrator field can only be 16 bit {}'.format(nn_int)
                )
            return IP.NoNextHop, ExtendedCommunities().add(
                TrafficRedirectASN4.make_traffic_redirect_asn4(ASN4(asn), nn_int)
            )

        if nn_int >= pow(2, LOCAL_ADMIN_32_BITS):
            raise ValueError('Local administrator field is a 32 bits number, value too large {}'.format(nn_int))

        return IP.NoNextHop, ExtendedCommunities().add(TrafficRedirect.make_traffic_redirect(ASN(asn), nn_int))

    raise ValueError('redirect format incorrect')


def redirect_next_hop(tokeniser: 'Tokeniser') -> ExtendedCommunities:
    return ExtendedCommunities().add(TrafficNextHopSimpson.make_traffic_nexthop_simpson(False))


def redirect_next_hop_ietf(tokeniser: 'Tokeniser') -> ExtendedCommunities | ExtendedCommunitiesIPv6:
    ip: IP = IP.from_string(tokeniser())
    if ip.ipv4():
        return ExtendedCommunities().add(TrafficNextHopIPv4IETF.make_traffic_nexthop_ipv4(cast(IPv4, ip), False))
    return ExtendedCommunitiesIPv6().add(TrafficNextHopIPv6IETF.make_traffic_nexthop_ipv6(cast(IPv6, ip), False))


def copy(tokeniser: 'Tokeniser') -> tuple[IP, ExtendedCommunities]:
    return IP.from_string(tokeniser()), ExtendedCommunities().add(
        TrafficNextHopSimpson.make_traffic_nexthop_simpson(True)
    )


def mark(tokeniser: 'Tokeniser') -> ExtendedCommunities:
    value: str = tokeniser()

    if not value.isdigit():
        raise ValueError(f"'{value}' is not a valid DSCP mark value\n  Must be a number 0-{DSCP_MAX_VALUE}")

    dscp_value: int = int(value)

    if dscp_value < 0 or dscp_value > DSCP_MAX_VALUE:
        raise ValueError(f'DSCP value {dscp_value} is out of range\n  Must be 0-{DSCP_MAX_VALUE}')

    return ExtendedCommunities().add(TrafficMark.make_traffic_mark(dscp_value))


def action(tokeniser: 'Tokeniser') -> ExtendedCommunities:
    value: str = tokeniser()

    sample: bool = 'sample' in value
    terminal: bool = 'terminal' in value

    if not sample and not terminal:
        raise ValueError(f"'{value}' is not a valid flow action\n  Valid options: sample, terminal, sample-terminal")

    return ExtendedCommunities().add(TrafficAction.make_traffic_action(sample, terminal))


def _interface_set(data: str) -> InterfaceSet:
    colon_count = data.count(':')

    trans: str
    direction: str
    prefix: str
    suffix: str
    trans_bool: bool

    if colon_count == INTERFACE_SET_COLON_COUNT:
        # New format: transitive:direction:asn:group-id
        trans, direction, prefix, suffix = data.split(':', INTERFACE_SET_COLON_COUNT)
        if trans == 'transitive':
            trans_bool = True
        elif trans == 'non-transitive':
            trans_bool = False
        else:
            raise ValueError(f"'{trans}' is not a valid transitivity type\n  Valid options: transitive, non-transitive")
    elif colon_count == 2:
        # Old format (backward compat): direction:asn:group-id (defaults to transitive)
        direction, prefix, suffix = data.split(':', 2)
        trans_bool = True
    else:
        raise ValueError(
            f"'{data}' is not a valid interface-set\n"
            f'  Format: <transitive|non-transitive>:<input|output|input-output>:<asn>:<group-id>\n'
            f'  Or: <input|output|input-output>:<asn>:<group-id> (defaults to transitive)'
        )
    if prefix.count('.'):
        raise ValueError(f"'{prefix}' is not a valid ASN\n  Must be a 32-bit integer (not dotted notation)")
    int_direction: int
    if direction == 'input':
        int_direction = DIRECTION_INPUT
    elif direction == 'output':
        int_direction = DIRECTION_OUTPUT
    elif direction == 'input-output':
        int_direction = DIRECTION_INPUT_OUTPUT
    else:
        raise ValueError(f"'{direction}' is not a valid direction\n  Valid options: input, output, input-output")
    asn: int = int(prefix)
    group_id: int = int(suffix)
    if not ASN4.validate(asn):
        raise ValueError(f'ASN {asn} is invalid\n  Must be 0 to {ASN.MAX_4BYTE} (32 bits)')
    if not InterfaceSet.validate_group_id(group_id):
        raise ValueError(f'group-id {group_id} is invalid\n  Must be 0 to {InterfaceSet.GROUP_ID_MAX} (14 bits)')
    return InterfaceSet.make_interface_set(ASN(asn), group_id, int_direction, trans_bool)


def interface_set(tokeniser: 'Tokeniser') -> ExtendedCommunities:
    communities: ExtendedCommunities = ExtendedCommunities()

    value: str = tokeniser()
    if value == '[':
        while True:
            value = tokeniser()
            if value == ']':
                break
            communities.add(_interface_set(value))
    else:
        communities.add(_interface_set(value))

    return communities
