"""command/peer.py

Dynamic peer management commands (create/delete).

Created by Thomas Mangin on 2025-11-23.
Copyright (c) 2009-2025 Exa Networks. All rights reserved.
License: 3-clause BSD. (See the COPYRIGHT file)
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

from exabgp.protocol.ip import IP
from exabgp.protocol.family import AFI, SAFI, FamilyTuple
from exabgp.bgp.neighbor import Neighbor
from exabgp.bgp.neighbor.capability import GracefulRestartConfig
from exabgp.bgp.message.open.asn import ASN
from exabgp.bgp.message.open.routerid import RouterID
from exabgp.configuration.neighbor.api import ParseAPI

if TYPE_CHECKING:
    from exabgp.reactor.api import API
    from exabgp.reactor.loop import Reactor


def register_peer() -> None:
    """Register peer management commands.

    This function is called during module initialization to ensure
    the handler functions are available.
    """
    pass


def _parse_ip(value: str) -> IP:
    """Parse IP address string into IP object."""
    try:
        return IP.from_string(value)
    except Exception as e:
        raise ValueError(f'invalid IP address {value}: {e}')


def _parse_asn(value: str) -> ASN:
    """Parse ASN string into ASN object."""
    try:
        asn_int = int(value)
        if asn_int < 0 or asn_int > 4294967295:  # Max ASN4
            raise ValueError('ASN out of range')
        return ASN(asn_int)
    except ValueError as e:
        raise ValueError(f'invalid ASN {value}: {e}')


def _parse_families(value: str) -> list[FamilyTuple]:
    """Parse family-allowed value into list of (AFI, SAFI) tuples.

    Accepts formats like:
    - ipv4-unicast
    - ipv4-unicast/ipv6-unicast
    - in-open (returns empty list, meaning negotiate in BGP OPEN)
    """
    # Special case: 'in-open' means negotiate families in BGP OPEN message
    if value == 'in-open':
        return []

    families = []
    for family_str in value.split('/'):
        family_str = family_str.strip()
        if not family_str:
            continue

        # Parse AFI-SAFI format (e.g., ipv4-unicast)
        parts = family_str.split('-')
        if len(parts) != 2:
            raise ValueError(f'invalid family format {family_str}, expected <afi>-<safi>')

        afi_str, safi_str = parts

        # Convert to AFI/SAFI objects
        # Note: from_string() typically returns undefined values rather than raising,
        # but we keep exception handling for safety against future changes
        try:
            afi = AFI.from_string(afi_str)
        except (KeyError, AttributeError):
            raise ValueError(f'unknown AFI {afi_str}')

        try:
            safi = SAFI.from_string(safi_str)
        except (KeyError, AttributeError):
            raise ValueError(f'unknown SAFI {safi_str}')

        families.append((afi, safi))

    return families


def _parse_neighbor_params(line: str) -> tuple[dict[str, Any], list[str]]:
    """Parse neighbor parameters from command line.

    API format: neighbor <ip> local-address <ip> local-as <asn> peer-as <asn> [router-id <ip>] [family-allowed <families>] [graceful-restart <seconds>] [group-updates true|false] [api <process>]...

    Line should NOT include the command word (create/delete).
    Example: "neighbor 127.0.0.1 local-address 127.0.0.1 local-as 1 peer-as 1 api peer-lifecycle"

    Returns:
        Tuple of (parameters dict, list of API process names - empty if none specified)
    """

    # Helper to parse key-value parameter
    from typing import Callable

    def parse_param(
        key: str, tokens: list[str], i: int, seen: set[str], parser: Callable[[str], Any]
    ) -> tuple[Any, int]:
        if key in seen:
            raise ValueError(f'duplicate parameter: {key}')
        if i + 1 >= len(tokens):
            raise ValueError(f'missing value for {key}')
        seen.add(key)
        return parser(tokens[i + 1]), i + 2

    tokens = line.split()
    if len(tokens) < 2:
        raise ValueError('no neighbor selector')
    # Accept both 'neighbor' (v4) and 'peer' (v6) as first token
    if tokens[0] not in ('neighbor', 'peer'):
        raise ValueError('command must start with "neighbor <ip>" or "peer <ip>"')

    params: dict[str, Any] = {}
    api_processes: list[str] = []
    seen_params: set[str] = set()

    # Parse peer IP (second token)
    params['peer-address'] = _parse_ip(tokens[1])

    # Parse remaining tokens as key-value pairs
    i = 2
    while i < len(tokens):
        key = tokens[i]

        if key == 'local-address' or key == 'local-ip':
            # Accept both 'local-address' and 'local-ip' as aliases
            params['local-address'], i = parse_param('local-address', tokens, i, seen_params, _parse_ip)
        elif key == 'local-as':
            params['local-as'], i = parse_param(key, tokens, i, seen_params, _parse_asn)
        elif key == 'peer-as':
            params['peer-as'], i = parse_param(key, tokens, i, seen_params, _parse_asn)
        elif key == 'router-id':
            params['router-id'], i = parse_param(key, tokens, i, seen_params, RouterID)
        elif key == 'family-allowed':
            params['families'], i = parse_param(key, tokens, i, seen_params, _parse_families)
        elif key == 'graceful-restart':
            params['graceful-restart'], i = parse_param(key, tokens, i, seen_params, int)
        elif key == 'group-updates':
            if key in seen_params:
                raise ValueError(f'duplicate parameter: {key}')
            if i + 1 >= len(tokens):
                raise ValueError(f'missing value for {key}')
            value = tokens[i + 1].lower()
            if value not in ('true', 'false'):
                raise ValueError(f'group-updates must be true or false, got: {value}')
            params['group-updates'] = value == 'true'
            seen_params.add(key)
            i += 2
        elif key == 'api':
            # Multiple 'api' keywords allowed for multiple processes
            if i + 1 >= len(tokens):
                raise ValueError('missing process name after "api"')
            api_processes.append(tokens[i + 1])
            i += 2
        elif key == 'create':
            # Accept and ignore trailing 'create' command (for backwards compatibility with tests)
            i += 1
        elif key == 'delete':
            # Reject 'delete' command in create context
            raise ValueError('expected "create" command')
        else:
            raise ValueError(f'unknown parameter: {key}')

    # Validate all required parameters were provided
    required = {'local-address', 'local-as', 'peer-as'}
    missing = required - seen_params
    if missing:
        raise ValueError(f'missing required parameters: {", ".join(sorted(missing))}')

    # Empty list is returned when no API processes are specified

    # Default router-id to local-address if not provided
    if 'router-id' not in params:
        params['router-id'] = RouterID(str(params['local-address']))

    return params, api_processes


def _build_neighbor(params: dict[str, Any], api_processes: list[str] | None = None) -> Neighbor:
    """Build Neighbor object from parsed parameters.

    Args:
        params: Dictionary of parsed parameters (already validated by _parse_neighbor_params)
        api_processes: List of API process names for peer lifecycle events (None or empty list if none)

    Returns:
        Configured Neighbor object

    Raises:
        ValueError: If validation fails
    """
    # Validate required parameters
    required_params = {
        'peer-address': 'peer-address',
        'local-address': 'local-ip',  # Use 'local-ip' in error messages for backwards compatibility
        'local-as': 'local-as',
        'peer-as': 'peer-as',
        'router-id': 'router-id',
    }
    for param_key, error_name in required_params.items():
        if param_key not in params:
            raise ValueError(f'missing required parameter: {error_name}')

    neighbor = Neighbor()

    # Set required fields
    neighbor.session.peer_address = params['peer-address']
    neighbor.session.local_address = params['local-address']
    neighbor.session.local_as = params['local-as']
    neighbor.session.peer_as = params['peer-as']
    neighbor.session.router_id = params['router-id']

    # Optional: families (defaults to IPv4 unicast only)
    if 'families' in params and params['families']:
        for family in params['families']:
            neighbor.add_family(family)
    else:
        # Default to IPv4 unicast only
        neighbor.add_family((AFI.ipv4, SAFI.unicast))

    # Optional: graceful-restart (defaults to False)
    if 'graceful-restart' in params:
        gr_time = params['graceful-restart']
        if gr_time:
            neighbor.capability.graceful_restart = GracefulRestartConfig.with_time(gr_time)
        else:
            neighbor.capability.graceful_restart = GracefulRestartConfig.disabled()

    # Optional: group-updates (defaults to True per Neighbor.defaults)
    if 'group-updates' in params:
        neighbor.group_updates = params['group-updates']

    # Initialize API configuration (required before setting processes)
    neighbor.api = ParseAPI.flatten({})

    # Configure API processes for dynamic peer notifications
    if api_processes:
        neighbor.api['processes'] = api_processes

    # Validate completeness
    missing = neighbor.missing()
    if missing:
        raise ValueError(f'incomplete neighbor configuration, missing: {missing}')

    # Infer additional fields (e.g., md5-ip defaults to local-address)
    neighbor.infer()

    # Create RIB for route management
    neighbor.make_rib()

    return neighbor


def neighbor_create(
    self: 'API', reactor: 'Reactor', service: str, peers: list[str], command: str, use_json: bool
) -> bool:
    """Create a new BGP neighbor dynamically at runtime.

    API format: peer create <ip> local-address <ip> local-as <asn> peer-as <asn> [router-id <ip>] [family-allowed <families>] [graceful-restart <seconds>] [group-updates true|false] [api <process>]...

    Required parameters:
        - <ip> - peer IP address
        - local-address <ip> - local IP address (mandatory, no auto-discovery)
        - local-as <asn> - local AS number
        - peer-as <asn> - peer AS number

    Optional parameters:
        - router-id <ip> (defaults to local-address)
        - family-allowed <families> (defaults to ipv4-unicast)
        - graceful-restart <seconds> (defaults to disabled)
        - group-updates true|false (defaults to true)
        - api <process> (repeat 'api' keyword for multiple processes)

    Examples (v6 API format):
        peer create 127.0.0.2 local-address 127.0.0.1 local-as 65001 peer-as 65002
        peer create 127.0.0.2 local-address 127.0.0.1 local-as 65001 peer-as 65002 router-id 2.2.2.2 api peer-lifecycle
        peer create 10.0.0.2 local-address 10.0.0.1 local-as 65001 peer-as 65002 api proc1 api proc2
    """
    try:
        # command contains params after "peer create" stripped (e.g., "127.0.0.2 local-address 127.0.0.1 ...")
        # Add "peer " prefix for _parse_neighbor_params which expects "neighbor <ip>" or "peer <ip>" format
        line = 'peer ' + command.strip()

        # Parse parameters and API processes
        params, api_processes = _parse_neighbor_params(line)

        # Build Neighbor object with API process configuration
        neighbor = _build_neighbor(params, api_processes)
    except Exception as e:
        # Log full exception for debugging
        import traceback
        import sys

        sys.stderr.write(f'Exception in neighbor_create: {e}\n')
        traceback.print_exc(file=sys.stderr)
        sys.stderr.flush()
        reactor.processes.answer_error_sync(service, f'Unexpected error: {type(e).__name__}: {e}')
        return False

    try:
        # Check if peer already exists - use name() as key (string, matches dict types)
        key = neighbor.name()
        if key in reactor._peers:
            reactor.processes.answer_error_sync(service, f'peer already exists: {neighbor.name()}')
            return False

        # Add to configuration (for reload persistence - though we'll mark as dynamic)
        reactor.configuration.neighbors[key] = neighbor

        # Create and register Peer (import here to avoid circular import)
        from exabgp.reactor.peer import Peer

        peer = Peer(neighbor, reactor)
        reactor._peers[key] = peer

        # Mark as dynamic peer (ephemeral - removed on reload)
        reactor._dynamic_peers.add(key)

        # Success response - use _answer_sync() for sync context
        reactor.processes._answer_sync(service, 'done')
        return True

    except ValueError as e:
        reactor.processes.answer_error_sync(service, f'neighbor create failed: {e}')
        return False
    except Exception as e:
        reactor.processes.answer_error_sync(service, f'neighbor create error: {e}')
        return False


def peer_delete(self: 'API', reactor: 'Reactor', service: str, peers: list[str], command: str, use_json: bool) -> bool:
    """Delete BGP peer(s) dynamically at runtime (v6 command).

    v6 format: peer delete <selector>

    Supports full peer selector syntax.

    Examples:
        peer delete 127.0.0.2                        # Delete specific peer
        peer delete *                                # Delete all peers (dangerous!)
        peer delete 127.0.0.2 local-as 1             # Delete with filter

    Note: Only dynamic peers created via 'peer create' should be deleted.
          Deleting static (configured) peers may cause issues on reload.
    """
    try:
        # peers list already parsed by dispatcher
        if not peers:
            # No matches - return error
            reactor.processes.answer_error_sync(service, 'no neighbors match the selector')
            return False

        # Delete each matched peer
        deleted_count = 0
        for peer_name in peers:
            if peer_name in reactor._peers:
                # Get peer object
                peer = reactor._peers[peer_name]

                # Stop peer (sends NOTIFICATION, graceful shutdown)
                peer.remove()

                # Remove from reactor
                del reactor._peers[peer_name]

                # Remove from configuration
                if peer_name in reactor.configuration.neighbors:
                    del reactor.configuration.neighbors[peer_name]

                # Remove from dynamic peers tracking
                if peer_name in reactor._dynamic_peers:
                    reactor._dynamic_peers.remove(peer_name)

                deleted_count += 1

        # Success response
        reactor.processes.answer_done_sync(service)
        return True

    except ValueError as e:
        reactor.processes.answer_error_sync(service, f'neighbor delete failed: {e}')
        return False
    except Exception as e:
        reactor.processes.answer_error_sync(service, f'neighbor delete error: {e}')
        return False
