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

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.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.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 SmartEdgeServiceActivator
    extends AbstractRadiusServiceActivator
{
	private static final Logger logger = LogManager.getLogger();
	
	public static final int REDBACK_VENDOR = 2352;
	
	public static final int SERVICE_NAME = 190;
	public static final int DEACTIVATE_SERVICE_NAME = 194;
	
	/**
	 * Ключ - код опции, значение - сервис
	 */
	protected Map<Integer, String> inetOptionSeService = new HashMap<>();
	protected Map<Integer, Integer> inetOptionSeServiceTag = new HashMap<>();
	
	/**
	 * Ничего не делать для закрытия соединения.
	 * @see #disableServicesOnClose
	 */
	protected static final int CLOSE_MODE_NONE = 1;

	/**
	 * Посылать PoD пакет для закрытия соединения.
	 * @see #disableServicesOnClose
	 */
	protected static final int CLOSE_MODE_POD = 2;
	
	/**
	 * Режим закрытия соединения.
	 * @see #CLOSE_MODE_NONE
	 * @see #CLOSE_MODE_POD
	 */
	protected int closeMode;
	
	/**
	 * Режим закрытия соединения для переключения из отключен во включен (если {@link #withoutBreak}=false).
	 * @see #CLOSE_MODE_NONE
	 * @see #CLOSE_MODE_POD
	 * @see #CLOSE_MODE_SUBSCR_COMMAND
	 */
	protected int closeEnableMode;
	
	/**
	 * Режим закрытия соединения для переключения из включен в отключен (если {@link #withoutBreak}=false).
	 * @see #CLOSE_MODE_NONE
	 * @see #CLOSE_MODE_POD
	 * @see #CLOSE_MODE_SUBSCR_COMMAND
	 */
	protected int closeDisableMode;
	
	/**
	 * Нужно ли посылать 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;

	/**
	 * Набор атрибутов для закрытия всех сервисов sa.radius.service.closeAttributes=
	 */
	protected RadiusAttributeSet serviceCloseAttributes;
	
	/**
	 * @see #closeMode
	 */
	private final int defaultCloseMode;
	/**
	 * @see #closeEnableMode
	 */
	private final int defaultCloseEnableMode;
	/**
	 * @see #coaOnEnable
	 */
	private final boolean defaultCoaOnEnable;
	/**
	 * @see #closeRemoveFromKeyMap
	 */
	private final boolean defaultCloseRemoveFromKeyMap;
	/**
	 * @see #disableServicesOnClose
	 */
	private final boolean defaultDisableServicesOnClose;
	
	/**
	 * Опции-сервисы, в которые переключаем, когда доступ ограничен.
	 */
	protected Set<Integer> disableServiceOptions;
	
	/**
	 * false - по старому, true - с помощью опций
	 */
	protected boolean optionMode;
	
	/**
	 * При отключении/включении сервисов, если пришел NAK - все равно менять текущий набор опций - deviceOptions.
	 */
	protected boolean forceModifyOnServiceNak;
	
	/**
	 * Если true и optionMode, то смена скоростей одной командой. 
	 */
	protected boolean coaMode;
	
	public SmartEdgeServiceActivator()
	{
		// параметры по умолчанию
		this(
			   false, // defaultWithoutBreak - сбрасываем соединение при переключении состояния
			   CLOSE_MODE_POD, // defaultCloseMode - посылаем PoD при необходимости сбросить соединение
			   CLOSE_MODE_POD, // defaultCloseEnableMode - посылаем PoD пакет при необходимости сбросить соединение при переключении из состояния отключен во включен (при withoutBreak=false)
			   false, // defaultCloseEnableMode - по умолчанию не включаем сервисы при необходимости переключить из состояния отключен во включен (при withoutBreak=false)
			   false, // defaultDisableServicesOnClose - по умолчанию не отключаем сервисы когда нужно сбросить соединение
			   false // defaultCloseRemoveFromKeyMap - по умолчанию не выдаем NAK, когда нужно сбросить соединение
		);
	}
	
	protected SmartEdgeServiceActivator( boolean defaultWithoutBreak )
	{
		this( defaultWithoutBreak, CLOSE_MODE_POD, CLOSE_MODE_POD, false, false, false );
	}

	protected SmartEdgeServiceActivator( boolean defaultWithoutBreak, int defaultCloseMode, int defaultCloseEnableMode,
										 boolean defaultCoaOnEnable, boolean defaultDisableServicesOnClose, boolean defaultCloseRemoveFromKeyMap )
	{
		super( "sa.radius.option.", defaultWithoutBreak, "Acct-Session-Id", false );
		
		this.defaultCloseMode = defaultCloseMode;
		this.defaultCloseEnableMode = defaultCloseEnableMode;
		this.defaultCoaOnEnable = defaultCoaOnEnable;
		this.defaultDisableServicesOnClose = defaultDisableServicesOnClose;
		this.defaultCloseRemoveFromKeyMap = defaultCloseRemoveFromKeyMap;
	}

	@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, 0 ) > 0;
		this.optionMode = deviceConfig.getInt( SA_RADIUS_OPTION_MODE, 0 ) > 0; 
		
		if( optionMode )
		{
			// вычленение из SE атрибутов соответствия опций модуля опциям SE
			Map<Integer, RadiusAttributeSet> optionRadiusAttributesMap = this.optionRadiusAttributesMap.getRealmMap().get( "default" );
			// определение сервисов на каждой из опций
			for( Map.Entry<Integer, RadiusAttributeSet> e : optionRadiusAttributesMap.entrySet() )
			{
				//Service-Name
				List<RadiusAttribute<?>> list = e.getValue().getAttributes( REDBACK_VENDOR, SERVICE_NAME );
				if( list == null || list.size() == 0 )
				{
					continue;
				}

				int option = e.getKey();

				for( RadiusAttribute<?> ra : list )
				{
					/*if( ra.getTag() != 1 )
					{
						continue;
					}*/

					String seService = String.valueOf( ra.getValue() );

					inetOptionSeService.put( option, seService );
					inetOptionSeServiceTag.put( option, ra.getTag() );

					logger.info( "Inet option: " + option + " => SE service: " + seService );
				}
			}
		}
		
		//TODO
		this.closeMode = deviceConfig.getInt( SA_RADIUS_CONNECTION_CLOSE_MODE, defaultCloseMode );
		this.closeEnableMode = deviceConfig.getInt( SA_RADIUS_CONNECTION_CLOSE_ENABLE_MODE, defaultCloseEnableMode );
		this.closeDisableMode = deviceConfig.getInt( SA_RADIUS_CONNECTION_CLOSE_DISABLE_MODE, closeMode );
		
		// нужно ли слать CoA на включение сервисов при переключении из ограничнного доступа
		// по умолчанию просто ждем следующего DHCP-REQUEST чтобы ответить NAK и выдать былый адрес
		this.coaOnEnable = deviceConfig.getInt( SA_RADIUS_CONNECTION_COA_ON_ENABLE, defaultCoaOnEnable ? 1 : 0 ) > 0;

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

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

		// набор атрибутов для закрытия всех сервисов sa.radius.service.closeAttributes=
		if( optionMode )
		{
			this.serviceCloseAttributes = RadiusAttributeSet.newRadiusAttributeSet( deviceConfig.get( SA_RADIUS_SERVICE_CLOSE_ATTRIBUTES, "" ) );
		}
		else
		{
			this.serviceCloseAttributes = RadiusAttributeSet.newRadiusAttributeSet( deviceConfig.get( SA_RADIUS_SERVICE_CLOSE_ATTRIBUTES, deviceConfig.get( "close.attributes", "Deactivate-Service-Name:1=RSE-SVC-EXT" ) ) );
		}

		if( optionMode )
		{
			// сервис(ы), отправляемый в режиме Reject-To-Accept
			List<Integer> disableServiceOptions = Utils.toIntegerList( deviceConfig.get( SA_RADIUS_SERVICE_DISABLE_OPTION_IDS, "" ) );
			if( disableServiceOptions.size() == 0 )
			{
				List<String> disableServiceNames = Utils.toList( deviceConfig.get( SA_RADIUS_SERVICE_DISABLE, deviceConfig.get( "radius.serviceName.disable", "" ) ) );
				if( disableServiceNames.size() > 0 )
				{
					LOOP:
					for( String disableServiceName : disableServiceNames )
					{
						for( Map.Entry<Integer, String> e : inetOptionSeService.entrySet() )
						{
							if( disableServiceName.equals( e.getValue() ) )
							{
								disableServiceOptions.add( e.getKey() );
								continue LOOP;
							}
						}
					}
				}
			}
			
			if( disableServiceOptions.size() > 0 )
			{
				this.disableServiceOptions = Collections.newSetFromMap( new LinkedHashMap<Integer, Boolean>() );
				this.disableServiceOptions.addAll( disableServiceOptions );
			}
			else
			{
				this.disableServiceOptions = null;
			}

			logger.info( "Disable options: " + disableServiceOptions );
		}
		
		this.forceModifyOnServiceNak = deviceConfig.getInt( SA_RADIUS_CONNECTION_MODIFY_FORCE_ON_SERVICE_NAK, 0 ) > 0;

		return null;
	}

	@Override
	public Object destroy()
	        throws Exception
	{
		super.destroy();

		return null;
	}

	/**
	 * Изменение состояния сессии или набора опций.
	 */
	@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( e.getNewState() == InetServState.STATE_DISABLE.getCode() )
		{
			// если всегда сбрасываем соединение при переключении состояния - вызываем просто connectionClose
			// например, чтобы поменять IP с белого на серый
			// sa.radius.connection.withoutBreak=0
			if( !withoutBreak )
			{
				return connectionClose( e, connection, closeDisableMode, disableServicesOnClose );
			}
			
			// устанавливаем флаг, что нужно будет поменять состояние соединения в базе
			// sa.radius.connection.stateModify=1
			if( needConnectionStateModify )
			{
				e.setConnectionStateModified( true );
			}

			// отключаем соединение
			return connectionDisable( e, connection );
		}

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

				// если при необходимости включить доступ до того как поменяли IP мы хотим включить доступ на SE, то идем дальше
				// иначе return
				if( !coaOnEnable )
				{
					return result;
				}
			}
			
			// устанавливаем флаг, что нужно будет поменять состояние соединения в базе
			// sa.radius.connection.stateModify=1
			if( needConnectionStateModify )
			{
				e.setConnectionStateModified( true );
			}
			
			if( optionMode )
			{
				return sendCommands( e, connection, disableServiceOptions, e.getNewOptions() );
			}
		}
		
		//Set<String> seServicesToDisable = optionsToServiceSet( e.getOldOptions() );
		final Set<RadiusAttribute<String>> seServicesToDisable = optionsToAttributeSet( e.getOldOptions(), DEACTIVATE_SERVICE_NAME );
		
		RadiusPacket request = null;

        if( !serviceCloseAttributes.isEmpty() || seServicesToDisable.size() > 0 ) // закрываем все сервисы
        {
            request = radiusClient.createModifyRequest();
            prepareRequest( request, connection );
            request.addAttributes( serviceCloseAttributes );

            for( RadiusAttribute<String> seServiceToDisable : seServicesToDisable )
            {
                request.addAttribute( seServiceToDisable );
            }
            
            // по возможности смену скорости отправляем одним пакетом
            boolean onePacket = coaMode && seServicesToDisable.size() > 0 && serviceCloseAttributes.isEmpty() && enableRadiusAttributes.isEmpty();

            if( !onePacket )
            {
                logger.info( "Send close services CoA: \n" + request );

                radiusClient.send( request );
                request = null;
            }
        }

        // готовим пакет на включение сервисов
        if( request == null )
        {
            request = radiusClient.createModifyRequest();
            prepareRequest( request, connection );
        }
		
		// если раньше было состояние отключено, а теперь включено
		// добавляем атрибуты из sa.radius.enable.attributes=
		if( e.getOldState() == InetServState.STATE_DISABLE.getCode() )
		{
			request.addAttributes( enableRadiusAttributes );
		}
		
		final String realm = e.getRealm();

		// открываем все подключенные сервисы
		// используя атрибуты опций из конфига устройства с префиксом sa.radius.option.attributesPrefix=radius.inetOption.
		// (т.е. по умолчанию атрибуты radius.inetOption.<id_опции>.attributes=)
		// и/или конфига опций
		for( Integer option : e.getNewOptions() )
		{
			RadiusAttributeSet set = optionRadiusAttributesMap.get( realm, option );
			if( set != null )
			{
				request.addAttributes( set );
			}
		}

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

		return result( radiusClient.sendAsync( request ) );
	}
	
	protected Set<String> optionsToServiceSet( final Collection<Integer> options )
	{
		if( options == null || options.size() == 0 || inetOptionSeService == null || inetOptionSeService.size() == 0 )
		{
			return Collections.emptySet();
		}

		final Set<String> result = new HashSet<>();

		for( Integer option : options )
		{
			String seService = inetOptionSeService.get( option );
			if( Utils.isBlankString( seService ) )
			{
				continue;
			}

			result.add( seService );
		}

		return result;
	}
	
	protected Set<RadiusAttribute<String>> optionsToAttributeSet( final Collection<Integer> options, final int type )
	{
		if( options == null || options.size() == 0 || inetOptionSeService == null || inetOptionSeService.size() == 0 )
		{
			return Collections.emptySet();
		}

		final Set<RadiusAttribute<String>> result = new HashSet<>();

		for( Integer option : options )
		{
			final String seService = inetOptionSeService.get( option );
			if( Utils.isBlankString( seService ) )
			{
				continue;
			}
			
			Integer tag = inetOptionSeServiceTag.get( option );
			if( tag == null )
			{
				tag = 1;
			}

			result.add( new RadiusAttribute<String>( REDBACK_VENDOR, type, tag, seService ) );
		}

		return result;
	}
	
	protected Object sendCommands( final ServiceActivatorEvent e, final InetConnection connection, final Collection<Integer> optionsDeactivate, final Collection<Integer> optionsActivate )
		throws Exception
	{
		logger.info( "sendCommands" );

		// final Set<String> servicesToDeactivate = optionsToServiceSet( optionsDeactivate );
		final Set<RadiusAttribute<String>> servicesToDeactivate = optionsToAttributeSet( optionsDeactivate, DEACTIVATE_SERVICE_NAME );

		if( servicesToDeactivate.size() > 0 )
		{
			final RadiusPacket request = radiusClient.createModifyRequest();
			prepareRequest( request, connection );
			for( RadiusAttribute<String> seServiceToDeactivate : servicesToDeactivate )
			{
				request.addAttribute( seServiceToDeactivate );
			}

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

			radiusClient.send( request );
		}

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

			final String realm = e.getRealm();

			boolean wasAdded = false;

			for( Integer option : optionsActivate )
			{
				RadiusAttributeSet set = optionRadiusAttributesMap.get( realm, option );
				if( set != null )
				{
					wasAdded = true;
					request.addAttributes( set );
				}
			}

			if( !wasAdded )
			{
				return null;
			}

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

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

		return null;
	}
	
	/**
	 * Ограничение доступа.
	 */
	protected Object connectionDisable( final ServiceActivatorEvent e, final InetConnection connection )
	        throws Exception
	{
		logger.info( "Connection disable" );

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

		if( optionMode )
		{
			// переключение с disabled option на disabled state. SE-сервис-ограничение уже должен висеть - ничего не делаем 
			if( disableServiceOptions != null && disableServiceOptions.equals( e.getOldOptions() ) )
			{
				logger.info( "Rebind detected" );
				e.setConnectionStateModified( true );
				return null;
			}
			
			return sendCommands( e, connection, e.getOldOptions(), disableServiceOptions );
		}
		else
		{
			// выключение сервиса, отправляем атрибуты редиректа
			// sa.radius.disable.attributes= radius.disable.attributes=
			request.addAttributes( disableRadiusAttributes );
		}

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

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

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

		if( disableServices )
		{
			result = connectionDisable( e, connection );
		}
		else
		{
			result = null;
		}

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

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

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

				break;
			}
		}

		return result;
	}

	/**
	 * Сброс соединения.<br>
	 * {@inheritDoc}
	 */
	@Override
	public Object connectionClose( final ServiceActivatorEvent e )
		throws Exception
	{
		logger.info( "Connection close" );

		final InetConnection connection = e.getConnection();

		return connectionClose( e, connection, closeMode, disableServicesOnClose );
	}
	
	protected Future<Boolean> result( Future<Boolean> result )
	{
		if( forceModifyOnServiceNak && result != null )
		{
			final Future<Boolean> delegate = result;
			result = 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;
				}
			};
		}

		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_OPTION_MODE, "0" ) );
        configParameters.add( new ConfigParameter( SA_RADIUS_CONNECTION_CLOSE_MODE, "" ) );
        configParameters.add( new ConfigParameter( SA_RADIUS_CONNECTION_CLOSE_ENABLE_MODE, "" ) );
        configParameters.add( new ConfigParameter( SA_RADIUS_CONNECTION_CLOSE_DISABLE_MODE, "" ) );
        configParameters.add( new ConfigParameter( SA_RADIUS_CONNECTION_COA_ON_ENABLE, "" ) );
        configParameters.add( new ConfigParameter( SA_RADIUS_CONNECTION_CLOSE_DISABLE_SERVICES, "" ) );
        configParameters.add( new ConfigParameter( SA_RADIUS_CONNECTION_CLOSE_REMOVE_FROM_KEY_MAP, "" ) );
        configParameters.add( new ConfigParameter( SA_RADIUS_CONNECTION_MODIFY_FORCE_ON_SERVICE_NAK, "" ) );
        configParameters.add( new ConfigParameter( SA_RADIUS_SERVICE_CLOSE_ATTRIBUTES, "" ) );
        configParameters.add( new ConfigParameter( SA_RADIUS_SERVICE_DISABLE, "" ) );
        configParameters.add( new ConfigParameter( SA_RADIUS_SERVICE_DISABLE_OPTION_IDS, "" ) );
        return configParameters;
    }
}
