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

import java.net.InetAddress;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import jakarta.annotation.Resource;

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

import ru.bitel.bgbilling.apps.inet.access.sa.ServiceActivator;
import ru.bitel.bgbilling.apps.inet.access.sa.ServiceActivatorAdapter;
import ru.bitel.bgbilling.apps.inet.access.sa.ServiceActivatorEvent;
import ru.bitel.bgbilling.common.model.ConfigParameter;
import ru.bitel.bgbilling.kernel.network.radius.RadiusAttribute;
import ru.bitel.bgbilling.kernel.network.radius.RadiusAttributeSet;
import ru.bitel.bgbilling.kernel.network.radius.RadiusAttributeSetRealmMap;
import ru.bitel.bgbilling.kernel.network.radius.RadiusClient;
import ru.bitel.bgbilling.kernel.network.radius.RadiusDictionary;
import ru.bitel.bgbilling.kernel.network.radius.RadiusPacket;
import ru.bitel.bgbilling.kernel.network.radius.nas.Nas;
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.server.InetUtils;
import ru.bitel.bgbilling.modules.inet.server.radius.InetRadiusProcessor;
import ru.bitel.bgbilling.modules.inet.server.radius.RadiusHourlyDataLogger;
import ru.bitel.bgbilling.server.util.Setup;
import ru.bitel.common.ParameterMap;
import ru.bitel.common.Utils;
import ru.bitel.common.ref.ConcurrentWeakHashMap;

public abstract class AbstractRadiusServiceActivator
    extends ServiceActivatorAdapter
    implements ServiceActivator
{
	private static final Logger logger = LogManager.getLogger();
	
	protected final static String RADIUS_HOST = "radius.host";
	protected final static String RADIUS_PORT = "radius.port";
	protected final static String RADIUS_IDENTIFIER = "radius.identifier";
	protected final static String DHCP_DISABLE_MODE = "dhcp.disable.mode";
	protected final static String SA_ISG = "sa.isg";
	protected final static String SA_RADIUS_SECRET = "sa.radius.secret";
	protected final static String SA_RADIUS_LOG = "sa.radius.log";
    protected final static String SA_RADIUS_LOG_COA = "sa.radius.log.coa";
    protected final static String SA_RADIUS_LOG_POD = "sa.radius.log.pod";
    protected final static String SA_RADIUS_SOURCE_HOST = "sa.radius.sourceHost";
    protected final static String SA_RADIUS_SOURCE_PORT = "sa.radius.sourcePort";
    protected final static String SA_RADIUS_ENABLE_ATTRIBUTES = "sa.radius.enable.attributes";
    protected final static String SA_RADIUS_DISABLE_ATTRIBUTES = "sa.radius.disable.attributes";
    protected final static String SA_RADIUS_POD_ATTRIBUTES = "sa.radius.pod.attributes";
    protected final static String SA_RADIUS_COA_ATTRIBUTES = "sa.radius.coa.attributes";
    protected final static String SA_RADIUS_CONNECTION_ATTRIBUTES = "sa.radius.connection.attributes";
    protected final static String SA_RADIUS_CONNECTION_COA_MODE = "sa.radius.connection.coa.mode";
    protected final static String SA_RADIUS_CONNECTION_COA_ON_ENABLE = "sa.radius.connection.coa.onEnable";
    protected final static String SA_RADIUS_CONNECTION_CLOSE_MODE = "sa.radius.connection.close.mode";
    protected final static String SA_RADIUS_CONNECTION_CLOSE_ENABLE_MODE = "sa.radius.connection.close.enableMode";
    protected final static String SA_RADIUS_CONNECTION_CLOSE_DISABLE_MODE = "sa.radius.connection.close.disableMode";
    protected final static String SA_RADIUS_CONNECTION_CLOSE_REMOVE_FROM_KEY_MAP = "sa.radius.connection.close.removeFromKeyMap";
    protected final static String SA_RADIUS_CONNECTION_CLOSE_DISABLE_SERVICES = "sa.radius.connection.close.disableServices";
    protected final static String SA_RADIUS_CONNECTION_MODIFY_FORCE_ON_SERVICE_NAK = "sa.radius.connection.modify.forceOnServiceNak";
    protected final static String SA_RADIUS_CONNECTION_STATE_MODIFY = "sa.radius.connection.stateModify";
    protected final static String SA_RADIUS_CONNECTION_WITHOUT_BREAK = "sa.radius.connection.withoutBreak";
    protected final static String SA_RADIUS_OPTION_MODE = "sa.optionMode";
    protected final static String SA_RADIUS_OPTION_ATTRIBUTES_PRIFIX = "sa.radius.option.attributesPrefix";
    protected final static String SA_RADIUS_SERVICE_DISABLE = "sa.radius.service.disable";
    protected final static String SA_RADIUS_SERVICE_DISABLE_OPTION_IDS = "sa.radius.service.disable.optionIds";
    protected final static String SA_RADIUS_SERVICE_ENABLE = "sa.radius.service.enable";
    protected final static String SA_RADIUS_SERVICE_CLOSE_ATTRIBUTES = "sa.radius.service.closeAttributes";
    protected final static String SA_RADIUS_REALM_ADD_ATTRIBUTES = "sa.radius.realm.addAttributes";
	
	/**
	 * Класс, записывающий radius пакеты в файлы логов. Нужен для логирования CoA и PoD.
	 */
	@Resource( name = "radiusDataLogger" )
	protected RadiusHourlyDataLogger dataLogger;
	
    @Resource(name = "radiusProcessor")
    protected InetRadiusProcessor processor;

	/**
	 * Код устройства
	 */
	protected int deviceId;

	/**
	 * IP адрес наса.
	 */
	protected InetAddress nasHostAddr;

	/**
	 * Идентификатор наса
	 */
	protected String nasIdentifier;

	/**
	 * Radius-клиент
	 */
	protected RadiusClient radiusClient;

	/**
	 * Привязка атрибутов к реалму
	 */
	protected Map<String, RadiusAttributeSet> realmRadiusAttributesMap;

	/**
	 * Привязка атрибутов к опциям модуля inet из конфига модуля:
	 * <code><pre>radius.inetOption.1.attributes=mpd-limit=out#1=all shape 256000 pass;mpd-limit=in#1=all rate-limit 10000000 pass
	 *radius.inetOption.2.attributes=mpd-limit=out#1=all shape 512000 pass;mpd-limit=in#1=all rate-limit 10000000 pass</pre></code>
	 */
	protected RadiusAttributeSetRealmMap optionRadiusAttributesMap;

	/**
	 * Атрибуты для установки ограничений при посылке CoA вместо PoD, например, ограничение скорости:
	 * <code><pre>radius.realm.disable.attributes=mpd-limit=out#1=all shape 60000 pass;mpd-limit=in#1=all rate-limit 10000000 pass</pre></code>
	 * @see #withoutBreak
	 */
	protected RadiusAttributeSet disableRadiusAttributes;
	
	protected RadiusAttributeSet enableRadiusAttributes;

	/**
	 * Соединения без разрывов, т.е. посылать CoA вместо PoD с ограничениями.
	 */
	protected boolean withoutBreak;

	/**
	 * Имена атрибутов, связанные с соединением, которые нужно посылать в CoA и PoD. 
	 */
	protected Set<String> connectionRadiusAttributes;

	/**
	 * Добавлять ли атрибуты из реалма.
	 */
	protected boolean addRealmAttributes;

	/**
	 * Фиксированные атрибуты, которые нужно посылать в PoD.
	 */
	protected RadiusAttributeSet podFixedAttributes;

	/**
	 * Фиксированные атрибуты, которые нужно посылать в CoA.
	 */
	protected RadiusAttributeSet coaFixedAttributes;

	/**
	 * Нужно ли после смены состояния соединения сразу менять состояние в базе.<br>
	 * Нет - если состояние меняется в обработчике протокола процессора при аккаунтинге.
	 */
	protected boolean needConnectionStateModify;

	private final String defaultOptionsPrefix;

	/**
	 * @see #withoutBreak
	 */
	private final boolean defaultWithoutBreak;

	/**
	 * @see #connectionRadiusAttributes
	 */
	private final String defaultConnectionAttributes;

	/**
	 * @see #needConnectionStateModify
	 * @see ServiceActivatorEvent#setConnectionStateModified(boolean)
	 */
	private final boolean defaultNeedConnectionStateModify;
	
	private boolean logCoA;
	private boolean logPoD;

	public AbstractRadiusServiceActivator( String optionsPrefix, boolean defaultWithoutBreak, String defaultConnectionAttributes, boolean defaultNeedConnectionStateModify )
	{
		if ( optionsPrefix == null )
		{
			optionsPrefix = "radius.inetOption.";
		}

		this.defaultOptionsPrefix = optionsPrefix;
		this.defaultWithoutBreak = defaultWithoutBreak;
		this.defaultConnectionAttributes = defaultConnectionAttributes;
		this.defaultNeedConnectionStateModify = defaultNeedConnectionStateModify;
	}
	
	static class ConfCache
	{
		public Map<String, RadiusAttributeSet> realmRadiusAttributesMap;
		public RadiusAttributeSetRealmMap optionRadiusAttributesMap;
		public RadiusAttributeSet disableRadiusAttributes;
		public RadiusAttributeSet enableRadiusAttributes;
		public Set<String> connectionRadiusAttributes;
		public RadiusAttributeSet podFixedAttributes;
		public RadiusAttributeSet coaFixedAttributes;
	}
	
	private static final ConcurrentWeakHashMap<ParameterMap, ConfCache> CONF_CACHE = new ConcurrentWeakHashMap<ParameterMap, ConfCache>();

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

		final List<String[]> hosts = device.getHostsAsString();
		final String[] host = (hosts != null && hosts.size() > 0) ? hosts.get( 0 ) : null;

		String nasHost = deviceConfig.get( RADIUS_HOST, host != null ? host[0] : device.getHost() );
		this.nasHostAddr = InetAddress.getByName( nasHost );

		int nasPort = deviceConfig.getInt( RADIUS_PORT, Utils.parseInt( host != null ? host[1] : "1700" ) );

		if ( nasPort <= 0 )
		{
			nasPort = 1700;
		}

        nasIdentifier = deviceConfig.get( RADIUS_IDENTIFIER, device.getIdentifier() );
		
        byte[] nasSecret = deviceConfig.get( SA_RADIUS_SECRET, deviceConfig.get( "radius.secret", device.getSecret() ) ).getBytes();
		String sourceHost = deviceConfig.get( SA_RADIUS_SOURCE_HOST, deviceConfig.get( "radius.sourceHost", null ) );
		int sourcePort = deviceConfig.getInt( SA_RADIUS_SOURCE_PORT, deviceConfig.getInt( "radius.sourcePort", -1 ) );

		int log = deviceConfig.getInt( SA_RADIUS_LOG, 1 );
		logCoA = deviceConfig.getInt( SA_RADIUS_LOG_COA, log ) > 0;
		logPoD = deviceConfig.getInt( SA_RADIUS_LOG_POD, log ) > 0;

		if ( logCoA || logPoD )
		{
			radiusClient = new RadiusClient( sourceHost, sourcePort, nasHostAddr, nasPort, nasSecret, setup, dataLogger, logCoA, logPoD );
		}
		else
		{
			radiusClient = new RadiusClient( sourceHost, sourcePort, nasHostAddr, nasPort, nasSecret );
		}
		
		ConfCache confCache = CONF_CACHE.get( deviceConfig );
		if ( confCache == null )
		{
			realmRadiusAttributesMap = RadiusAttributeSet.newRadiusAttributeRealmMap( deviceConfig, "radius.realm.", "attributes" );
			// optionRadiusAttributesMap = RadiusAttributeSet.newRadiusAttributeSetRealmMap( deviceConfig, deviceConfig.get( "sa.radius.option.attributesPrefix", defaultOptionsPrefix ), "attributes" );
			optionRadiusAttributesMap = InetUtils.newRadiusAttributeSetRealmMap( moduleId, deviceConfig, deviceConfig.get( "sa.radius.option.attributesPrefix", defaultOptionsPrefix ) );

			// атрибуты для отправки CoA, если нужно прекратить доступ
			String disableRadiusAttributesString = deviceConfig.get( SA_RADIUS_DISABLE_ATTRIBUTES, deviceConfig.get( "radius.disable.attributes", deviceConfig.get( "radius.realm.disable.attributes", null ) ) );
			disableRadiusAttributes = RadiusAttributeSet.newRadiusAttributeSet( disableRadiusAttributesString );
			
			enableRadiusAttributes = RadiusAttributeSet.getRadiusAttributeSet( deviceConfig.get( SA_RADIUS_ENABLE_ATTRIBUTES, null ) );

			connectionRadiusAttributes = new HashSet<>( Arrays.asList( deviceConfig.get( SA_RADIUS_CONNECTION_ATTRIBUTES, defaultConnectionAttributes ).split( "\\s*,\\s*" ) ) );

			podFixedAttributes = RadiusAttributeSet.newRadiusAttributeSet( deviceConfig.get( SA_RADIUS_POD_ATTRIBUTES, null ) );
			coaFixedAttributes = RadiusAttributeSet.newRadiusAttributeSet( deviceConfig.get( SA_RADIUS_COA_ATTRIBUTES, null ) );
			
			confCache = new ConfCache();
			confCache.realmRadiusAttributesMap = realmRadiusAttributesMap;
			confCache.optionRadiusAttributesMap = optionRadiusAttributesMap;
			confCache.disableRadiusAttributes = disableRadiusAttributes;
			confCache.enableRadiusAttributes = enableRadiusAttributes;
			confCache.connectionRadiusAttributes = connectionRadiusAttributes;
			confCache.podFixedAttributes = podFixedAttributes;
			confCache.coaFixedAttributes = coaFixedAttributes;
			
			CONF_CACHE.put( deviceConfig, confCache );
		}
		else
		{
			realmRadiusAttributesMap = confCache.realmRadiusAttributesMap;
			optionRadiusAttributesMap = confCache.optionRadiusAttributesMap;
			disableRadiusAttributes = confCache.disableRadiusAttributes;
			enableRadiusAttributes = confCache.enableRadiusAttributes;
			connectionRadiusAttributes = confCache.connectionRadiusAttributes;
			podFixedAttributes = confCache.podFixedAttributes;
			coaFixedAttributes = confCache.coaFixedAttributes;
		}

		// если нужно прекратить доступ - отправлять PoD или CoA
		withoutBreak = deviceConfig.getInt( SA_RADIUS_CONNECTION_WITHOUT_BREAK, defaultWithoutBreak ? 1 : 0 ) > 0;
		if ( withoutBreak && (disableRadiusAttributes == null || disableRadiusAttributes.isEmpty()) )
		{
			logger.info( "withoutBreak=true but disableRadiusAttributes is empty" );
			//withoutBreak = false;
		}

		addRealmAttributes = deviceConfig.getInt( SA_RADIUS_REALM_ADD_ATTRIBUTES, 0 ) > 0;

		needConnectionStateModify = deviceConfig.getInt( SA_RADIUS_CONNECTION_STATE_MODIFY, defaultNeedConnectionStateModify ? 1 : 0 ) > 0;

		return null;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public Object destroy()
	    throws Exception
	{
		radiusClient.destroy();

		return null;
	}

	@Override
	public Object connect()
		throws Exception
	{
		if ( processor != null && ( logCoA || logPoD ) )
		{
			final Nas<?, ?, ?> nas = processor.getNasList().get( deviceId );
			radiusClient.setNas( nas );
		}

		return null;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public Object disconnect()
		throws Exception
	{
		return null;
	}

	/**
	 * Подготовка запроса - установка атрибутов, связанная с соединением и насом, а также фиксированных атрибутов.
	 * @param request
	 * @param connection
	 * @see #connectionRadiusAttributes
	 * @see #coaFixedAttributes
	 * @see #podFixedAttributes
	 */
	protected void prepareRequest( RadiusPacket request, InetConnection connection )
	{
		radiusClient.setConnectionId( connection.getId() );
		
		if ( connectionRadiusAttributes.size() == 0 || connectionRadiusAttributes.contains( "NAS-Port" ) )
		{
			request.setAttribute( new RadiusAttribute<Integer>( -1, RadiusDictionary.NAS_Port, 0, connection.getDevicePort() ) );
		}

		if ( connectionRadiusAttributes.size() == 0 || connectionRadiusAttributes.contains( "Acct-Session-Id" ) )
		{
			request.setAttribute( new RadiusAttribute<String>( -1, RadiusDictionary.Acct_Session_Id, 0, connection.getAcctSessionId() ) );
		}

		if ( connectionRadiusAttributes.size() == 0 || connectionRadiusAttributes.contains( "User-Name" ) )
		{
			if ( connection.getUsername() != null )
			{
				request.setAttribute( new RadiusAttribute<String>( -1, RadiusDictionary.User_Name, 0, connection.getUsername() ) );
			}
		}

		if ( connectionRadiusAttributes.size() == 0 || connectionRadiusAttributes.contains( "Framed-IP-Address" ) )
		{
			if ( connection.getInetAddressBytes() != null )
			{
				request.setAttribute( new RadiusAttribute<Integer>( -1, RadiusDictionary.Framed_IP_Address, 0, ByteBuffer.wrap( connection.getInetAddressBytes() ) ) );
			}
		}

		if ( connectionRadiusAttributes.size() == 0 || connectionRadiusAttributes.contains( "NAS-IP-Address" ) )
		{
			int address = Utils.convertBytesToInt( nasHostAddr.getAddress() );
			if ( address != 0 )
			{
				request.setAttribute( new RadiusAttribute<Integer>( -1, RadiusDictionary.NAS_IP_Address, 0, address ) );
			}
		}

		if ( connectionRadiusAttributes.size() == 0 || connectionRadiusAttributes.contains( "NAS-Identifier" ) )
		{
			request.setAttribute( new RadiusAttribute.RadiusAttributeString( -1, RadiusDictionary.NAS_Identifier, nasIdentifier ) );
		}

		if ( request.getCode() == RadiusPacket.CoA_REQUEST )
		{
			// если есть фиксированные атрибуты - добавляем их
			if ( coaFixedAttributes != null )
			{
				request.addAttributes( coaFixedAttributes );
			}

			// если нужно добавить атрибуты реалма - добавляем
			if ( addRealmAttributes )
			{
				String[] usernameAndRealm = getUsernameAndRealm( connection.getUsername(), true );
				String realm = usernameAndRealm[1];
				if ( Utils.isBlankString( realm ) )
				{
					realm = "default";
				}

				RadiusAttributeSet set = realmRadiusAttributesMap.get( realm );
				if ( set != null )
				{
					request.addAttributes( set );
				}
			}
		}
		else if ( request.getCode() == RadiusPacket.DISCONNECT_REQUEST )
		{
			// если есть фиксированные атрибуты - добавляем их
			if ( podFixedAttributes != null )
			{
				request.addAttributes( podFixedAttributes );
			}
		}
	}

	private static String[] getUsernameAndRealm( String username, boolean usernameRemoveDomain )
	{
		String[] result = new String[2];

		int pos = username.indexOf( '@' );
		if ( pos > 0 )
		{
			result[0] = username.substring( pos );
			result[1] = username.substring( pos + 1 );
		}
		else
		{
			result[0] = username;
			result[1] = "default";
		}

		if ( usernameRemoveDomain )
		{
			pos = result[0].indexOf( '\\' );
			if ( pos >= 0 )
			{
				result[0] = result[0].substring( pos + 1 );
			}
		}

		return result;
	}
	
	/**
	 * Обычное CoA с набором атрибутов из опций.
	 * @param e
	 * @return
	 * @throws Exception
	 */
	protected Object connectionModifyCoA( ServiceActivatorEvent e )
	    throws Exception
	{
		logger.info( "Connection modify CoA: oldState: " + e.getOldState() + "; newState: " + e.getNewState() + "; oldOptionSet: " + e.getOldOptions() + "; newOptionSet: " + e.getNewOptions() );
		
		final InetConnection connection = e.getConnection();

		if ( e.getNewState() == InetServState.STATE_DISABLE.getCode() )
		{
			if ( !withoutBreak )
			{
				return connectionClose( e );
			}

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

			request.addAttributes( disableRadiusAttributes );

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

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

			return radiusClient.sendAsync( request );
		}

		if ( e.getOldState() == InetServState.STATE_DISABLE.getCode() )
		{
			if ( !withoutBreak )
			{
				return connectionClose( e );
			}
		}
		
		RadiusPacket request = radiusClient.createModifyRequest();
		prepareRequest( request, connection );
		
		final String realm = e.getRealm();

		// если раньше было состояние отключено, а теперь включено
		// добавляем атрибуты из sa.radius.enable.attributes=
		if ( e.getOldState() == InetServState.STATE_DISABLE.getCode() )
		{
			// дополнительные атрибуты при отмене ограничений (при withoutBreak=true)
			request.addAttributes( enableRadiusAttributes );
		}

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

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

		return radiusClient.sendAsync( request );
	}
	
	@Override
	public List<ConfigParameter> configParameterList()
	{
	    List<ConfigParameter> configParameters = super.configParameterList();
	    configParameters.add( new ConfigParameter( RADIUS_HOST, "" ) );
	    configParameters.add( new ConfigParameter( RADIUS_PORT, "1700" ) );
	    configParameters.add( new ConfigParameter( RADIUS_IDENTIFIER, "" ) );
        configParameters.add( new ConfigParameter( SA_RADIUS_SECRET, "" ) );
	    configParameters.add( new ConfigParameter( SA_RADIUS_LOG, "1" ) );
        configParameters.add( new ConfigParameter( SA_RADIUS_LOG_COA, "1" ) );
        configParameters.add( new ConfigParameter( SA_RADIUS_LOG_POD, "1" ) );
        configParameters.add( new ConfigParameter( SA_RADIUS_SOURCE_HOST, "" ) );
        configParameters.add( new ConfigParameter( SA_RADIUS_SOURCE_PORT, "-1" ) );
        configParameters.add( new ConfigParameter( SA_RADIUS_DISABLE_ATTRIBUTES, "" ) );
        configParameters.add( new ConfigParameter( SA_RADIUS_ENABLE_ATTRIBUTES, "" ) );
        configParameters.add( new ConfigParameter( SA_RADIUS_POD_ATTRIBUTES, "" ) );
        configParameters.add( new ConfigParameter( SA_RADIUS_COA_ATTRIBUTES, "" ) );
        configParameters.add( new ConfigParameter( SA_RADIUS_OPTION_ATTRIBUTES_PRIFIX, "" ) );
        configParameters.add( new ConfigParameter( SA_RADIUS_REALM_ADD_ATTRIBUTES, "0" ) );
        configParameters.add( new ConfigParameter( SA_RADIUS_CONNECTION_ATTRIBUTES, "" ) );
        configParameters.add( new ConfigParameter( SA_RADIUS_CONNECTION_STATE_MODIFY, "0" ) );
        configParameters.add( new ConfigParameter( SA_RADIUS_CONNECTION_WITHOUT_BREAK, "0" ) );
	    return configParameters;
	}
}