# Copyright 2016 Mirantis, Inc.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

import re

from oslo_log import log as logging

from networking_generic_switch._i18n import _
from networking_generic_switch.devices import netmiko_devices
from networking_generic_switch.devices import utils as device_utils
from networking_generic_switch import exceptions as exc

LOG = logging.getLogger(__name__)


class AristaEos(netmiko_devices.NetmikoSwitch):
    """Device Name: Arista EOS

    Port can be disabled: True

    VXLAN L2VNI Support
    ~~~~~~~~~~~~~~~~~~~

    For VXLAN L2VNI support, the ``ngs_bgp_asn`` configuration parameter is
    required. The ``ngs_vxlan_interface`` parameter can optionally be specified
    (defaults to ``Vxlan1``). The ``ngs_evpn_route_target`` parameter can
    optionally be specified to configure the route-target value. When set to
    ``auto`` (the default), the commands ``route-target export auto <ASN>``
    and ``route-target import auto <ASN>`` are used, which allows Arista
    EOS to automatically derive route-target values. Alternatively, an
    explicit value can be provided in the format ``<ASN>:<number>``
    (e.g., ``65000:100``).

    Supports two BUM (Broadcast, Unknown unicast, Multicast) traffic
    replication modes:

    1. **ingress-replication** (default) - Uses BGP EVPN for BUM traffic
    2. **multicast** - Uses ASM multicast groups with PIM Sparse Mode

    .. code-block:: ini

        [genericswitch:arista-switch]
        device_type = netmiko_arista_eos
        ngs_bgp_asn = 65000
        ngs_vxlan_interface = Vxlan1
        ngs_evpn_route_target = auto
        ngs_bum_replication_mode = ingress-replication
        ngs_mcast_group_base = 239.1.1.0
    """

    ADD_NETWORK = (
        'vlan {segmentation_id}',
        'name {network_name}',
    )

    DELETE_NETWORK = (
        'no vlan {segmentation_id}',
    )

    PLUG_SWITCH_TO_NETWORK = (
        'interface {vxlan_interface}',
        'vxlan vlan {segmentation_id} vni {vni}',
    )

    UNPLUG_SWITCH_FROM_NETWORK = (
        'interface {vxlan_interface}',
        'no vxlan vlan {segmentation_id} vni {vni}',
    )

    SHOW_VLAN_PORTS = ('show vlan id {segmentation_id} configured-ports',)

    SHOW_VLAN_VNI = ('show interfaces {vxlan_interface}',)

    PLUG_PORT_TO_NETWORK = (
        'interface {port}',
        'switchport mode access',
        'switchport access vlan {segmentation_id}',
    )

    DELETE_PORT = (
        'interface {port}',
        'no switchport access vlan {segmentation_id}',
        'no switchport mode trunk',
        'switchport trunk allowed vlan none'
    )

    SET_NATIVE_VLAN = (
        'interface {port}',
        'switchport mode trunk',
        'switchport trunk native vlan {segmentation_id}',
        'switchport trunk allowed vlan add {segmentation_id}'
    )

    SET_NATIVE_VLAN_BOND = (
        'interface {bond}',
        'switchport mode trunk',
        'switchport trunk native vlan {segmentation_id}',
        'switchport trunk allowed vlan add {segmentation_id}'
    )

    DELETE_NATIVE_VLAN = (
        'interface {port}',
        'no switchport trunk native vlan {segmentation_id}',
        'switchport trunk allowed vlan remove {segmentation_id}',
    )

    DELETE_NATIVE_VLAN_BOND = (
        'interface {bond}',
        'no switchport trunk native vlan {segmentation_id}',
        'switchport trunk allowed vlan remove {segmentation_id}',
    )

    ADD_NETWORK_TO_TRUNK = (
        'interface {port}',
        'switchport trunk allowed vlan add {segmentation_id}'
    )

    ADD_NETWORK_TO_BOND_TRUNK = (
        'interface {bond}',
        'switchport trunk allowed vlan add {segmentation_id}'
    )

    REMOVE_NETWORK_FROM_TRUNK = (
        'interface {port}',
        'switchport trunk allowed vlan remove {segmentation_id}'
    )

    DELETE_NETWORK_ON_BOND_TRUNK = (
        'interface {bond}',
        'switchport trunk allowed vlan remove {segmentation_id}'
    )

    ERROR_MSG_PATTERNS = (
        re.compile(r'% Invalid input'),
        re.compile(r'% Incomplete command'),
        re.compile(r'% VLAN \d+ is already mapped to VNI \d+'),
    )

    ENABLE_PORT = (
        'interface {port}',
        'no shutdown',
    )

    DISABLE_PORT = (
        'interface {port}',
        'shutdown',
    )

    ENSURE_VLAN = (
        'vlan {segmentation_id}',
        'exit',
    )

    PLUG_EVPN_VLAN = (
        'router bgp {bgp_asn}',
        'vlan {segmentation_id}',
        'rd auto',
        'redistribute learned',
    )

    PLUG_EVPN_ROUTE_TARGET_AUTO = (
        'route-target export auto {bgp_asn}',
        'route-target import auto {bgp_asn}',
    )

    PLUG_EVPN_ROUTE_TARGET_EXPLICIT = (
        'route-target both {evpn_route_target}',
    )

    PLUG_MCAST_FLOOD_VTEP = (
        'interface {vxlan_interface}',
        'vxlan vlan {segmentation_id} flood vtep {mcast_group}',
    )

    UNPLUG_MCAST_FLOOD_VTEP = (
        'interface {vxlan_interface}',
        'no vxlan vlan {segmentation_id} flood vtep',
    )

    UNPLUG_EVPN_VLAN = (
        'router bgp {bgp_asn}',
        'no vlan {segmentation_id}',
    )

    def __init__(self, device_cfg, *args, **kwargs):
        """Initialize Arista EOS device with VXLAN configuration support.

        Extracts VXLAN-related configuration before parent __init__ removes
        all ngs_* options.
        """
        # Extract VXLAN config before parent removes ngs_* options
        # Support both ngs_vxlan_interface (new) and deprecated
        self.vxlan_interface = device_cfg.get('ngs_vxlan_interface')
        if self.vxlan_interface is None:
            # Fallback to deprecated parameter for backward compatibility
            self.vxlan_interface = device_cfg.pop('vxlan_interface',
                                                  'Vxlan1')
            if self.vxlan_interface != 'Vxlan1':
                LOG.warning(
                    "Configuration parameter 'vxlan_interface' is "
                    "deprecated and will be removed in a future release. "
                    "Please use 'ngs_vxlan_interface' instead.")

        self.bgp_asn = device_cfg.get('ngs_bgp_asn')
        self.evpn_route_target = device_cfg.get('ngs_evpn_route_target',
                                                'auto')

        # Use shared utility for multicast config parsing
        mcast_config = device_utils.parse_vxlan_multicast_config(device_cfg)
        self.bum_replication_mode = mcast_config.bum_replication_mode
        self.mcast_group_base = mcast_config.mcast_group_base
        self.mcast_group_increment = mcast_config.mcast_group_increment
        self.mcast_group_map = mcast_config.mcast_group_map

        super(AristaEos, self).__init__(device_cfg, *args, **kwargs)

    def _get_multicast_group(self, vni: int) -> str:
        """Calculate multicast group address for a given VNI.

        Delegates to shared utility function for consistent multicast
        group derivation logic across all vendors.

        :param vni: VXLAN Network Identifier
        :returns: Multicast group IP address (e.g., '239.1.1.100')
        :raises: GenericSwitchConfigError if no mapping or base configured
        """
        return device_utils.get_vxlan_multicast_group(
            vni, self.mcast_group_map, self.mcast_group_base,
            self.device_name)

    def _parse_vlan_ports(self, output: str, segmentation_id: int) -> bool:
        """Parse Arista EOS 'show vlan id X configured-ports' output for ports.

        Uses 'configured-ports' to check for all ports with VLAN configuration
        regardless of operational state. This ensures proper cleanup decisions
        even when ports are administratively disabled.

        Filters out VXLAN interfaces (Vx1, Vxlan1, etc.) which appear in the
        Ports column but are not physical/logical ports that should prevent
        VLAN-to-VNI mapping cleanup.

        :param output: Command output from switch
        :param segmentation_id: VLAN identifier being checked
        :returns: True if VLAN has physical/logical ports, False otherwise
        """
        # Arista EOS output format:
        # VLAN  Name                             Status    Ports
        # ----- -------------------------------- --------- ------
        # 100   VLAN0100                         active    Et1, Et2
        # 105   VLAN0105                         active    Vx1
        lines = output.strip().split('\n')
        for line in lines:
            # Skip empty lines and separator lines
            if not line.strip() or '---' in line:
                continue
            # Skip header line (starts with "VLAN " - note the space)
            if line.startswith('VLAN '):
                continue
            parts = line.split()
            # First part should be VLAN ID
            if (parts and parts[0].isdigit()
                    and int(parts[0]) == segmentation_id):
                # Check if there are port entries (index 3 onwards)
                if len(parts) <= 3:
                    # No ports at all
                    return False
                # Extract port names from index 3 onwards
                # Ports are comma-separated: "Et1, Et2, Vx1"
                ports_str = ' '.join(parts[3:])
                # Split by comma and strip whitespace then filter out VXLAN
                # interfaces (Vx*)
                physical_ports = [
                    p for p in [x.strip() for x in ports_str.split(',')]
                    if not p.lower().startswith('vx')]
                return len(physical_ports) > 0
        # If no data row found, conservatively assume no ports
        return False

    def _parse_vlan_vni(self, output: str, segmentation_id: int,
                        vni: int) -> bool:
        """Parse Arista EOS 'show interfaces VxlanX' output.

        Detects VNI configuration in both ingress-replication and multicast
        modes.

        :param output: Command output from switch
        :param segmentation_id: VLAN identifier being checked
        :param vni: VNI to check for
        :returns: True if VLAN has this VNI, False otherwise
        """
        # Arista EOS output format:
        # Static VLAN to VNI mapping is [112, 112] [134, 134]
        # VLAN 100 flood VTEP 239.1.1.116 (multicast mode)
        # Format: [VLAN, VNI] pairs in brackets
        lines = output.strip().split('\n')
        for line in lines:
            if 'Static VLAN to VNI mapping' in line:
                # Extract all [VLAN, VNI] pairs from the line
                pairs = re.findall(r'\[(\d+),\s*(\d+)\]', line)
                for vlan_str, vni_str in pairs:
                    if int(vlan_str) == segmentation_id:
                        return int(vni_str) == vni
        # If we can't find the VNI, it's not configured
        return False

    @netmiko_devices.check_output('plug vni')
    def plug_switch_to_network(self, vni: int, segmentation_id: int,
                               physnet: str = None):
        """Configure L2VNI mapping with BGP EVPN on Arista EOS.

        Supports two BUM traffic replication modes:
        1. ingress-replication (default): Uses BGP EVPN for BUM
           replication. Recommended for most deployments.
        2. multicast: Uses ASM multicast groups for BUM replication.
           Requires PIM Sparse Mode with Anycast RP configured on the
           fabric infrastructure.

        Configures the following in order:
        0. Create VLAN (must exist before VXLAN mapping)
        1. Configure EVPN VLAN (BGP control plane for MAC/IP learning)
        2. Map VLAN to VNI on the VXLAN interface
        3. Configure BUM replication (ingress-replication or flood vtep)

        :param vni: VXLAN Network Identifier
        :param segmentation_id: VLAN identifier
        :param physnet: Physical network name (unused, kept for
                        compatibility)
        :returns: Command output
        """
        if not self.bgp_asn:
            LOG.error(
                _("ngs_bgp_asn configuration parameter is required for L2VNI "
                  "support on Arista EOS switches. Device: %(device)s"),
                {'device': device_utils.sanitise_config(self.config)})
            raise exc.GenericSwitchNetmikoConfigError()

        cmds = []

        # Step 0: Ensure VLAN exists
        cmds.extend(self._format_commands(
            self.ENSURE_VLAN,
            segmentation_id=segmentation_id))

        # Step 1: EVPN VLAN configuration (BGP control plane)
        # NOTE: EVPN used for MAC/IP learning in both modes
        cmds.extend(self._format_commands(
            self.PLUG_EVPN_VLAN,
            bgp_asn=self.bgp_asn,
            segmentation_id=segmentation_id))

        if self.evpn_route_target == 'auto':
            cmds.extend(self._format_commands(
                self.PLUG_EVPN_ROUTE_TARGET_AUTO,
                bgp_asn=self.bgp_asn))
        else:
            cmds.extend(self._format_commands(
                self.PLUG_EVPN_ROUTE_TARGET_EXPLICIT,
                evpn_route_target=self.evpn_route_target))

        # Step 2: Map VLAN to VNI
        cmds.extend(self._format_commands(
            self.PLUG_SWITCH_TO_NETWORK,
            vni=vni,
            segmentation_id=segmentation_id,
            vxlan_interface=self.vxlan_interface))

        # Step 3: BUM traffic replication configuration
        if self.bum_replication_mode == 'multicast':
            mcast_group = self._get_multicast_group(vni)
            cmds.extend(self._format_commands(
                self.PLUG_MCAST_FLOOD_VTEP,
                vxlan_interface=self.vxlan_interface,
                segmentation_id=segmentation_id,
                mcast_group=mcast_group))

        return self.send_commands_to_device(cmds)

    @netmiko_devices.check_output('unplug vni')
    def unplug_switch_from_network(self, vni: int, segmentation_id: int,
                                   physnet: str = None):
        """Remove L2VNI mapping and EVPN VLAN from Arista EOS.

        Removes configuration in reverse order of creation:
        1. Remove multicast flood vtep (if in multicast mode)
        2. Remove VXLAN map
        3. Remove EVPN VLAN configuration

        :param vni: VXLAN Network Identifier
        :param segmentation_id: VLAN identifier
        :param physnet: Physical network name (unused but kept for signature)
        :returns: Command output
        """
        if not self.bgp_asn:
            LOG.error(
                _("ngs_bgp_asn configuration parameter is required for L2VNI "
                  "support on Arista EOS switches. Device: %(device)s"),
                {'device': device_utils.sanitise_config(self.config)})
            raise exc.GenericSwitchNetmikoConfigError()

        cmds = []

        # Step 1: Remove multicast flood vtep (if in multicast mode)
        if self.bum_replication_mode == 'multicast':
            cmds.extend(self._format_commands(
                self.UNPLUG_MCAST_FLOOD_VTEP,
                vxlan_interface=self.vxlan_interface,
                segmentation_id=segmentation_id))

        # Step 2: Remove VXLAN map
        cmds.extend(self._format_commands(
            self.UNPLUG_SWITCH_FROM_NETWORK,
            vni=vni,
            segmentation_id=segmentation_id,
            vxlan_interface=self.vxlan_interface))

        # Step 3: Remove EVPN VLAN from BGP
        cmds.extend(self._format_commands(
            self.UNPLUG_EVPN_VLAN,
            bgp_asn=self.bgp_asn,
            segmentation_id=segmentation_id))

        return self.send_commands_to_device(cmds)

    def vlan_has_ports(self, segmentation_id: int) -> bool:
        """Check if a VLAN has any switch ports currently assigned.

        :param segmentation_id: VLAN identifier
        :returns: True if VLAN has ports, False otherwise
        """
        cmd = self._format_commands(
            self.SHOW_VLAN_PORTS,
            segmentation_id=segmentation_id,
            vxlan_interface=self.vxlan_interface)
        with self._get_connection() as net_connect:
            output = net_connect.send_command(cmd[0])
            return self._parse_vlan_ports(output, segmentation_id)

    def vlan_has_vni(self, segmentation_id: int, vni: int) -> bool:
        """Check if a VLAN already has a specific VNI mapping configured.

        :param segmentation_id: VLAN identifier
        :param vni: VNI to check for
        :returns: True if VLAN has this VNI, False otherwise
        """
        cmd = self._format_commands(
            self.SHOW_VLAN_VNI,
            segmentation_id=segmentation_id,
            vni=vni,
            vxlan_interface=self.vxlan_interface)
        with self._get_connection() as net_connect:
            output = net_connect.send_command(cmd[0])
            return self._parse_vlan_vni(output, segmentation_id, vni)
