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

import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

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

import ru.bitel.bgbilling.apps.inet.access.sa.ProtocolHandlerAdapter;
import ru.bitel.bgbilling.common.model.ConfigParameter;
import ru.bitel.bgbilling.kernel.container.managed.ServerContext;
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.RadiusDictionary;
import ru.bitel.bgbilling.kernel.network.radius.RadiusPacket;
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.InetServ;
import ru.bitel.bgbilling.modules.inet.common.bean.InetServState;
import ru.bitel.bgbilling.modules.inet.common.bean.InetServType;
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.RadiusAccessRequestHandler;
import ru.bitel.bgbilling.server.util.Setup;
import ru.bitel.common.ParameterMap;
import ru.bitel.common.Utils;
import ru.bitel.common.inet.IpAddress;
import ru.bitel.common.inet.IpNet;
import ru.bitel.common.sql.ConnectionSet;

public class AbstractRadiusProtocolHandler
    extends ProtocolHandlerAdapter
{
	private static final Logger logger = LogManager.getLogger();
	
	protected final static String RADIUS_MAC_ADDRESS_VENDOR = "radius.macAddress.vendor";
	protected final static String RADIUS_MAC_ADDRESS_TYPE = "radius.macAddress.type";
	protected final static String RADIUS_MAC_ADDRESS_PREFIX = "radius.macAddress.prefix";
	protected final static String RADIUS_MAC_ADDRESS_PATTERN = "radius.macAddress.pattern";

	protected final static String RADIUS_VLAN_VENDOR = "radius.vlan.vendor";
	protected final static String RADIUS_VLAN_TYPE = "radius.vlan.type";
	protected final static String RADIUS_VLAN_PREFIX = "radius.vlan.prefix";
	protected final static String RADIUS_VLAN_PATTERN = "radius.vlan.pattern";

	protected final static String RADIUS_AGENT_SVLAN_VENDOR = "radius.agentSvlan.vendor";
	protected final static String RADIUS_AGENT_SVLAN_TYPE = "radius.agentSvlan.type";
	protected final static String RADIUS_AGENT_SVLAN_PREFIX = "radius.agentSvlan.prefix";
	protected final static String RADIUS_AGENT_SVLAN_PATTERN = "radius.agentSvlan.pattern";

	protected final static String RADIUS_CALLING_STATION_ID_VENDOR = "radius.callingStationId.vendor";
	protected final static String RADIUS_CALLING_STATION_ID_TYPE = "radius.callingStationId.type";
	protected final static String RADIUS_CALLING_STATION_ID_PREFIX = "radius.callingStationId.prefix";

	protected final static String RADIUS_CALLED_STATION_ID_VENDOR = "radius.calledStationId.vendor";
	protected final static String RADIUS_CALLED_STATION_ID_TYPE = "radius.calledStationId.type";
	protected final static String RADIUS_CALLED_STATION_ID_PREFIX = "radius.calledStationId.prefix";
	
	protected final static String RADIUS_VENDOR = "radius.vendor";
	protected final static String RADIUS_DISABLE_PATTERN_ATTRIBUTES = "radius.disable.pattern.attributes";

	private final int defaultRadiusVendor;

	/**
	 * Код вендора
	 */
	protected int radiusVendor;

	/**
	 * Набор атрибутов, присутствие которых в аккаунтинг-пакете означает что соединение в состоянии {@link InetServ#STATE_DISABLE}.
	 */
	protected RadiusAttributeSet disablePatternAttributes;
	
	/**
	 * Вендор атрибута - MAC-адрес
	 */
	protected int macAddressVendor;
	
	/**
	 * Код атрибута - MAC-адрес
	 */
	protected int macAddressType;
	
	/**
	 * Префикс атрибута (если есть) - MAC-адрес
	 */
	protected String macAddressPrefix;
	
    /**
     * Шаблон поиска MAC-адреса
     */
    protected Pattern macAddressPattern;
	
	/**
	 * Вендор атрибута - значение для подмены Calling-Station-Id
	 */
	protected int callingStationIdVendor;
	
	/**
	 * Код атрибута - значение для подмены Calling-Station-Id
	 */
	protected int callingStationIdType;
	
	/**
	 * Префикс атрибута (если есть) - значение для подмены Calling-Station-Id
	 */
	protected String callingStationIdPrefix;
	
    /**
     * Вендор атрибута - значение для подмены Called-Station-Id
     */
    protected int calledStationIdVendor;
    
    /**
     * Код атрибута - значение для подмены Called-Station-Id
     */
    protected int calledStationIdType;
    
    /**
     * Префикс атрибута (если есть) - значение для подмены Called-Station-Id
     */
    protected String calledStationIdPrefix;
    
    /**
     * Вендор атрибута - значение для VLAN
     */
    protected int vlanVendor;
    
    /**
     * Код атрибута - значение для VLAN
     */
    protected int vlanType;
    
    /**
     * Префикс атрибута (если есть) - значение для VLAN
     */
    protected String vlanPrefix;
    
    /**
     * Шаблон поиска VLAN
     */
    protected Pattern vlanPattern;
    
    /**
     * Вендор атрибута - значение для SVLAN
     */
    protected int agentSvlanVendor;
    
    /**
     * Код атрибута - значение для SVLAN
     */
    protected int agentSvlanType;
    
    /**
     * Префикс атрибута (если есть) - значение для SVLAN
     */
    protected String agentSvlanPrefix;
    
    /**
     * Шаблон поиска SVLAN
     */
    protected Pattern agentSvlanPattern;
	
	/**
	 * Привязка атрибутов к опциям модуля 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;
	
	private String addFramedRouteHost;

	public AbstractRadiusProtocolHandler( int defaultRadiusVendor )
	{
		this.defaultRadiusVendor = defaultRadiusVendor;
	}

	public AbstractRadiusProtocolHandler()
	{
		this.defaultRadiusVendor = 2352; // Redback
	}

	@Override
	public void init( Setup setup, int moduleId, InetDevice inetDevice, InetDeviceType inetDeviceType, ParameterMap deviceConfig )
	    throws Exception
	{
		radiusVendor = deviceConfig.getInt( RADIUS_VENDOR, this.defaultRadiusVendor );

		String disablePatternAttributesString = deviceConfig.get( RADIUS_DISABLE_PATTERN_ATTRIBUTES, null );
		if( Utils.notBlankString( disablePatternAttributesString ) )
		{
			disablePatternAttributes = RadiusAttributeSet.newRadiusAttributeSet( disablePatternAttributesString );
			// deviceConfig.getInt( "radius.realm.disable.attribute.type", -1 ); //  HTTP-Redirect-Profile-Name  107 =  NOAUTH	
		}
		else
		{
			disablePatternAttributes = null;
		}
		
		macAddressVendor = deviceConfig.getInt( RADIUS_MAC_ADDRESS_VENDOR, radiusVendor );
		macAddressType = deviceConfig.getInt( RADIUS_MAC_ADDRESS_TYPE, -1 );
		macAddressPrefix = deviceConfig.get( RADIUS_MAC_ADDRESS_PREFIX, null );
		
        try
        {
            final String pattern = deviceConfig.get( RADIUS_MAC_ADDRESS_PATTERN, null );
            if( pattern != null )
            {
                macAddressPattern = Pattern.compile( pattern );
            }
        }
        catch( Exception ex )
        {
            logger.error( ex.getMessage(), ex );
        }
		
		callingStationIdVendor = deviceConfig.getInt( RADIUS_CALLING_STATION_ID_VENDOR, radiusVendor );
		callingStationIdType = deviceConfig.getInt( RADIUS_CALLING_STATION_ID_TYPE, -1 );
		callingStationIdPrefix = deviceConfig.get( RADIUS_CALLING_STATION_ID_PREFIX, null );
		
        calledStationIdVendor = deviceConfig.getInt( RADIUS_CALLED_STATION_ID_VENDOR, radiusVendor );
        calledStationIdType = deviceConfig.getInt( RADIUS_CALLED_STATION_ID_TYPE, -1 );
        calledStationIdPrefix = deviceConfig.get( RADIUS_CALLED_STATION_ID_PREFIX, null );
        
        vlanVendor = deviceConfig.getInt( RADIUS_VLAN_VENDOR, radiusVendor );
        vlanType = deviceConfig.getInt( RADIUS_VLAN_TYPE, -1 );
        vlanPrefix = deviceConfig.get( RADIUS_VLAN_PREFIX, null );

        try
        {
            final String pattern = deviceConfig.get( RADIUS_VLAN_PATTERN, null );
            if( pattern != null )
            {
                vlanPattern = Pattern.compile( pattern );
            }
        }
        catch( Exception ex )
        {
            logger.error( ex.getMessage(), ex );
        }
        
        agentSvlanVendor = deviceConfig.getInt( RADIUS_AGENT_SVLAN_VENDOR, radiusVendor );
        agentSvlanType = deviceConfig.getInt( RADIUS_AGENT_SVLAN_TYPE, -1 );
        agentSvlanPrefix = deviceConfig.get( RADIUS_AGENT_SVLAN_PREFIX, null );

        try
        {
            final String pattern = deviceConfig.get( RADIUS_AGENT_SVLAN_PATTERN, null );
            if( pattern != null )
            {
                agentSvlanPattern = Pattern.compile( pattern );
            }
        }
        catch( Exception ex )
        {
            logger.error( ex.getMessage(), ex );
        }
		
		optionRadiusAttributesMap = InetUtils.newRadiusAttributeSetRealmMap( moduleId, deviceConfig, "radius.inetOption." );
		
		if( deviceConfig.getInt( "radius.addFramedRoute", 0 ) > 0 )
		{
			addFramedRouteHost = deviceConfig.get( "radius.addFramedRoute.gateway", null );

			if( Utils.isBlankString( addFramedRouteHost ) )
			{
				List<InetSocketAddress> hostList = inetDevice.getHosts();
				if( hostList != null && hostList.size() > 0 )
				{
					InetAddress address = hostList.get( 0 ).getAddress();
					addFramedRouteHost = IpAddress.toString( address.getAddress() );
				}
			}

			if( Utils.isBlankString( addFramedRouteHost ) )
			{
				addFramedRouteHost = null;
			}
		}
	}

	/**
	 * Установка состояние соединения по присутствии в Accounting пакете определенных атрибутов.
	 * @param request
	 * @see #disablePatternAttributes
	 */
	protected void setStateFromAttributes( final RadiusPacket request )
	{
		if( disablePatternAttributes != null )
		{
			// если пакет содержит атррибуты - значит соединение в состоянии disable
			if( request.contains( disablePatternAttributes ) )
			{
				logger.debug( "State is disable (from attributes)" );
				request.setOption( InetRadiusProcessor.DEVICE_STATE, InetServState.STATE_DISABLE.getCode() );
			}
			// иначе - enable
			else
			{
				logger.debug( "State is enable (from attributes)" );
				request.setOption( InetRadiusProcessor.DEVICE_STATE, InetServState.STATE_ENABLE.getCode() );
			}
		}
	}
	
	/**
	 * Установка MAC-адреса из RADIUS-атрибута.
	 * @param request
	 */
	protected void setMacAddress( final RadiusPacket request )
	{
        Object macAddress = request.getAttributeValue( macAddressVendor, macAddressType, macAddressPrefix, macAddressPattern, null );
        if( macAddress == null )
        {
            if( macAddressPattern != null )
            {
                logger.info( "MAC not found by given pattern" );
            }

            return;
        }

        if( macAddress instanceof String )
        {
            request.setOption( InetRadiusProcessor.MAC_ADDRESS, (String)macAddress );
        }
        else if( macAddress instanceof byte[] )
        {
            request.setOption( InetRadiusProcessor.MAC_ADDRESS_BYTES, (byte[])macAddress );
        }
        else
        {
            logger.error( "Unknown type for macAddress attribute." );
        }
    }
	
	/**
	 * Подмена Calling-Station-Id из RADIUS-атрибута.
	 * @param request
	 */
	protected void setCallingStationId( final RadiusPacket request )
	{
		final Object callingStationId = request.getAttributeValue( callingStationIdVendor, callingStationIdType, callingStationIdPrefix, null );
		if( callingStationId != null )
		{
			request.setStringAttribute( -1, RadiusDictionary.Calling_Station_Id, String.valueOf( callingStationId ) );
		}
	}
	
    /**
     * Подмена Called-Station-Id из RADIUS-атрибута.
     * @param request
     */
    protected void setCalledStationId( final RadiusPacket request )
    {
        final Object calledStationId = request.getAttributeValue( calledStationIdVendor, calledStationIdType, calledStationIdPrefix, null );
        if( calledStationId != null )
        {
            request.setStringAttribute( -1, RadiusDictionary.Called_Station_Id, String.valueOf( calledStationId ) );
        }
    }
    
    /**
     * Установка VLAN из запроса
     * @param request
     */
    protected void setVlan( final RadiusPacket request )
    {
        final Object vlanObject = request.getAttributeValue( vlanVendor, vlanType, vlanPrefix, vlanPattern, null );
        if( vlanObject == null )
        {
            if( vlanPattern != null )
            {
                logger.info( "VLAN not found by given pattern" );
            }

            return;
        }

        request.setOption( InetRadiusProcessor.VLAN_ID, String.valueOf( vlanObject ) );
    }
    
    /**
     * Установка SVLAN из запроса
     * @param request
     */
    protected void setAgentSvlan( final RadiusPacket request )
    {
        final Object agentSvlanObject = request.getAttributeValue( agentSvlanVendor, agentSvlanType, agentSvlanPrefix, agentSvlanPattern, null );
        if( agentSvlanObject == null )
        {
            if( agentSvlanPattern != null )
            {
                logger.info( "VLAN not found by given pattern" );
            }

            return;
        }

        request.setOption( InetRadiusProcessor.AGENT_SVLAN, String.valueOf( agentSvlanObject ) );
    }
	
	/**
	 * Получение значения атрибута с учетом префикса, если установлен.
	 * @param request
	 * @param vendor
	 * @param type
	 * @param prefix необязательный префикс
	 * @return
	 * 
	 * @deprecated use {@link RadiusPacket#getAttributeValue(int, int, String, Object)}
	 */
    @Deprecated
	protected Object getAttributeValue( final RadiusPacket request, final int vendor, final int type, final String prefix )
	{
	    return request.getAttributeValue( vendor, type, prefix, null );
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void preprocessAccessRequest( RadiusPacket request, RadiusPacket response, ConnectionSet connectionSet )
	    throws Exception
	{
		// устанавливаем MAC-адрес
		setMacAddress( request );
		// подменяем Calling-Station-Id
		setCallingStationId( request );
        // подменяем Called-Station-Id
        setCalledStationId( request );
        // устанавливаем VLAN из пакета
        setVlan( request );
        // устанавливаем SVLAN из пакета
        setAgentSvlan( request );
		// устанавливаем состояние по наличию определенных атрибутов
		setStateFromAttributes( request );
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void postprocessAccessRequest( RadiusPacket request, RadiusPacket response, ConnectionSet connectionSet )
		throws Exception
	{
		addFramedRoute( response );
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void preprocessAccountingRequest( RadiusPacket request, RadiusPacket response, ConnectionSet connectionSet )
	    throws Exception
	{
		// устанавливаем MAC-адрес
		setMacAddress( request );
		// подменяем Calling-Station-Id
		setCallingStationId( request );
        // подменяем Called-Station-Id
        setCalledStationId( request );
        // устанавливаем VLAN из пакета
        setVlan( request );
        // устанавливаем SVLAN из пакета
        setAgentSvlan( request );
		// устанавливаем состояние по наличию определенных атрибутов
		setStateFromAttributes( request );
	}

	/**
	 * Этот метод по умолчанию не вызывается, т.к. данный класс не имплиментирует интерфейс {@link RadiusAccessRequestHandler}.
	 * Однако он содержит стандартную логику выдачи атрибутов, чтобы ее можно было изменить.<br>
	 * Также, для того чтобы отработала логика выдачи по умолчанию достаточно вернуть как результат false.
	 * @see RadiusAccessRequestHandler#addResponseAttributes(ServerContext, InetServType, InetServ, RadiusAttributeSet, String, Map, RadiusAttributeSet, Set)
	 * @see RadiusAccessRequestHandler
	 *
	 * @param context
	 * @param inetServType
	 * @param inetServ
	 * @param response
	 * @param realm
	 * @param realmAttributeMap
	 * @param inetServAttributes
	 * @param optionSet
	 * @return true - если все атрибуты выданы в этом методе, т.е. добавлять атрибуты логикой по умолчанию не нужно,
	 * false - чтобы сработала логика выдачи атрибутов по умолчанию.  
	 * @throws Exception
	 */
	public boolean addResponseAttributes( ServerContext context, InetServType inetServType, InetServ inetServ, RadiusPacket response, String realm,
	                                      Map<String, RadiusAttributeSet> realmAttributeMap, RadiusAttributeSet inetServAttributes, Set<Integer> optionSet )
	    throws Exception
	{
		// атрибуты реалма
		/*if( InetRadiusProcessor.REALM_DISABLE.equals( realm ) )
		{
			RadiusAttributeSet set = disableAttributeMap.get( radiusSession.errorCode );
			if( set == null )
			{
				set = realmAttributeMap.get( realm );
			}

			if( set != null )
			{
				response.addAttributes( set );
			}
		}
		else
		{
			RadiusAttributeSet set = realmAttributeMap.get( realm );
			if( set != null )
			{
				response.addAttributes( set );
			}
		}*/
		
		// атрибуты реалма
		RadiusAttributeSet realmAttributeSet = realmAttributeMap.get( realm );
		if( realmAttributeSet != null )
		{
			response.addAttributes( realmAttributeSet );
		}

		// атрибуты сервиса/типа сервиса
		if( inetServAttributes != null )
		{
			response.addAttributes( inetServAttributes );
		}

		// для disable не выдаем атрибуты опций
		if( InetRadiusProcessor.REALM_DISABLE.equals( realm ) )
		{
			return true;
		}

		// атрибуты опций
		if( optionSet != null && optionSet.size() > 0 )
		{
			for( Integer option : optionSet )
			{
				final RadiusAttributeSet optionRadiusAttributes = this.optionRadiusAttributesMap.get( realm, option );
				if( optionRadiusAttributes != null )
				{
					response.addAttributes( optionRadiusAttributes );
				}
			}
		}

		return true;
	}
	
	protected void addFramedRoute( RadiusPacket response )
		throws Exception
	{
		// Framed-Route=x.x.x.x/32 y.y.y.y 1, где x.x.x.x - IP-адрес абонента, y.y.y.y - это IP-адрес Redback'а,
		// а единица - это дистанция, она не изменна
		if( addFramedRouteHost != null )
		{
			final byte[] address = response.getByteAttribute( -1, RadiusDictionary.Framed_IP_Address, null );

			if( address != null )
			{
				final String subnet = IpNet.toString( address, 32 );
				// и добавляем его как Framed-Route, чтобы можно было обсчитывать по Netflow
				response.setAttribute( new RadiusAttribute<String>( -1, RadiusDictionary.Framed_Route, -1, subnet + " " + addFramedRouteHost + " 1" ) );
			}
		}
	}
	
	@Override
	public List<ConfigParameter> configParameterList()
	{
	    List<ConfigParameter> configParameters = super.configParameterList();
	    
	    configParameters.add( new ConfigParameter( RADIUS_MAC_ADDRESS_VENDOR, "" ) );
	    configParameters.add( new ConfigParameter( RADIUS_MAC_ADDRESS_TYPE, "" ) );
	    configParameters.add( new ConfigParameter( RADIUS_MAC_ADDRESS_PREFIX, "" ) );
	    configParameters.add( new ConfigParameter( RADIUS_MAC_ADDRESS_PATTERN, "" ) );
	    
	    configParameters.add( new ConfigParameter( RADIUS_VLAN_VENDOR, "" ) );
	    configParameters.add( new ConfigParameter( RADIUS_VLAN_TYPE, "" ) );
	    configParameters.add( new ConfigParameter( RADIUS_VLAN_PREFIX, "" ) );
	    configParameters.add( new ConfigParameter( RADIUS_VLAN_PATTERN, "" ) );
	    
	    configParameters.add( new ConfigParameter( RADIUS_AGENT_SVLAN_VENDOR, "" ) );
	    configParameters.add( new ConfigParameter( RADIUS_AGENT_SVLAN_TYPE, "" ) );
	    configParameters.add( new ConfigParameter( RADIUS_AGENT_SVLAN_PREFIX, "" ) );
	    configParameters.add( new ConfigParameter( RADIUS_AGENT_SVLAN_PATTERN, "" ) );

	    configParameters.add( new ConfigParameter( RADIUS_CALLING_STATION_ID_VENDOR, "" ) );
	    configParameters.add( new ConfigParameter( RADIUS_CALLING_STATION_ID_TYPE, "" ) );
	    configParameters.add( new ConfigParameter( RADIUS_CALLING_STATION_ID_PREFIX, "" ) );
	    	    
	    configParameters.add( new ConfigParameter( RADIUS_CALLED_STATION_ID_VENDOR, "" ) );
	    configParameters.add( new ConfigParameter( RADIUS_CALLED_STATION_ID_TYPE, "" ) );
	    configParameters.add( new ConfigParameter( RADIUS_CALLED_STATION_ID_PREFIX, "" ) );
	    	    
	    configParameters.add( new ConfigParameter( RADIUS_VENDOR, "" ) );
	    configParameters.add( new ConfigParameter( RADIUS_DISABLE_PATTERN_ATTRIBUTES, "" ) );
	    return configParameters;
	}
}