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

import java.util.Collections;
import java.util.HashMap;
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.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.common.BGException;
import ru.bitel.bgbilling.common.model.ConfigParameter;
import ru.bitel.bgbilling.kernel.network.dns.DnsClient;
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.InetServ;
import ru.bitel.bgbilling.modules.inet.server.bean.Keys;
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;

/**
 * Динамический DNS для соединений. Либо всегда привязывает DNS, если есть устройство в дереве с данным ServiceActivator,
 * либо если установлен список опций Inet - dns.inetOptions= - то при наличии активной опции (т.е. в этом случае, например, в тарифе указывается опция Inet
 * при активности определенной тарифной опции - тарифную опцию активировали - DNS привязался, деактивировали - отвязался). 
 * <pre>
    #если пусто - то запись добавляется всегда, если нет - то только если в сервисе активна одна из указанных опций
    #(т.е. возможность включать/выключать DynDNS для сервиса)
    dns.inetOptions=
    #
    dns.zone=
    dns.resolver=
    dns.sharedKey.name=
    dns.sharedKey.value=
    dns.ttl=3600
 * </pre>
 * @author amir
 *
 */
public class DynDnsServiceActivator
	extends ServiceActivatorAdapter
	implements ServiceActivator
{
	private static final Logger logger = LogManager.getLogger();

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

	private DnsClient dnsClient;

	private Set<Integer> deviceTypeIds;
	private Set<Integer> skipDeviceTypeIds;

	/**
	 * Опция или опции при которых привязываем DNS. Если null - значит всегда привязываем (если данный ServiceActivator установлен).
	 */
	private Set<Integer> options;

	@Override
	public Object init( Setup setup, int moduleId, InetDevice device, InetDeviceType deviceType, ParameterMap config )
		throws Exception
	{
		super.init( setup, moduleId, device, deviceType, config );

		dnsClient = new DnsClient( config );

		options = Utils.toIntegerSet( config.get( Keys.SA_DNS_INET_OPTIONS.key, config.get( "dns.inetOptions", null ) ) );
		if( options.size() == 0 )
		{
			options = null;
		}

		deviceTypeIds = Utils.toIntegerSet( config.get( Keys.SA_DNS_DEVICE_TYPE_IDS.key, null ) );
		if( deviceTypeIds.size() == 0 )
		{
			deviceTypeIds = null;
		}

		skipDeviceTypeIds = Utils.toIntegerSet( config.get( Keys.SA_DNS_SKIP_DEVICE_TYPE_IDS.key, null ) );
		if( skipDeviceTypeIds.size() == 0 )
		{
			skipDeviceTypeIds = null;
		}

		return null;
	}

	@Override
	public Object destroy()
		throws Exception
	{
		dnsClient.close();
		dnsClient = null;

		return super.destroy();
	}

	private Map<Integer, String> templateMap = new HashMap<>();

	private MacrosFormat macro = createMacrosFormat();

	private String getHostname( final ServiceActivatorEvent e, final InetConnection connection )
	{
		final InetDeviceRuntime inetDeviceRuntime = access.deviceMap.get( connection.getDeviceId() );

		final int deviceTypeId = inetDeviceRuntime.inetDeviceType.getId();

		if( skipDeviceTypeIds != null && skipDeviceTypeIds.contains( deviceTypeId ) )
		{
			return null;
		}

		if( deviceTypeIds != null && !deviceTypeIds.contains( deviceTypeId ) )
		{
			return null;
		}

		String template = templateMap.computeIfAbsent( connection.getDeviceId(), ( deviceId ) -> {
			return inetDeviceRuntime.config.get( Keys.SA_DNS_HOSTNAME.key, "$login" );
		} );

		InetServ inetServ = e.getNewInetServ();

		String result = macro.format( template, inetServ, connection );

		if( Utils.isBlankString( result ) )
		{
			return null;
		}

		logger.info( "Hostname: " + result );

		return result;
	}

	private void register( final ServiceActivatorEvent e )
	{
		final InetConnection connection = e.getConnection();
		final String hostname = getHostname( e, connection );

		if( hostname != null )
		{
			dnsClient.registerAsync( hostname, IpAddress.toString( connection.getInetAddressBytes() ) );
		}
	}

	private void unregister( final ServiceActivatorEvent e )
	{
		final InetConnection connection = e.getConnection();
		final String hostname = getHostname( e, connection );

		if( hostname != null )
		{
			dnsClient.unregisterAsync( hostname );
		}
	}

	@Override
	public Object onAccountingStart( final ServiceActivatorEvent e )
		throws Exception
	{
		if( options != null && Collections.disjoint( e.getNewOptions(), options ) )
		{
			return null;
		}

		register( e );

		return null;
	}

	@Override
	public Object onAccountingStop( final ServiceActivatorEvent e )
		throws Exception
	{
		if( options != null && Collections.disjoint( e.getNewOptions(), options ) )
		{
			return null;
		}

		unregister( e );

		return null;
	}

	@Override
	public Object connectionModify( final ServiceActivatorEvent e )
		throws Exception
	{
		if( options == null )
		{
			return null;
		}

		if( Collections.disjoint( e.getOldOptions(), options ) )
		{
			if( !Collections.disjoint( e.getNewOptions(), options ) )
			{
				register( e );
			}
		}
		else
		{
			if( Collections.disjoint( e.getNewOptions(), options ) )
			{
				unregister( e );
			}
		}

		return null;
	}

	protected MacrosFormat createMacrosFormat()
	{
		return new MacrosFormat()
		{
			@Override
			protected Object invoke( String macros, Object[] args, Object[] globalArgs )
			{
				try
				{
					return invokeImpl( macros, args, globalArgs );
				}
				catch( BGException e )
				{
					logger.error( e.getMessage(), e );
				}

				return null;
			}

			private Object invokeImpl( String macros, Object[] args, Object[] globalArgs )
				throws BGException
			{
				if( "login".equals( macros ) )
				{
					InetServ inetServ = (InetServ)globalArgs[0];

					String login = inetServ.getLogin();
					if( Utils.isBlankString( login ) )
					{
						return null;
					}

					return login.replace( '@', '-' ).toLowerCase();
				}
				else if( "identifier".equals( macros ) )
				{
					InetServ inetServ = (InetServ)globalArgs[0];

					List<String> identifierList = inetServ.getIdentifierList();
					return identifierList != null && identifierList.size() > 0 ? identifierList.get( 0 ) : null;
				}
				else if( "param".equals( macros ) )
				{
					InetServ inetServ = (InetServ)globalArgs[0];

					InetServRuntime inetServRuntime = access.getInetServRuntimeMap().get( inetServ.getId() );

					return inetServRuntime.getConfig().get( getString( args, 0, "param" ), getString( args, 1, "" ) );
				}
				else
				{
					return null;
				}
			}
		};
	}
	
	@Override
	public List<ConfigParameter> configParameterList()
	{
	    List<ConfigParameter> configParameters = super.configParameterList();
	    configParameters.add( new ConfigParameter( Keys.SA_DNS_INET_OPTIONS.key, "" ) );
	    configParameters.add( new ConfigParameter( Keys.SA_DNS_DEVICE_TYPE_IDS.key, "" ) );
	    configParameters.add( new ConfigParameter( Keys.SA_DNS_SKIP_DEVICE_TYPE_IDS.key, "" ) );
	    configParameters.add( new ConfigParameter( Keys.SA_DNS_HOSTNAME.key, "" ) );
	    return configParameters;
	}
}
