"""neighbor/__init__.py

Created by Thomas Mangin on 2015-06-04.
Copyright (c) 2009-2017 Exa Networks. All rights reserved.
License: 3-clause BSD. (See the COPYRIGHT file)
"""

# import sys
from __future__ import annotations

from copy import deepcopy
from typing import Any

from exabgp.bgp.message.update.nlri import NLRI
from exabgp.bgp.neighbor import Neighbor
from exabgp.bgp.neighbor.capability import GracefulRestartConfig
from exabgp.configuration.core import Error, Parser, Scope, Section
from exabgp.configuration.neighbor.api import ParseAPI
from exabgp.configuration.neighbor.family import ParseAddPath, ParseFamily
from exabgp.configuration.neighbor.parser import (
    hold_time,
    inherit,
    local_address,
    router_id,
    ttl,
)

# from exabgp.configuration.parser import asn
from exabgp.configuration.parser import auto_asn, auto_boolean

# Removed imports migrated to schema validators:
# description, domainname, hostname, md5, rate_limit, source_interface
from exabgp.configuration.schema import ActionKey, ActionOperation, ActionTarget, Container, Leaf, ValueType
from exabgp.configuration.validator import IntValidators

# Removed imports migrated to schema validators: boolean, ip, peer_ip, port
from exabgp.environment import getenv
from exabgp.logger import lazymsg, log
from exabgp.protocol.family import AFI, SAFI, FamilyTuple
from exabgp.protocol.ip import IP, IPRange
from exabgp.util.enumeration import TriState


class ParseNeighbor(Section):
    TTL_SECURITY = 255

    # Schema definition for BGP neighbor configuration
    # Schema uses explicit enum fields (target/operation/key)
    schema = Container(
        description='BGP neighbor (peer) configuration',
        children={
            # Session parameters
            'peer-address': Leaf(
                type=ValueType.IP_RANGE,
                description='IP address or range of the BGP peer',
                mandatory=True,
                example='127.0.0.1',
                target=ActionTarget.SCOPE,
                operation=ActionOperation.SET,
                key=ActionKey.COMMAND,
            ),
            'local-address': Leaf(
                type=ValueType.IP_ADDRESS,
                description='Local IP address for the BGP session',
                target=ActionTarget.SCOPE,
                operation=ActionOperation.SET,
                key=ActionKey.COMMAND,
            ),
            'local-link-local': Leaf(
                type=ValueType.IP_ADDRESS,
                description='Local IPv6 link-local address for LLNH capability (fe80::/10)',
                target=ActionTarget.SCOPE,
                operation=ActionOperation.SET,
                key=ActionKey.COMMAND,
            ),
            'local-as': Leaf(
                type=ValueType.ASN,
                description='Local AS number (or "auto" to copy peer-as)',
                mandatory=True,
                target=ActionTarget.SCOPE,
                operation=ActionOperation.SET,
                key=ActionKey.COMMAND,
            ),
            'peer-as': Leaf(
                type=ValueType.ASN,
                description='Peer AS number (or "auto" to copy local-as)',
                mandatory=True,
                target=ActionTarget.SCOPE,
                operation=ActionOperation.SET,
                key=ActionKey.COMMAND,
            ),
            'router-id': Leaf(
                type=ValueType.IP_ADDRESS,
                description='BGP router ID (defaults to local-address)',
                target=ActionTarget.SCOPE,
                operation=ActionOperation.SET,
                key=ActionKey.COMMAND,
            ),
            'description': Leaf(
                type=ValueType.STRING,
                description='Neighbor description',
                target=ActionTarget.SCOPE,
                operation=ActionOperation.SET,
                key=ActionKey.COMMAND,
            ),
            'host-name': Leaf(
                type=ValueType.STRING,
                description='Hostname capability value',
                target=ActionTarget.SCOPE,
                operation=ActionOperation.SET,
                key=ActionKey.COMMAND,
            ),
            'domain-name': Leaf(
                type=ValueType.STRING,
                description='Domain name capability value',
                target=ActionTarget.SCOPE,
                operation=ActionOperation.SET,
                key=ActionKey.COMMAND,
            ),
            # Timers
            'hold-time': Leaf(
                type=ValueType.INTEGER,
                description='BGP hold time in seconds (0 disables)',
                default=180,
                validator=IntValidators.hold_time(),
                target=ActionTarget.SCOPE,
                operation=ActionOperation.SET,
                key=ActionKey.COMMAND,
            ),
            'rate-limit': Leaf(
                type=ValueType.INTEGER,
                description='Rate limit for updates (messages per second)',
                target=ActionTarget.SCOPE,
                operation=ActionOperation.SET,
                key=ActionKey.COMMAND,
            ),
            # Connection options
            'passive': Leaf(
                type=ValueType.BOOLEAN,
                description='Wait for incoming connections',
                default=True,
                target=ActionTarget.SCOPE,
                operation=ActionOperation.SET,
                key=ActionKey.COMMAND,
            ),
            'listen': Leaf(
                type=ValueType.PORT,
                description='Local TCP port to listen on',
                default=179,
                target=ActionTarget.SCOPE,
                operation=ActionOperation.SET,
                key=ActionKey.COMMAND,
            ),
            'connect': Leaf(
                type=ValueType.PORT,
                description='Remote TCP port to connect to',
                default=179,
                target=ActionTarget.SCOPE,
                operation=ActionOperation.SET,
                key=ActionKey.COMMAND,
            ),
            'source-interface': Leaf(
                type=ValueType.STRING,
                description='Source interface for BGP session',
                target=ActionTarget.SCOPE,
                operation=ActionOperation.SET,
                key=ActionKey.COMMAND,
            ),
            # TTL security
            'outgoing-ttl': Leaf(
                type=ValueType.INTEGER,
                description='TTL for outgoing packets (255 for GTSM)',
                validator=IntValidators.range(1, 255),
                target=ActionTarget.SCOPE,
                operation=ActionOperation.SET,
                key=ActionKey.COMMAND,
            ),
            'incoming-ttl': Leaf(
                type=ValueType.INTEGER,
                description='Minimum TTL for incoming packets (GTSM)',
                validator=IntValidators.range(1, 255),
                target=ActionTarget.SCOPE,
                operation=ActionOperation.SET,
                key=ActionKey.COMMAND,
            ),
            # Authentication
            'md5-password': Leaf(
                type=ValueType.STRING,
                description='TCP MD5 authentication password',
                target=ActionTarget.SCOPE,
                operation=ActionOperation.SET,
                key=ActionKey.COMMAND,
            ),
            'md5-base64': Leaf(
                type=ValueType.BOOLEAN,
                description='Password is base64 encoded',
                default=False,
                target=ActionTarget.SCOPE,
                operation=ActionOperation.SET,
                key=ActionKey.COMMAND,
            ),
            'md5-ip': Leaf(
                type=ValueType.IP_ADDRESS,
                description='IP address for MD5 authentication',
                target=ActionTarget.SCOPE,
                operation=ActionOperation.SET,
                key=ActionKey.COMMAND,
            ),
            # Behavior options
            'group-updates': Leaf(
                type=ValueType.BOOLEAN,
                description='Group updates for efficiency',
                default=True,
                target=ActionTarget.SCOPE,
                operation=ActionOperation.SET,
                key=ActionKey.COMMAND,
            ),
            'auto-flush': Leaf(
                type=ValueType.BOOLEAN,
                description='Auto-flush updates',
                default=True,
                target=ActionTarget.SCOPE,
                operation=ActionOperation.SET,
                key=ActionKey.COMMAND,
            ),
            'adj-rib-out': Leaf(
                type=ValueType.BOOLEAN,
                description='Maintain Adj-RIB-Out',
                default=False,
                target=ActionTarget.SCOPE,
                operation=ActionOperation.SET,
                key=ActionKey.COMMAND,
            ),
            'adj-rib-in': Leaf(
                type=ValueType.BOOLEAN,
                description='Maintain Adj-RIB-In',
                default=False,
                target=ActionTarget.SCOPE,
                operation=ActionOperation.SET,
                key=ActionKey.COMMAND,
            ),
            'manual-eor': Leaf(
                type=ValueType.BOOLEAN,
                description='Manual End-of-RIB control',
                default=False,
                target=ActionTarget.SCOPE,
                operation=ActionOperation.SET,
                key=ActionKey.COMMAND,
            ),
            'inherit': Leaf(
                type=ValueType.STRING,
                description='Inherit from template',
                target=ActionTarget.SCOPE,
                operation=ActionOperation.EXTEND,
                key=ActionKey.COMMAND,
            ),
            # Subsections
            'family': Container(description='Address families to negotiate'),
            'capability': Container(description='BGP capabilities'),
            'tcp-ao': Container(description='TCP-AO (RFC 5925) authentication'),
            'add-path': Container(description='ADD-PATH configuration'),
            'nexthop': Container(description='Next-hop encoding options'),
            'api': Container(description='External process API'),
            'static': Container(description='Static routes to announce'),
            'flow': Container(description='FlowSpec rules'),
            'l2vpn': Container(description='L2VPN/VPLS configuration'),
            'operational': Container(description='Operational messages'),
            'announce': Container(description='Route announcements'),
        },
    )

    syntax = ''

    known = {
        # Cannot migrate (return complex types or optional values):
        'inherit': inherit,  # returns list[str]
        'router-id': router_id,  # returns RouterID object
        'hold-time': hold_time,  # returns HoldTime object
        'local-address': local_address,  # returns IP|None
        'local-as': auto_asn,  # returns ASN|None
        'peer-as': auto_asn,  # returns ASN|None
        'outgoing-ttl': ttl,  # returns int|None
        'incoming-ttl': ttl,  # returns int|None
        'md5-base64': auto_boolean,  # returns bool|None
        # Migrated to schema validators:
        # description, host-name, domain-name, source-interface, md5-password,
        # passive, listen, connect, group-updates, auto-flush, adj-rib-out,
        # adj-rib-in, manual-eor, peer-address, md5-ip, rate-limit
    }

    # action dict removed - schema provides action enums via get_action_enums()
    # Note: 'inherit' uses EXTEND operation, specified via schema target/operation/key fields

    default = {
        'md5-base64': False,
        'passive': True,
        'group-updates': True,
        'auto-flush': True,
        'adj-rib-out': False,
        'adj-rib-in': False,
        'manual-eor': False,
    }

    name = 'neighbor'

    def __init__(self, parser: Parser, scope: Scope, error: Error) -> None:
        Section.__init__(self, parser, scope, error)
        self._neighbors: list[bytes] = []
        self.neighbors: dict[str, Neighbor] = {}

    def clear(self) -> None:
        self._neighbors = []
        self.neighbors = {}

    def pre(self) -> bool:
        return self.parse(self.name, 'peer-address')

    def _post_get_scope(self) -> dict[str, Any]:
        for inherited in self.scope.pop('inherit', []):
            data = self.scope.template('neighbor', inherited)
            self.scope.inherit(data)
        result: dict[str, Any] = self.scope.get()
        return result

    # Map config keys to Neighbor attributes (BGP policy)
    _CONFIG_TO_NEIGHBOR: dict[str, str] = {
        'description': 'description',
        'hold-time': 'hold_time',
        'rate-limit': 'rate_limit',
        'host-name': 'host_name',
        'domain-name': 'domain_name',
        'group-updates': 'group_updates',
        'auto-flush': 'auto_flush',
        'adj-rib-in': 'adj_rib_in',
        'adj-rib-out': 'adj_rib_out',
        'manual-eor': 'manual_eor',
    }

    # Map config keys to Session attributes (connection config)
    _CONFIG_TO_SESSION: dict[str, str] = {
        'router-id': 'router_id',
        'local-address': 'local_address',
        'local-link-local': 'local_link_local',
        'source-interface': 'source_interface',
        'peer-address': 'peer_address',
        'local-as': 'local_as',
        'peer-as': 'peer_as',
        'passive': 'passive',
        'listen': 'listen',
        'connect': 'connect',
        'md5-password': 'md5_password',
        'md5-base64': 'md5_base64',
        'md5-ip': 'md5_ip',
        'outgoing-ttl': 'outgoing_ttl',
        'incoming-ttl': 'incoming_ttl',
    }

    def _post_neighbor(self, local: dict[str, Any], families: list[FamilyTuple]) -> Neighbor:
        neighbor = Neighbor()

        # Set neighbor (BGP policy) attributes
        for config_key, attr_name in self._CONFIG_TO_NEIGHBOR.items():
            conf = local.get(config_key, None)
            if conf is not None:
                setattr(neighbor, attr_name, conf)

        # Set session (connection) attributes
        for config_key, attr_name in self._CONFIG_TO_SESSION.items():
            conf = local.get(config_key, None)
            if conf is not None:
                setattr(neighbor.session, attr_name, conf)

        # Handle TCP-AO section configuration
        tcp_ao = local.get('tcp-ao', {})
        if tcp_ao:
            if 'keyid' in tcp_ao:
                neighbor.session.tcp_ao_keyid = tcp_ao['keyid']
            if 'algorithm' in tcp_ao:
                neighbor.session.tcp_ao_algorithm = tcp_ao['algorithm']
            if 'password' in tcp_ao:
                neighbor.session.tcp_ao_password = tcp_ao['password']
            if 'base64' in tcp_ao:
                neighbor.session.tcp_ao_base64 = tcp_ao['base64']

        # auto_discovery is now derived from local_address being IP.NoNextHop
        # (which is the default if local-address is not set in config)
        if neighbor.session.auto_discovery:
            neighbor.session.md5_ip = None

        # Derive optional fields (router_id, md5_ip) from required ones
        neighbor.session.infer()

        # Check for missing required session fields
        missing = neighbor.session.missing()
        if missing:
            self.error.set(f'{missing} must be set')

        for family in families:
            neighbor.add_family(family)

        return neighbor

    def _post_families(self, local: dict[str, Any]) -> list[FamilyTuple]:
        families: list[FamilyTuple] = []
        for family in ParseFamily.convert:
            for pair in local.get('family', {}).get(family, []):
                families.append(pair)

        return families or NLRI.known_families()

    def _post_capa_default(self, neighbor: Neighbor, local: dict[str, Any]) -> None:
        capability = local.get('capability', {})
        cap = neighbor.capability

        # Map config keys to typed attributes
        if 'asn4' in capability:
            cap.asn4 = TriState.from_bool(capability['asn4'])
        if 'extended-message' in capability:
            cap.extended_message = TriState.from_bool(capability['extended-message'])
        if 'multi-session' in capability:
            cap.multi_session = TriState.from_bool(capability['multi-session'])
        if 'operational' in capability:
            cap.operational = TriState.from_bool(capability['operational'])
        if 'nexthop' in capability:
            cap.nexthop = TriState.from_bool(capability['nexthop'])
        if 'aigp' in capability:
            cap.aigp = TriState.from_bool(capability['aigp'])
        if 'add-path' in capability:
            cap.add_path = capability['add-path']
        if 'route-refresh' in capability:
            cap.route_refresh = 2 if capability['route-refresh'] else 0  # REFRESH.NORMAL or 0
        if 'software-version' in capability:
            cap.software_version = 'exabgp' if capability['software-version'] else None
        if 'link-local-nexthop' in capability:
            if capability['link-local-nexthop'] is not None:
                cap.link_local_nexthop = TriState.from_bool(capability['link-local-nexthop'])
        if 'link-local-prefer' in capability:
            cap.link_local_prefer = capability['link-local-prefer']
        if 'graceful-restart' in capability:
            gr = capability['graceful-restart']
            if gr is False:
                cap.graceful_restart = GracefulRestartConfig.disabled()
            elif isinstance(gr, int):
                # gr == 0 means enabled but use hold-time (inferred later)
                cap.graceful_restart = GracefulRestartConfig.with_time(gr)

    def _post_capa_addpath(self, neighbor: Neighbor, local: dict[str, Any], families: list[FamilyTuple]) -> None:
        if not neighbor.capability.add_path:
            return

        add_path = local.get('add-path', {})
        if not add_path:
            for family in families:
                neighbor.add_addpath(family)
            return

        for afi_name in ParseAddPath.convert:
            for entry in add_path.get(afi_name, []):
                pair, limit = entry

                if pair not in families:
                    pair_log = pair

                    def _log_skip(pair_arg: FamilyTuple = pair_log) -> str:
                        return 'skipping add-path family ' + str(pair_arg) + ' as it is not negotiated'

                    log.debug(_log_skip, 'configuration')
                    continue
                neighbor.add_addpath(pair)
                if limit > 0:
                    neighbor.capability.paths_limit_per_family[pair] = limit

    def _post_capa_nexthop(self, neighbor: Neighbor, local: dict[str, Any]) -> None:
        # The default is to auto-detect by the presence of the nexthop block
        # if this is manually set, then we honor it
        nexthop = local.get('nexthop', {})
        if neighbor.capability.nexthop.is_unset() and nexthop:
            neighbor.capability.nexthop = TriState.TRUE

        if not neighbor.capability.nexthop.is_enabled():
            return

        nexthops: list[tuple[AFI, SAFI, AFI]] = []
        for family in nexthop:
            nexthops.extend(nexthop[family])

        if not nexthops:
            return

        for afi, safi, nhafi in nexthops:
            if (afi, safi) not in neighbor.families():
                log.debug(
                    lazymsg('nexthop.skipped afi={afi} safi={safi} reason=not_negotiated', afi=afi, safi=safi),
                    'configuration',
                )
                continue
            if (nhafi, safi) not in neighbor.families():
                log.debug(
                    lazymsg('nexthop.skipped afi={nhafi} safi={safi} reason=not_negotiated', nhafi=nhafi, safi=safi),
                    'configuration',
                )
                continue
            neighbor.add_nexthop(afi, safi, nhafi)

    def _post_capa_rr(self, neighbor: Neighbor) -> None:
        if neighbor.capability.route_refresh:
            if not neighbor.adj_rib_out:
                log.warning(
                    lazymsg(
                        'neighbor.route_refresh.adj_rib_out peer={peer} action=auto_enabled reason=route_refresh_requires_cache',
                        peer=neighbor.session.peer_address,
                    ),
                    'configuration',
                )
                neighbor.adj_rib_out = True

    def _post_routes(self, neighbor: Neighbor, local: dict[str, Any]) -> None:
        # NOTE: this may modify change but does not matter as want to modified

        neighbor.routes = []
        for route in self.scope.pop_routes():
            # remove_self may well have side effects on route
            neighbor.routes.append(neighbor.resolve_self(route))

        # old format
        for section in ('static', 'l2vpn', 'flow'):
            routes = local.get(section, {}).get('routes', [])
            for route in routes:
                # remove_self may well have side effects on route
                neighbor.routes.append(neighbor.resolve_self(route))

        routes = local.get('routes', [])
        for route in routes:
            # remove_self may well have side effects on route
            neighbor.routes.append(neighbor.resolve_self(route))

    def _init_neighbor(self, neighbor: Neighbor, local: dict[str, Any]) -> None:
        families = neighbor.families()
        for route in neighbor.routes:
            # remove_self may well have side effects on route
            route = neighbor.resolve_self(route)
            if route.nlri.family().afi_safi() in families:
                # This add the family to neighbor.families()
                neighbor.rib.outgoing.add_to_rib_watchdog(route)

        for message in local.get('operational', {}).get('routes', []):
            if message.family().afi_safi() in families:
                if message.name == 'ASM':
                    neighbor.asm[message.family().afi_safi()] = message
                else:
                    neighbor.messages.append(message)
        self.neighbors[neighbor.name()] = neighbor

    def post(self) -> bool:
        local = self._post_get_scope()
        families = self._post_families(local)
        neighbor = self._post_neighbor(local, families)

        self._post_capa_default(neighbor, local)
        self._post_capa_addpath(neighbor, local, families)
        self._post_capa_nexthop(neighbor, local)
        self._post_capa_rr(neighbor)
        self._post_routes(neighbor, local)

        neighbor.api = ParseAPI.flatten(local.pop('api', {}))

        neighbor.infer()
        missing = neighbor.missing()
        if missing:
            return self.error.set('incomplete neighbor, missing {}'.format(missing))

        if not neighbor.session.auto_discovery:
            if neighbor.session.local_address.afi != neighbor.session.peer_address.afi:
                return self.error.set('local-address and peer-address must be of the same family')
        if neighbor.session.peer_address is IP.NoNextHop:
            return self.error.set('peer-address must be set')

        # Validate local-link-local is actually a link-local address if set
        if neighbor.session.local_link_local is not None:
            if not neighbor.session.local_link_local.is_link_local():
                return self.error.set('local-link-local must be an IPv6 link-local address (fe80::/10)')

        # peer_address is always IPRange when parsed from configuration (see parser.peer_ip)
        assert isinstance(neighbor.session.peer_address, IPRange)
        peer_range = neighbor.session.peer_address
        neighbor.range_size = peer_range.mask.size()

        if neighbor.range_size > 1 and not (neighbor.session.passive or getenv().bgp.passive):
            return self.error.set('can only use ip ranges for the peer address with passive neighbors')

        if neighbor.index() in self._neighbors:
            return self.error.set('duplicate peer definition {}'.format(neighbor.session.peer_address.top()))
        self._neighbors.append(neighbor.index())

        md5_error = neighbor.session.validate_md5()
        if md5_error:
            return self.error.set(md5_error)

        tcp_ao_error = neighbor.session.validate_tcp_ao()
        if tcp_ao_error:
            return self.error.set(tcp_ao_error)

        # check we are not trying to announce routes without the right MP announcement
        for route in neighbor.routes:
            family = route.nlri.family().afi_safi()
            if family not in families and family != (AFI.ipv4, SAFI.unicast):
                return self.error.set(
                    'Trying to announce a route of type {},{} when we are not announcing the family to our peer'.format(
                        *route.nlri.family().afi_safi()
                    ),
                )

        # create one neighbor object per family for multisession
        # NOTE: deepcopy per family is memory-intensive but required for multi-session
        if neighbor.capability.multi_session.is_enabled() and len(neighbor.families()) > 1:
            for family in neighbor.families():
                m_neighbor = deepcopy(neighbor)
                m_neighbor.make_rib()
                m_neighbor.rib.outgoing.families = {family}
                self._init_neighbor(m_neighbor, local)
        else:
            neighbor.make_rib()
            self._init_neighbor(neighbor, local)

        local.clear()
        return True
