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

import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.List;
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.Access;
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.apps.inet.accounting.event.InetAccountingEvent;
import ru.bitel.bgbilling.apps.inet.accounting.event.InetConnectionAliveEvent;
import ru.bitel.bgbilling.kernel.event.EventListener;
import ru.bitel.bgbilling.kernel.event.EventListenerContext;
import ru.bitel.bgbilling.kernel.event.EventProcessor;
import ru.bitel.bgbilling.kernel.event.common.Event;
import ru.bitel.bgbilling.kernel.network.radius.RadiusAttributeSet;
import ru.bitel.bgbilling.kernel.network.radius.RadiusClient;
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.server.runtime.InetServRuntime;
import ru.bitel.bgbilling.modules.inet.server.runtime.device.InetDeviceRuntime;
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.util.MacrosFormat;

/**
 * Генератор RADIUS-accounting-start/stop для IPoE/DHCP-схем.
 *
 * sa.radius.fanout.accessRequest.attributes.macros=User-Name=$contractTitle();Acct-Session-Id=$connectionId()-$acctSessionId();Framed-IP-Address=$ip();Calling-Station-Id=$callingStationId()
 * sa.radius.fanout.accessResponse.attributes.macros=User-Name=$contractTitle();Acct-Session-Id=$connectionId()-$acctSessionId();Framed-IP-Address=$ip();Calling-Station-Id=$callingStationId()
 * sa.radius.fanout.accountingStart.attributes.macros=User-Name=$contractTitle();Acct-Session-Id=$connectionId()-$acctSessionId();Acct-Status-Type=1;Framed-IP-Address=$ip();Calling-Station-Id=$callingStationId()
 * sa.radius.fanout.accountingStop.attributes.macros=User-Name=$contractTitle();Acct-Session-Id=$connectionId()-$acctSessionId();Acct-Status-Type=2;Framed-IP-Address=$ip();Calling-Station-Id=$callingStationId();Acct-Terminate-Cause=1
 * sa.radius.fanout.accountingUpdate.attributes.macros=User-Name=$contractTitle();Acct-Session-Id=$connectionId()-$acctSessionId();Acct-Status-Type=3;Framed-IP-Address=$ip();Calling-Station-Id=$callingStationId()
 *
 * Исключить сервисы (например, те, что в dhcp.disable.servId)
 * sa.radius.fanout.skipServIds=
 *
 * @author amir
 */
public class RadiusFanoutServiceActivator
    extends ServiceActivatorAdapter
    implements ServiceActivator, EventListener<Event>
{
    private static final Logger logger = LogManager.getLogger();

    private class RadiusFanoutMacros
    extends MacrosFormat
    {
        private final String patternString;

        public RadiusFanoutMacros( String pattern )
        {
            this.patternString = pattern;
        }

        @Override
        public StringBuffer format( StringBuffer appendable, Object... globalArgs )
        {
            try
            {
                return super.format( appendable, patternString, globalArgs );
            }
            catch ( Exception ex )
            {
                return appendable.append( ex.getMessage() );
            }
        }

        @Override
        protected Object invoke( final String macros, final Object[] args, final Object[] globalArgs )
        {
            final InetServRuntime inetServRuntime = (InetServRuntime)globalArgs[0];
            final InetConnection connection = (InetConnection)globalArgs[1];

            if ( "contractTitle".equals( macros ) )
            {
                String contractTitle = inetServRuntime.contractRuntime.getContractTitle();
                if ( Utils.notBlankString( contractTitle ) )
                {
                    return contractTitle;
                }

                return String.valueOf( inetServRuntime.contractRuntime.contractId );
            }
            else if ( "ip".equals( macros ) )
            {
                byte[] address = connection.getInetAddressBytes();
                if ( address == null )
                {
                    address = inetServRuntime.getInetServ().getAddressFrom();
                }

                return IpAddress.toString( address );
            }
            else if ( "contractId".equals( macros ) )
            {
                return String.valueOf( inetServRuntime.contractRuntime.contractId );
            }
            else if ( "inetServId".equals( macros ) || "servId".equals( macros ) )
            {
                return String.valueOf( inetServRuntime.inetServId );
            }
            else if ( "connectionId".equals( macros ) )
            {
                return connection.getId();
            }
            else if ( "callingStationId".equals( macros ) )
            {
                return connection.getCallingStationId();
            }
            else if ( "acctSessionId".equals( macros ) )
            {
                return connection.getAcctSessionId();
            }
            else if ( "deviceId".equals( macros ) )
            {
                return connection.getDeviceId();
            }
            else if ( "agentDeviceId".equals( macros ) )
            {
                return connection.getAgentDeviceId();
            }
            else if ( "circuitId".equals( macros ) )
            {
                return connection.getCircuitId();
            }

            return connection.getId();
        }
    }

    @Resource( name = "access" )
    private Access access;

    /**
     * Устройство, для которого создан этот ServiceActivator
     * (для одного устройства создается цепочка ServiceActivator'ов).
     */
    @Resource
    private InetDeviceRuntime deviceRuntime;

    /**
     * ID устройства, для которого указан этот ServiceActivator.
     */
//    private int deviceId;

    private Set<Integer> skipServIds;

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

    protected RadiusClient accountingRequestClient;
    protected RadiusClient accountingResponseClient;

    protected RadiusFanoutMacros accessRequestMacros;
    protected RadiusFanoutMacros accessResponseMacros;

    protected RadiusFanoutMacros accountingStartMacros;
    protected RadiusFanoutMacros accountingStopMacros;
    protected RadiusFanoutMacros accountingUpdateMacros;

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

        this.accessRequestMacros = new RadiusFanoutMacros( deviceConfig.get( "sa.radius.fanout.accessRequest.attributes.macros", "" ) );
        this.accessResponseMacros = new RadiusFanoutMacros( deviceConfig.get( "sa.radius.fanout.accessResponse.attributes.macros", "" ) );

        this.accountingStartMacros = new RadiusFanoutMacros( deviceConfig.get( "sa.radius.fanout.accountingStart.attributes.macros",
                                                                               "User-Name=$contractTitle();Acct-Session-Id=$connectionId()-$acctSessionId();Acct-Status-Type=1;Framed-IP-Address=$ip();Calling-Station-Id=$callingStationId()" ) );

        this.accountingStopMacros = new RadiusFanoutMacros( deviceConfig.get( "sa.radius.fanout.accountingStop.attributes.macros",
                                                                              "User-Name=$contractTitle();Acct-Session-Id=$connectionId()-$acctSessionId();Acct-Status-Type=2;Framed-IP-Address=$ip();Calling-Station-Id=$callingStationId();Acct-Terminate-Cause=1" ) );

        this.accountingUpdateMacros = new RadiusFanoutMacros( deviceConfig.get( "sa.radius.fanout.accountingUpdate.attributes.macros", "" ) );

        this.skipServIds = Utils.toIntegerSet( deviceConfig.get( "sa.radius.fanout.skipServIds", null ) );

        this.initClients( device, deviceConfig );

        if ( Utils.notBlankString( accountingUpdateMacros.patternString ) )
        {
            // добавляем слушатель апдейтов
            EventProcessor.getInstance().addListener( this, InetAccountingEvent.class,
                                                      this.access.moduleId, this.access.accountingRootDeviceIdsQuery );

            EventProcessor.getInstance().addListener( this, InetConnectionAliveEvent.class,
                                                      this.access.moduleId, this.access.accountingRootDeviceIdsQuery );
        }

        return null;
    }

    private void initClients( InetDevice device, ParameterMap deviceConfig )
        throws UnknownHostException
    {
        byte[] nasSecret = deviceConfig.get( "sa.radius.fanout.secret",
                                             deviceConfig.get( "sa.radius.secret",
                                                               deviceConfig.get( "radius.secret", device.getSecret() ) ) )
                                       .getBytes();

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

        String nasHost = deviceConfig.get( "sa.radius.fanout.host", host != null ? host[0] : device.getHost() );
        InetAddress nasHostAddr = InetAddress.getByName( nasHost );

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

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

        String sourceHost = deviceConfig.get( "sa.radius.fanout.sourceHost",
                                              deviceConfig.get( "sa.radius.sourceHost", deviceConfig.get( "radius.sourceHost", null ) ) );
        // ServiceActivator'ов создается столько, сколько дочерних устройств, поэтому эта фича будет работать, только если у RadiusFanoutServiceActivator одно
        // дочернее устройство
        int sourcePort = deviceConfig.getInt( "sa.radius.fanout.sourcePort",
                                              deviceConfig.getInt( "sa.radius.sourcePort", deviceConfig.getInt( "radius.sourcePort", -1 ) ) );

        this.accountingRequestClient = new RadiusClient( sourceHost, sourcePort, nasHostAddr, nasPort, nasSecret );
        this.accountingRequestClient.setReuseAddress( true );

        {
            String accessRequestSourceHost = deviceConfig.get( "sa.radius.fanout.accessRequest.sourceHost", null );
            String accessRequestNasHost = deviceConfig.get( "sa.radius.fanout.accessRequest.host", nasHost );
            int accessRequestNasPort = deviceConfig.getInt( "sa.radius.fanout.accessRequest.port", nasPort );

            if ( Utils.notBlankString( accessRequestSourceHost ) )
            {
                this.accessRequestClient = new RadiusClient( accessRequestSourceHost, 
                                                             deviceConfig.getInt( "sa.radius.fanout.accessRequest.sourcePort", 0 ),
                                                             InetAddress.getByName( accessRequestNasHost ),
                                                             accessRequestNasPort, nasSecret );
            }
            else
            {
                this.accessRequestClient = new RadiusClient( sourceHost, 
                                                             sourcePort, 
                                                             InetAddress.getByName( accessRequestNasHost ), 
                                                             accessRequestNasPort, nasSecret );
            }

            this.accessRequestClient.setReuseAddress( true );
        }

        {
            String accessResponseNasHost = deviceConfig.get( "sa.radius.fanout.accessResponse.host", null );
            int accessRequestNasPort = deviceConfig.getInt( "sa.radius.fanout.accessResponse.port", 0 );

            if ( Utils.notBlankString( accessResponseNasHost ) && accessRequestNasPort > 0 )
            {
                this.accessResponseClient = new RadiusClient( deviceConfig.get( "sa.radius.fanout.accessResponse.sourceHost", null ), 
                                                              deviceConfig.getInt( "sa.radius.fanout.accessResponse.sourcePort", 0 ), 
                                                              InetAddress.getByName( accessResponseNasHost ),
                                                              accessRequestNasPort, nasSecret );
                this.accessResponseClient.setReuseAddress( true );
            }
        }

        {
            String accountingResponseNasHost = deviceConfig.get( "sa.radius.fanout.accountingResponse.host", null );
            int accountingResponseNasPort = deviceConfig.getInt( "sa.radius.fanout.accountingResponse.port", 0 );

            if ( Utils.notBlankString( accountingResponseNasHost ) && accountingResponseNasPort > 0 )
            {
                this.accountingResponseClient = new RadiusClient( deviceConfig.get( "sa.radius.fanout.accountingResponse.sourceHost", null ), 
                                                                  deviceConfig.getInt( "sa.radius.fanout.accountingResponse.sourcePort", 0 ),
                                                                  InetAddress.getByName( accountingResponseNasHost ),
                                                                  accountingResponseNasPort, nasSecret );
                this.accountingResponseClient.setReuseAddress( true );
            }
        }
    }

    @Override
    public Object destroy()
        throws Exception
    {
        try
        {
            if ( Utils.notBlankString( accountingUpdateMacros.patternString ) )
            {
                // удаляем слушатель апдейтов
                EventProcessor.getInstance().removeListener( this );
            }
        }
        finally
        {
            if ( accountingRequestClient != null )
            {
                accountingRequestClient.destroy();
            }

            if ( accessRequestClient != null )
            {
                accessRequestClient.destroy();
            }

            if ( accountingResponseClient != null )
            {
                accountingResponseClient.destroy();
            }

            if ( accessResponseClient != null )
            {
                accessResponseClient.destroy();
            }
        }

        return null;
    }

    private void sendPackets( final ServiceActivatorEvent event, final RadiusFanoutMacros requestMacros, final RadiusFanoutMacros responseMacros,
                              final byte code )
        throws InvalidKeyException, SocketException, NoSuchAlgorithmException
    {
        sendPackets( event.getInetServRuntime(), event.getConnection(), requestMacros, responseMacros, code );
    }

    private void sendPackets( final InetServRuntime inetServRuntime, final InetConnection connection, final RadiusFanoutMacros requestMacros,
                              final RadiusFanoutMacros responseMacros, final byte code )
        throws InvalidKeyException, SocketException, NoSuchAlgorithmException
    {
        if ( skipServIds.contains( inetServRuntime.inetServId ) )
        {
            logger.debug( "Skip fanout accounting event by 'sa.radius.fanout.skipServIds' option for servId: " + inetServRuntime.inetServId );
            return;
        }

        String macrosRadiusAttributesString = requestMacros.format( new StringBuffer( 200 ), inetServRuntime, connection ).toString();
        RadiusAttributeSet macrosRadiusAttributes = RadiusAttributeSet.newRadiusAttributeSet( macrosRadiusAttributesString );

        final RadiusClient requestClient;
        final RadiusClient responseClient;

        switch( code )
        {
            case RadiusPacket.ACCESS_REQUEST:
                requestClient = this.accessRequestClient;
                responseClient = this.accessResponseClient;
                break;

            default:
            case RadiusPacket.ACCOUNTING_REQUEST:
                requestClient = this.accountingRequestClient;
                responseClient = this.accountingResponseClient;
                break;
        }

        final RadiusPacket request;

        synchronized( requestClient ) // см. #notify
        {
            request = requestClient.createRequest( code );

            request.addAttributes( macrosRadiusAttributes );

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

            requestClient.sendAsync( request );
        }

        if ( responseClient != null )
        {
            final RadiusPacket response = request.createResponse();
            if ( response.getCode() == RadiusPacket.ACCESS_REJECT )
            {
                response.setCode( RadiusPacket.ACCESS_ACCEPT );
            }

            if ( responseMacros != null && Utils.notBlankString( responseMacros.patternString ) )
            {
                macrosRadiusAttributesString = responseMacros.format( new StringBuffer( 200 ), inetServRuntime, connection ).toString();
                macrosRadiusAttributes = RadiusAttributeSet.newRadiusAttributeSet( macrosRadiusAttributesString );

                response.addAttributes( macrosRadiusAttributes );
            }

            logger.info( "Send fanout response: \n" + response );

            synchronized( responseClient )
            {
                responseClient.sendAsync( response );
            }
        }
    }

    @Override
    public Object onAccountingStart( ServiceActivatorEvent event )
        throws Exception
    {
        logger.info( "Processing onAccountingStart event" );
        if ( Utils.notBlankString( accessRequestMacros.patternString ) )
        {
            sendPackets( event, accessRequestMacros, accessResponseMacros, RadiusPacket.ACCESS_REQUEST );
        }

        sendPackets( event, accountingStartMacros, null, RadiusPacket.ACCOUNTING_REQUEST );

        return null;
    }

    @Override
    public Object onAccountingStop( ServiceActivatorEvent event )
        throws Exception
    {
        logger.info( "Processing onAccountingStart event" );
        sendPackets( event, accountingStopMacros, null, RadiusPacket.ACCOUNTING_REQUEST );

        return null;
    }

    /**
     * Данный метод вызывается вне потока ServiceActivator.
     */
    @Override
    public void notify( final Event e, final EventListenerContext ctx )
    {
        final InetConnection connection;

        if ( e instanceof InetAccountingEvent )
        {
            InetAccountingEvent accountingEvent = (InetAccountingEvent)e;

            // обрабатываем только update'ы
            if ( accountingEvent.getType() != InetAccountingEvent.TYPE_UPDATE )
            {
                return;
            }

            connection = accountingEvent.getConnection();
        }
        else
        {
            InetConnectionAliveEvent connectionAliveEvent = (InetConnectionAliveEvent)e;

            connection = connectionAliveEvent.getConnection();
        }

        // обрабатываем только это устройство
        if ( connection.getDeviceId() != this.deviceRuntime.inetDeviceId )
        {
            return;
        }

        final InetServRuntime inetServRuntime = access.getInetServRuntimeMap().get( connection.getServId() );
        if ( inetServRuntime == null ) // возможно не наше событие
        {
            return;
        }

        try
        {
            sendPackets( inetServRuntime, connection, this.accountingUpdateMacros, null, RadiusPacket.ACCOUNTING_REQUEST );
        }
        catch( InvalidKeyException | SocketException | NoSuchAlgorithmException ex )
        {
            logger.error( ex.getMessage() );
        }
    }
}
