package ru.bitel.bgbilling.modules.inet.dyn.device.cisco;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import ru.bitel.bgbilling.apps.inet.access.InetConnectionManager;
import ru.bitel.bgbilling.apps.inet.access.sa.ServiceActivator;
import ru.bitel.bgbilling.apps.inet.access.sa.ServiceActivatorEvent;
import ru.bitel.bgbilling.common.model.ConfigParameter;
import ru.bitel.bgbilling.kernel.event.EventProcessor;
import ru.bitel.bgbilling.kernel.network.radius.RadiusAttribute;
import ru.bitel.bgbilling.kernel.network.radius.RadiusAttributeSet;
import ru.bitel.bgbilling.kernel.network.radius.RadiusDictionary;
import ru.bitel.bgbilling.kernel.network.radius.RadiusClient.ResponseFuture;
import ru.bitel.bgbilling.kernel.network.radius.RadiusPacket;
import ru.bitel.bgbilling.modules.inet.common.bean.InetConnection;
import ru.bitel.bgbilling.modules.inet.common.bean.InetDevice;
import ru.bitel.bgbilling.modules.inet.common.bean.InetDeviceType;
import ru.bitel.bgbilling.modules.inet.common.bean.enums.InetServState;
import ru.bitel.bgbilling.modules.inet.dyn.device.radius.AbstractRadiusServiceActivator;
import ru.bitel.bgbilling.server.util.Setup;
import ru.bitel.common.ParameterMap;
import ru.bitel.common.Utils;

public class ISGServiceActivator
    extends AbstractRadiusServiceActivator
    implements ServiceActivator
{
	private static final Logger logger = LogManager.getLogger();

	/**
	 * Ключ - код опции, значение - сервис
	 */
	protected Map<Integer, String> inetOptionIsgService = new HashMap<>();

	/**
	 * Имя(имена) сервиса, при котором доступ отключен.
	 */
	protected Set<String> disableServiceNames;

    /**
     * Имя(имена) сервиса, при котором доступ подключен.
     */
    protected Set<String> enableServiceNames;

	/**
	 * Отправка в атрибуте cisco-SSG-Command-Code в первом пакете отключение необходимых сервисов, во втором - включение необходимыъ сервисов.
	 * Для старших cisco.
	 */
	protected static final int COA_MODE_SSG_COMMAND_PACKET = 0;

	/**
	 * Отправка в атрибуте cisco-SSG-Command-Code каждая команда отдельным пакетом.
	 */
	protected static final int COA_MODE_SSG_COMMAND = 1;

	/**
	 * Отправка в атрибуте cisco-avpair="subscriber:command=deactivate-service", каждая команда отдельным пакетом.
	 */
	protected static final int COA_MODE_SUBSCR_COMMAND = 2;
	
	/**
	 * Отправка в атрибуте cisco-SSG-Command-Code в одном пакете отключение и включение необходимых сервисов.
	 * Для старших cisco с subscriber service multiple-accept.
	 */
	protected static final int COA_MODE_SSG_COMMAND_ONE_PACKET = 3;

	/**
	 * Режим отправки команд
	 */
	protected int coaMode;

	@Deprecated
	protected static final int CLOSE_MODE_POD_DEPRECATED = 0;

	/**
	 * Ничего не делать для закрытия соединения.
	 * @see #disableServicesOnClose
	 */
	protected static final int CLOSE_MODE_NONE = 1;

	/**
	 * Посылать PoD пакет для закрытия соединения.
	 * @see #disableServicesOnClose
	 */
	protected static final int CLOSE_MODE_POD = 2;

	/**
	 * Посылать subscriber:command=account-logoff для закрытия соединения.
	 * @see #disableServicesOnClose
	 */
	protected static final int CLOSE_MODE_SUBSCR_COMMAND = 3;

	/**
	 * Режим закрытия соединения.
	 * @see #CLOSE_MODE_NONE
	 * @see #CLOSE_MODE_POD
	 * @see #CLOSE_MODE_SUBSCR_COMMAND
	 */
	protected int closeMode;

	/**
	 * Режим закрытия соединения для переключения из отключен во включен (если {@link #withoutBreak}=false).
	 * @see #CLOSE_MODE_NONE
	 * @see #CLOSE_MODE_POD
	 * @see #CLOSE_MODE_SUBSCR_COMMAND
	 */
	protected int closeEnableMode;

	/**
	 * Нужно ли посылать CoA при переводе из disable в enable (при withoutBreak=false)
	 */
	protected boolean coaOnEnable;

	/**
	 * Нужно ли закрывать сервисы при закрытии сессии.
	 */
	protected boolean disableServicesOnClose;

	/**
	 * Нужно ли удалять из keymap вторичной авторизации. keymap используется в InetDhcpHelperProcessor и InetRadiusHelperProcessor.
	 * При удалении из keymap InetDhcpHelperProcessor начнет выдавать NAK, InetRadiusHelperProcessor перестанет авторизовать.
	 * Для работы с ISG+DHCP должен быть true.
	 */
	protected boolean closeRemoveFromKeyMap;
	
	/**
	 * При отключении/включении сервисов, если пришел NAK - все равно менять текущий набор опций - deviceOptions.
	 */
	protected boolean forceModifyOnServiceNak;

	public ISGServiceActivator()
	{
		super( null, false, "Acct-Session-Id", false );
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public Object init( Setup setup, int moduleId, InetDevice device, InetDeviceType deviceType, ParameterMap deviceConfig )
	    throws Exception
	{
		super.init( setup, moduleId, device, deviceType, deviceConfig );

		this.coaMode = deviceConfig.getInt( SA_RADIUS_CONNECTION_COA_MODE, deviceConfig.getInt( "radius.coa.mode", deviceConfig.getInt( "coa.mode", COA_MODE_SSG_COMMAND ) ) );

		parseOptionsIsgServices();

		// сервис(ы), отправляемый в режиме Reject-To-Accept
		List<String> disableServiceNames = Utils.toList( deviceConfig.get( SA_RADIUS_SERVICE_DISABLE, deviceConfig.get( "radius.serviceName.disable", "" ) ) );// INET_FAKE
		if ( disableServiceNames.size() > 0 )
		{
			this.disableServiceNames = Collections.newSetFromMap( new LinkedHashMap<>() );
			this.disableServiceNames.addAll( disableServiceNames );
		}
		else
		{
			this.disableServiceNames = null;
		}

		logger.info( "Disable services: " + disableServiceNames );

        List<String> enableServiceNames = Utils.toList( deviceConfig.get( SA_RADIUS_SERVICE_ENABLE ) );
        if ( enableServiceNames.size() > 0 )
        {
            this.enableServiceNames = Collections.newSetFromMap( new LinkedHashMap<>() );
            this.enableServiceNames.addAll( enableServiceNames );
        }
        else
        {
            this.enableServiceNames = null;
        }

        logger.info( "Enable services: " + enableServiceNames );

		this.coaOnEnable = deviceConfig.getInt( SA_RADIUS_CONNECTION_COA_ON_ENABLE, 0 ) > 0;

		this.closeMode = deviceConfig.getInt( SA_RADIUS_CONNECTION_CLOSE_MODE, CLOSE_MODE_NONE );
		this.closeEnableMode = deviceConfig.getInt( SA_RADIUS_CONNECTION_CLOSE_ENABLE_MODE, CLOSE_MODE_NONE );

		// при отключечнии соединения по умолчанию убирать из keymap (т.е. начинать посылать NAK на DHCP запросы)
		this.closeRemoveFromKeyMap = deviceConfig.getInt( SA_RADIUS_CONNECTION_CLOSE_REMOVE_FROM_KEY_MAP, 1 ) > 0;

		// при отключении соединения по умолчанию отключать сервисы
		this.disableServicesOnClose = deviceConfig.getInt( SA_RADIUS_CONNECTION_CLOSE_DISABLE_SERVICES, 1 ) > 0;
		
		this.forceModifyOnServiceNak = deviceConfig.getInt( SA_RADIUS_CONNECTION_MODIFY_FORCE_ON_SERVICE_NAK, 0 ) > 0;

		return null;
	}

    private void parseOptionsIsgServices()
    {
        // вычленение из ISG атрибутов соответствия опций модуля опциям ISG
		Map<Integer, RadiusAttributeSet> optionRadiusAttributesMap = this.optionRadiusAttributesMap.getRealmMap().get( "default" );
		// определение сервисов на каждой из опций
		for ( Map.Entry<Integer, RadiusAttributeSet> e : optionRadiusAttributesMap.entrySet() )
		{
			List<RadiusAttribute<?>> list = e.getValue().getAttributes( 9, 250 );
			if ( list == null || list.size() == 0 )
			{
				continue;
			}

			int option = e.getKey();

			for ( RadiusAttribute<?> ra : list )
			{
				String value = String.valueOf( ra.getValue() );
				if ( !value.startsWith( "A" ) )
				{
					continue;
				}

				String isgService = value.substring( 1 );
				inetOptionIsgService.put( option, isgService );

				logger.info( "Inet option: " + option + " => ISG service: " + isgService );
			}
		}
    }

	/**
	 * {@inheritDoc}
	 */
	@Override
	public Object connectionModify( ServiceActivatorEvent e )
	    throws Exception
	{
        logger.info( "Connection modify: oldState: " + e.getOldState() + "; newState: " + e.getNewState() + "; oldOptionSet: " + e.getOldOptions() + "; newOptionSet: " + e.getNewOptions() );

        final InetConnection connection = e.getConnection();

        if ( connectionRadiusAttributes.contains( "Acct-Session-Id" ) && connection.getAcctSessionId() == null )
        {
            logger.info( "AcctSession is null - flow session, skipping." );
            return null;
        }

        // если нужно переключить в состояние отключен
		if ( e.getNewState() == InetServState.STATE_DISABLE.getCode() )
		{
			// если всегда сбрасываем соединение при переключении состояния - вызываем просто connectionClose
			// например, чтобы поменять IP с белого на серый
			// sa.radius.connection.withoutBreak=0
			if ( !withoutBreak )
			{
				return connectionClose( e );
			}

			// устанавливаем флаг, что нужно будет поменять состояние соединения в базе
			// sa.radius.connection.stateModify=1
			if ( needConnectionStateModify )
			{
				e.setConnectionStateModified( true );
			}

			// отключаем соединение
			return sendCommands( connection, optionsToServiceNames( e.getOldOptions(), enableServiceNames ), disableServiceNames );
		}

		// если раньше было состояние отключено, а теперь включено
		if ( e.getOldState() == InetServState.STATE_DISABLE.getCode() )
		{
			// если всегда сбрасываем соединение при переключении состояния - вызываем просто connectionClose
			// например, чтобы поменять IP с белого на серый
			// sa.radius.connection.withoutBreak=0
			if ( !withoutBreak )
			{
				if ( closeRemoveFromKeyMap )
				{
					// убрать из DHCP, чтобы выдало NaK
					logger.debug( "Remove connection from key map." );
					EventProcessor.getInstance().request( new InetConnectionManager.ConnectionRemoveEvent( connection ) );
				}

				Object result = connectionClose( connection, closeEnableMode, null );

				// если при необходимости включить доступ до того как поменяли IP мы хотим включить доступ на SE, то идем дальше
				// иначе return
				if ( !coaOnEnable )
				{
					return result;
				}
			}

			// устанавливаем флаг, что нужно будет поменять состояние соединения в базе
			// sa.radius.connection.stateModify=1
			if ( needConnectionStateModify )
			{
				e.setConnectionStateModified( true );
			}

            // отключаем disable сервис и включаем активные опции
            Set<String> serviceNamesActivate = optionsToServiceNames( e.getNewOptions(), null );
            // есть ли включение сервисов в опциях
            boolean optionServiceNames = serviceNamesActivate != null && serviceNamesActivate.size() > 0;

            // если указан сервис enable - включаем и его
            if ( enableServiceNames != null )
            {
                serviceNamesActivate.addAll( enableServiceNames );
            }

            final Future<Boolean> result = sendCommands( connection, disableServiceNames, serviceNamesActivate );

            // если есть включение/выключение сервисов-опций, то прекращаем работу
            if ( optionServiceNames )
            {
				return result;
			}
			// если нет включения сервисов в опция, то похоже сервисы только для DISABLE, а для обычной работы - обычные атрибуты
			else
			{
				return sendCoa( e, connection, result );
			}
		}

		// отключаем/включаем сервисы-опции
		Collection<Integer> removeOptions = e.getOptionsToRemove();
		Collection<Integer> addOptions = e.getOptionsToAdd();
		
		Collection<String> serviceNamesActivate = optionsToServiceNames( addOptions, null );

		Future<Boolean> result = sendCommands( connection, optionsToServiceNames( removeOptions, null ), serviceNamesActivate );
		
		// если есть включение/выключение сервисов, то всё хорошо
		if ( serviceNamesActivate != null && serviceNamesActivate.size() > 0 )
		{
			return result;
		}
		// если нет включения сервисов, то похоже сервисы только для DISABLE, а для обычной работы - обычные атрибуты
		else
		{
			return sendCoa( e, connection, result );
		}
	}
	
	private Future<Boolean> sendCoa( final ServiceActivatorEvent e, final InetConnection connection, Future<Boolean> result )
		throws Exception
	{
		RadiusPacket request = radiusClient.createModifyRequest();
		prepareRequest( request, connection );

		final String realm = e.getRealm();
		
		boolean simpleCoA = false;

		// для активных опций модуля inet просто добавляем привязанные атрибуты
		// (подразумевается что параметры будут просто переопределены/перетерты)
		for ( Integer option : e.getNewOptions() )
		{
			RadiusAttributeSet set = optionRadiusAttributesMap.get( realm, option );
			if ( set != null && !set.isEmpty() )
			{
				simpleCoA = true;
				request.addAttributes( set );
			}
		}

		if ( simpleCoA )
		{
			if ( result != null )
			{
				result.get( 500, TimeUnit.MILLISECONDS );
			}

			logger.info( "Send CoA: \n" + request );

			return radiusClient.sendAsync( request );
		}
		else
		{
			return result;
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public Object connectionClose( ServiceActivatorEvent e )
	    throws Exception
	{
	    logger.info( "Connection close" );

        final InetConnection connection = e.getConnection();

        if ( connectionRadiusAttributes.contains( "Acct-Session-Id" ) && connection.getAcctSessionId() == null )
        {
            logger.info( "AcctSessionId is null - flow session, skipping." );
            return null;
        }

		Object result;

		if ( closeRemoveFromKeyMap )
		{
			// убрать из DHCP, чтобы выдало NaK
			logger.debug( "Remove connection from key map." );
			EventProcessor.getInstance().request( new InetConnectionManager.ConnectionRemoveEvent( connection ) );
		}

		if ( disableServicesOnClose )
		{
			result = sendCommands( connection, optionsToServiceNames( e.getOldOptions(), null ), disableServiceNames );
		}
		else
		{
			result = null;
		}

		return connectionClose( connection, closeMode, result );
	}

	/**
	 * Закрытие соединения по указанному режиму.
	 * @see #CLOSE_MODE_NONE
	 * @see #CLOSE_MODE_POD
	 * @see #CLOSE_MODE_SUBSCR_COMMAND
	 * @param connection
	 * @param closeMode
	 * @param result
	 * @return
	 * @throws Exception
	 */
	protected Object connectionClose( final InetConnection connection, final int closeMode, Object result )
	    throws Exception
	{
		logger.info( "Connection close mode " + closeMode );

		switch( closeMode )
		{
			default:
			case CLOSE_MODE_NONE:
			{
				break;
			}

			// отправляем PoD-пакет
			case CLOSE_MODE_POD_DEPRECATED:
			case CLOSE_MODE_POD:
			{
				RadiusPacket request = radiusClient.createDisconnectRequest();
				prepareRequest( request, connection );

				logger.info( "Send PoD: \n" + request );
				result = radiusClient.sendAsync( request );

				break;
			}

			case CLOSE_MODE_SUBSCR_COMMAND:
			{
				logger.info( "Connection close (logoff)" );

				RadiusPacket packet = radiusClient.createModifyRequest();
				prepareRequest( packet, connection );

				packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 1, "subscriber:command=account-logoff" ) );

				logger.info( "Send logoff CoA:\n" + packet );
				result = radiusClient.sendAsync( packet );

				break;
			}
		}

		return result;
	}
	
	/**
	 * Преобразование кодов опций в названия сервисов ISG.
	 * @param options
	 * @param serviceNames
	 * @return
	 */
	protected Set<String> optionsToServiceNames( final Collection<Integer> options, final Collection<String> serviceNames )
	{
        if ( options == null || options.size() == 0 )
        {
            if ( serviceNames == null )
            {
                logger.info( "optionsToServiceNames: options and serviceNames are empty" );
                return null;
            }

            return new HashSet<>( serviceNames );
        }

		final Set<String> result = Collections.newSetFromMap( new LinkedHashMap<>( options.size() + 2 ) );
		if ( serviceNames != null )
		{
			result.addAll( serviceNames );
		}

		for ( Integer option : options )
		{
			String serviceName = inetOptionIsgService.get( option );
			if ( serviceName == null )
			{
				logger.info( "Not found ISG service for Inet option: " + option );
				continue;
			}

			result.add( serviceName );
		}

		return result;
	}

	/**
	 * Отправка команд на деактивацию и активацию сервисов
	 * @param connection
	 * @param serviceNamesDeactivate - список сервисов, которые нужно деактивировать
	 * @param serviceNamesActivate - список сервисов, которые нужно активировать
	 * @return
	 * @throws Exception
	 */
	protected Future<Boolean> sendCommands( final InetConnection connection, final Collection<String> serviceNamesDeactivate, final Collection<String> serviceNamesActivate )
	    throws Exception
	{
		Future<Boolean> result = null;

		switch( coaMode )
		{
			case COA_MODE_SSG_COMMAND_ONE_PACKET:
			{
                result = sendBinaryCommandsInOnePacket( connection, serviceNamesDeactivate, serviceNamesActivate, result );
                break;
			}
			
			case COA_MODE_SSG_COMMAND_PACKET:
			{
				if ( serviceNamesDeactivate != null && serviceNamesDeactivate.size() > 0 )
				{
					RadiusPacket packet = radiusClient.createModifyRequest();
					prepareRequest( packet, connection );

					for ( String serviceName : serviceNamesDeactivate )
					{
						String value = "\\0xc" + serviceName;
						// добавление cisco-SSG-Command-Code
						packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 252, value ) );
					}

                    logger.info( "Send deactivate services CoA" );
					result = radiusClient.sendAsync( packet );
				}

				if ( serviceNamesActivate != null && serviceNamesActivate.size() > 0 )
				{
					RadiusPacket packet = radiusClient.createModifyRequest();
					prepareRequest( packet, connection );

					for ( String serviceName : serviceNamesActivate )
					{
						String value = "\\0xb" + serviceName;
						// добавление cisco-SSG-Command-Code
						packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 252, value ) );
					}

                    logger.info( "Send activate services CoA" );
					result = radiusClient.sendAsync( packet );
				}

				break;
			}

			case COA_MODE_SSG_COMMAND:
			{
				if ( serviceNamesDeactivate != null && serviceNamesDeactivate.size() > 0 )
				{
					for ( String serviceName : serviceNamesDeactivate )
					{
						RadiusPacket packet = radiusClient.createModifyRequest();
						prepareRequest( packet, connection );

						String value = "\\0xc" + serviceName;
						// добавление cisco-SSG-Command-Code
						packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 252, value ) );

						logger.info( "Send deactivate service CoA" );
						result = radiusClient.sendAsync( packet );
					}
				}

				if ( serviceNamesActivate != null && serviceNamesActivate.size() > 0 )
				{
					for ( String serviceName : serviceNamesActivate )
					{
						RadiusPacket packet = radiusClient.createModifyRequest();
						prepareRequest( packet, connection );

						String value = "\\0xb" + serviceName;
						// добавление cisco-SSG-Command-Code
						packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 252, value ) );

						logger.info( "Send activate service CoA" );
						result = radiusClient.sendAsync( packet );
					}
				}

				break;
			}

			case COA_MODE_SUBSCR_COMMAND:
			default:
			{
                result = sendTextCommands( connection, serviceNamesDeactivate, serviceNamesActivate, result );
                break;
			}
		}

		if ( forceModifyOnServiceNak && result != null )
		{
            result = alwaysTrue( result );
		}

		return result;
	}
	
    private Future<Boolean> alwaysTrue( final Future<Boolean> delegate )
    {
        return new Future<Boolean>()
        {
            @Override
            public boolean cancel( boolean mayInterruptIfRunning )
            {
                return delegate.cancel( mayInterruptIfRunning );
            }

            @Override
            public boolean isCancelled()
            {
                return delegate.isCancelled();
            }

            @Override
            public boolean isDone()
            {
                return delegate.isDone();
            }

            @Override
            public Boolean get()
                throws InterruptedException, ExecutionException
            {
                delegate.get();
                return Boolean.TRUE;
            }

            @Override
            public Boolean get( long timeout, TimeUnit unit )
                throws InterruptedException, ExecutionException, TimeoutException
            {
                delegate.get( timeout, unit );
                return Boolean.TRUE;
            }
        };
    }

    private Future<Boolean> sendBinaryCommandsInOnePacket( final InetConnection connection, final Collection<String> serviceNamesDeactivate, final Collection<String> serviceNamesActivate,
                                                           Future<Boolean> result )
        throws Exception
    {
        if ( (serviceNamesDeactivate != null && serviceNamesDeactivate.size() > 0)
            || (serviceNamesActivate != null && serviceNamesActivate.size() > 0) )
        {
            RadiusPacket packet = radiusClient.createModifyRequest();
            prepareRequest( packet, connection );

            if ( serviceNamesDeactivate != null && serviceNamesDeactivate.size() > 0 )
            {
                for ( String serviceName : serviceNamesDeactivate )
                {
                    String value = "\\0xc" + serviceName;
                    // добавление cisco-SSG-Command-Code
                    packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 252, value ) );
                }
            }

            if ( serviceNamesActivate != null && serviceNamesActivate.size() > 0 )
            {
                for ( String serviceName : serviceNamesActivate )
                {
                    String value = "\\0xb" + serviceName;
                    // добавление cisco-SSG-Command-Code
                    packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 252, value ) );
                }
            }

            result = radiusClient.sendAsync( packet );
            logger.info( "Send deactivate/activate services CoA:\n" + packet );

            result = checkPushInvokeFailed( connection, serviceNamesDeactivate, serviceNamesActivate, result );
        }

        return result;
    }

    private Future<Boolean> checkPushInvokeFailed( final InetConnection connection, final Collection<String> serviceNamesDeactivate, final Collection<String> serviceNamesActivate,
                                                   Future<Boolean> result )
    {
        try
        {
            Boolean r = result.get( 500, TimeUnit.MILLISECONDS );
            if ( r == null || r == true )
            {
                return result;
            }

            // на Push invoke failed проверяем, какие сервисы включены, и включаем через replace-all
            ResponseFuture<Boolean> responseFuture = (ResponseFuture<Boolean>)result;
            RadiusPacket response = responseFuture.getResponse();
            String replyMessage = response.getStringAttribute( -1, RadiusDictionary.Reply_Message, null );

            if ( !"Push invoke failed".equals( replyMessage ) )
            {
                return result;
            }

            // сохраняем текущую информацию в логах сессии для отладки
            RadiusPacket statusQuery = radiusClient.createModifyRequest();
            prepareRequest( statusQuery, connection );
            statusQuery.setStringAttribute( 9, 1, "subscriber:command=account-status-query" );

            Future<Boolean> future = radiusClient.sendAsync( statusQuery );
            r = future.get( 1, TimeUnit.SECONDS );

            logger.info( "CoA failed. Trying text commands" );

            // пытаемся переключить сервисы с помощью subscriber:activation-mode=replace-all
            result = sendTextCommandsReplace( connection, serviceNamesDeactivate, serviceNamesActivate, result );
            // вернем, что команда выполнилась в любом случае (как при sa.radius.connection.modify.forceOnServiceNak=1)
            result = alwaysTrue( result );
        }
        catch( Exception ex )
        {}

        return result;
    }

    private Future<Boolean> sendTextCommands( final InetConnection connection, final Collection<String> serviceNamesDeactivate, final Collection<String> serviceNamesActivate, Future<Boolean> result )
        throws Exception
    {
        if ( serviceNamesDeactivate != null && serviceNamesDeactivate.size() > 0 )
        {
            for ( String serviceName : serviceNamesDeactivate )
            {
                RadiusPacket packet = radiusClient.createModifyRequest();
                prepareRequest( packet, connection );

                packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 1, "subscriber:service-name=" + serviceName ) );
                packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 1, "subscriber:command=deactivate-service" ) );

                logger.info( "Send deactivate service CoA:\n" + packet );
                result = radiusClient.sendAsync( packet );
            }
        }

        if ( serviceNamesActivate != null && serviceNamesActivate.size() > 0 )
        {
            for ( String serviceName : serviceNamesActivate )
            {
                RadiusPacket packet = radiusClient.createModifyRequest();
                prepareRequest( packet, connection );

                packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 1, "subscriber:service-name=" + serviceName ) );
                packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 1, "subscriber:command=activate-service" ) );

                logger.info( "Send activate service CoA:\n" + packet );
                result = radiusClient.sendAsync( packet );
            }
        }

        return result;
    }

    /**
     * Переключение сервисов с помощью subscriber:activation-mode=replace-all
     * @param connection
     * @param serviceNamesDeactivate
     * @param serviceNamesActivate
     * @param result
     * @return
     * @throws Exception
     */
    private Future<Boolean> sendTextCommandsReplace( final InetConnection connection, final Collection<String> serviceNamesDeactivate, final Collection<String> serviceNamesActivate,
                                                     Future<Boolean> result )
        throws Exception
    {
        if ( serviceNamesActivate != null && serviceNamesActivate.size() > 0 )
        {
            int i = 0;

            for ( String serviceName : serviceNamesActivate )
            {
                RadiusPacket packet = radiusClient.createModifyRequest();
                prepareRequest( packet, connection );

                packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 1, "subscriber:service-name=" + serviceName ) );
                packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 1, "subscriber:command=activate-service" ) );

                if ( (i++) == 0 )
                {
                    packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 1, "subscriber:activation-mode=replace-all" ) );

                    logger.info( "Send activate (replace) service CoA:\n" + packet );
                    result = radiusClient.sendAsync( packet );
                    result.get();
                }
                else
                {
                    logger.info( "Send activate service CoA:\n" + packet );
                    result = radiusClient.sendAsync( packet );
                }
            }
        }
        else if ( serviceNamesDeactivate != null && serviceNamesDeactivate.size() > 0 )
        {
            for ( String serviceName : serviceNamesDeactivate )
            {
                RadiusPacket packet = radiusClient.createModifyRequest();
                prepareRequest( packet, connection );

                packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 1, "subscriber:service-name=" + serviceName ) );
                packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 1, "subscriber:command=deactivate-service" ) );

                logger.info( "Send deactivate service CoA:\n" + packet );
                result = radiusClient.sendAsync( packet );
            }
        }

        return result;
    }
    
    @Override
    public List<ConfigParameter> configParameterList()
    {
        List<ConfigParameter> configParameters = super.configParameterList(); 
        configParameters.add( new ConfigParameter( SA_RADIUS_CONNECTION_COA_MODE, "" ) );
        configParameters.add( new ConfigParameter( SA_RADIUS_SERVICE_DISABLE, "" ) );
        configParameters.add( new ConfigParameter( SA_RADIUS_SERVICE_ENABLE, "" ) );
        configParameters.add( new ConfigParameter( SA_RADIUS_CONNECTION_COA_ON_ENABLE, "0" ) );
        configParameters.add( new ConfigParameter( SA_RADIUS_CONNECTION_CLOSE_MODE, String.valueOf( CLOSE_MODE_NONE ) ) );
        configParameters.add( new ConfigParameter( SA_RADIUS_CONNECTION_CLOSE_ENABLE_MODE, "0" ) );
        configParameters.add( new ConfigParameter( SA_RADIUS_CONNECTION_CLOSE_DISABLE_SERVICES, "1" ) );
        configParameters.add( new ConfigParameter( SA_RADIUS_CONNECTION_CLOSE_REMOVE_FROM_KEY_MAP, "1" ) );
        configParameters.add( new ConfigParameter( SA_RADIUS_CONNECTION_MODIFY_FORCE_ON_SERVICE_NAK, "0" ) );
        
        return configParameters;
    }
}
