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

import java.net.InetSocketAddress;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

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

import jakarta.annotation.Resource;
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.BGRuntimeException;
import ru.bitel.bgbilling.common.bean.IPUtils;
import ru.bitel.bgbilling.kernel.admin.errorlog.server.AlarmSender;
import ru.bitel.bgbilling.kernel.base.server.DefaultContext;
import ru.bitel.bgbilling.kernel.contract.api.common.util.MACParser;
import ru.bitel.bgbilling.kernel.contract.runtime.ContractRuntime;
import ru.bitel.bgbilling.kernel.contract.runtime.ContractRuntimeMap;
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.common.bean.enums.InetServState;
import ru.bitel.bgbilling.modules.inet.dyn.device.terminal.TerminalSession.MikrotikApiTerminalSession;
import ru.bitel.bgbilling.modules.inet.dyn.device.terminal.TerminalSession.MikrotikTerminalSession;
import ru.bitel.bgbilling.modules.inet.server.runtime.InetInterfaceMap;
import ru.bitel.bgbilling.modules.inet.server.runtime.InetOptionRuntime;
import ru.bitel.bgbilling.modules.inet.server.runtime.InetOptionRuntimeMap;
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.Preferences;
import ru.bitel.common.TimeUtils;
import ru.bitel.common.Utils;
import ru.bitel.common.groovy.GroovyMacro;
import ru.bitel.common.inet.IpAddress;
import ru.bitel.common.inet.IpNet;
import ru.bitel.common.ref.ConcurrentReferenceHashMap;
import ru.bitel.common.ref.ConcurrentWeakHashMap;
import ru.bitel.common.util.StringCache;
import ru.bitel.oss.kernel.entity.common.bean.EntityAttr;
import ru.bitel.oss.systems.inventory.resource.server.ip.dynamic.IpResourceRuntime;

/**
 * sa.terminal.type=telnet|ssh|shell
 * 
 * sa.terminal.serv.modify=
 * sa.terminal.serv.modify.enable=
 * sa.terminal.serv.modify.disable=
 * 
 * @author amir
 */
public class TerminalServiceActivator
	extends ServiceActivatorAdapter
	implements ServiceActivator
{
	private static final Logger logger = LogManager.getLogger();

	private static final StringCache STRING_CACHE = new StringCache( 1000 );

    private static final ConcurrentReferenceHashMap<Integer, Semaphore> SEMAPHORE_MAP = new ConcurrentReferenceHashMap<>(
        ConcurrentReferenceHashMap.ReferenceType.SOFT,
        ConcurrentReferenceHashMap.ReferenceType.STRONG );

	/**
	 * Набор команд, с after и before
	 * @author amir
	 */
	protected static class Commands
	{
		public final String[] commands;
		public final String[] beforeCommands;
		public final String[] afterCommands;

		public Commands( String[] commands, String[] beforeCommands, String[] afterCommands )
		{
			this.commands = commands;
			this.beforeCommands = beforeCommands;
			this.afterCommands = afterCommands;
		}

        @Override
        public String toString()
        {
            return "Commands [commands=" + Arrays.toString( commands ) + ", beforeCommands=" + Arrays.toString( beforeCommands ) + ", afterCommands=" + Arrays.toString( afterCommands ) + "]";
        }
	}

	/**
	 * Группа команд, например null/create/cancel, modify/enable/disable
	 * @author amir
	 */
	protected static class CommandGroup
	{
        public final Commands commands;
		public final Commands enableCommands;
		public final Commands disableCommands;

		public CommandGroup( Commands commands, Commands enableCommands, Commands disableCommands )
		{
			this.commands = commands;
			this.enableCommands = enableCommands;
			this.disableCommands = disableCommands;
		}

        @Override
        public String toString()
        {
            return "CommandGroup [commands=" + commands + ", enableCommands=" + enableCommands + ", disableCommands=" + disableCommands + "]";
        }
	}

	protected static class CommandSet
	{
		protected String prefix;

		/**
		 * create/cancel
		 */
		protected CommandGroup servCommands;

		/**
		 * modify/enable/disable
		 */
		protected CommandGroup servModifyCommands;

		/**
		 * enable/disable
		 */
		protected Map<Integer, CommandGroup> servOptionCommands;

		/**
		 * modify/enable/disable
		 */
		protected CommandGroup connectionModifyCommands;

		/**
		 * close/onAccountingStart/onAccountingStop
		 */
		protected CommandGroup connectionCommands;

		/**
		 * enable/disable
		 */
		protected Map<Integer, CommandGroup> connectionOptionCommands;

        @Override
        public String toString()
        {
            return "CommandSet [prefix=" + prefix + ", servCommands=" + servCommands + ", servModifyCommands=" + servModifyCommands + ", servOptionCommands=" + servOptionCommands + ", connectionModifyCommands=" + connectionModifyCommands + ", connectionCommands=" + connectionCommands + ", connectionOptionCommands=" + connectionOptionCommands + "]";
        }
		
		
	}

	private static Preferences defaults = new Preferences();
	static
	{
		defaults.set( "serv.create.after", "${(newState()==1)?(serviceEnable()+optionsEnable()):serviceDisable()}" );

		defaults.set( "serv.modify.before", "${(newState()==1&&oldState()!=1)?(serviceEnable()+optionsEnable()):''}" );
		defaults.set( "serv.modify.after", "${(newState()!=1)?(optionsDisable()+serviceDisable()):''};"
										   + "${(newState()==1&&oldState()==1)?(optionsSwitch()):''}" );

		defaults.set( "serv.cancel.before", "${optionsDisable()+serviceDisable()}" );

		defaults.set( "connection.modify.before", "${(newState()==1&&oldState()!=1)?(connectionEnable()+optionsEnable()):''}" );
        defaults.set( "connection.modify.after", "${(newState()!=1)?(optionsDisable()+connectionDisable()):''};"
                                                 + "${(newState()==1&&oldState()==1)?(optionsSwitch()):''}" );
		
		defaults.set( "connection.onAccountingStart.before", "${(newState()!=1)?(connectionDisable()):(connectionEnable())}" );
		defaults.set( "connection.onAccountingStart.after", "${(newState()==1)?(optionsEnable()):''}" );
		
		defaults.set( "connection.onAccountingStop.before", "${(oldState()==1)?(optionsDisable()):''}" );

		////
		/*
				defaults.set( "sa.terminal.serv.create.after", "${(newState()==1)?$(':serv.modify.enable'):$(':serv.modify.disable')}" );
		
				defaults.set( "sa.terminal.serv.modify.before", "${(newState()==1)?$(':serv.modify.enable'):''}" );
				defaults.set( "sa.terminal.serv.modify.after", "${(newState()==1)?'':$(':serv.modify.disable')}" );
		
				defaults.set( "sa.terminal.connection.modify.before", "${(newState()==1)?$(':connection.modify.enable'):''}" );
				defaults.set( "sa.terminal.connection.modify.after", "${(newState()==1)?'':$(':connection.modify.disable')}" );
				*/
	}

	/**
	 * Мап опций Inet.
	 */
	protected InetOptionRuntimeMap optionRuntimeMap;

	/**
	 * Фильтр опций Inet, с котороми происходит работа.
	 */
	protected Set<Integer> workingOptions;

	/**
	 * Нужно ли после смены состояния соединения сразу менять состояние в базе.
	 */
	protected boolean needConnectionStateModify;
	
	/**
	 * Нужно ли вызывать cancel/create при обнаружении изменений iface/vlan или дочерних сервисов:<br>
	 * 0 - не нужно,<br>
	 * 1 - нужно только если поменялся iface/vlan на родительском,<br>
	 * 2 - нужно, если поменялся iface/vlan на родительском или изменились дочерние сервисы.<br>
	 */
	protected int needCancelCreate;

	/**
	 * Не обрабатываем onAccountingStart/Stop для дочерних/сервисных сессий.
	 */
	protected boolean skipServiceAccountingEvents;

	protected Set<Integer> accountingEventDeviceIds;

	/**
	 * Ограничение по кол-ву одновременно подключенных к устройству SA.
	 */
	protected int connectionSemaphorePermits = 0;

	protected Semaphore connectionSemaphore;

	protected boolean connectionSemaphoreAcquired;

	final class TGroovyMacro
		extends GroovyMacro
	{
		public TGroovyMacro()
		{
			super( true );
		}

		@Override
		public Object[] getGlobalArgs()
		{
			return super.getGlobalArgs();
		}

		@Override
		protected Object invoke( String name, Object[] args )
		{
			try
			{
				return TerminalServiceActivator.this.invoke( this, name, args );
			}
			catch( Exception e )
			{
				throw new BGRuntimeException( e );
			}
		}
	}

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

	protected final GroovyMacro macro = new TGroovyMacro();

	protected int moduleId;

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

	/**
	 * Устройство.
	 */
	protected InetDevice device;

	/**
	 * Конфигурация устройства.
	 */
	protected ParameterMap config;

	protected String host;
	protected int port;

	protected TerminalSession terminalSession;

	protected String endSequence;
	
	private boolean skipCommand;

	protected boolean lazyConnect;

	private static final ConcurrentWeakHashMap<ParameterMap, CommandConfig> COMMAND_CONFIG_MAP = new ConcurrentWeakHashMap<ParameterMap, CommandConfig>();

	public static class CommandConfig
	{
		/**
		 * Команды, выполняемые при подключении к терминалу.
		 */
		protected String[] connectCommands;
		/**
		 * Команды, выполняемые перед отключением от терминала.
		 */
		protected String[] disconnectCommands;

		/**
		 * Команда выхода (отключения от терминала).
		 */
		protected String exitCommand;

		/**
		 * Набор команд по умолчанию.
		 */
		protected CommandSet commandSet;

		protected Map<String, CommandSet> commandSetMap;

		protected Map<Integer, CommandSet> servTypeCommandSetMap;
	}

	private CommandConfig commandConfig;
	
    /**
     * Последний ответ на команду.
     */
    private String lastResponse = "";

    /**
     * Переменные.
     */
    private Map<String, Object> variables = new HashMap<>();

    /**
     * Если результат (ответ) выполнения команды подходит под данный шаблон, то отправляется alarm.
     */
    private Pattern alarmPattern = null;
    private Pattern errorPattern = null;

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

		this.device = device;
		this.deviceId = device.getId();
		this.config = config;

		initWorkingOptions( moduleId, config );
		
		String type = config.get( "sa.terminal.protocol", "telnet" );

		initHostAndPort( device, config, type );

		String username = device.getUsername();
		String password = device.getPassword();

		username = config.get( "sa.terminal.username", config.get( "sa.username", device.getUsername() ) );
		password = config.get( "sa.terminal.password", config.get( "sa.password", device.getPassword() ) );

		this.terminalSession = TerminalSession.newTerminalSession( type, host, port, username, password );
		this.terminalSession.setSourceHost( config.get( "sa.terminal.sourceHost", null ) );
		this.terminalSession.setSourcePort( config.getInt( "sa.terminal.sourcePort", 0 ) );
		this.terminalSession.setConfiguration( config.sub( "sa.terminal." )  );
		this.endSequence = config.get( "sa.terminal.endSequence", config.get( "sa.endSequence", null ) );
		this.lazyConnect = config.getInt( "sa.terminal.lazyConnect", config.getInt( "sa.lazyConnect", 1 ) ) > 0;

        final ParameterMap deviceConfig = new Preferences( deviceType.getConfig(), "\n" )
            .inherit( new Preferences( device.getInvConfig(), "\n" ) )
            .inherit( new Preferences( device.getConfig(), "\n" ) );

		this.commandConfig = parseCommandConfig( deviceConfig );

		this.needConnectionStateModify = config.getInt( "sa.terminal.connection.stateModify", 1 ) > 0;

		this.skipServiceAccountingEvents = config.getInt( "sa.terminal.connection.skipServiceAccounting", 1 ) > 0;

		this.accountingEventDeviceIds = Utils.toIntegerSet( config.get( "sa.terminal.connection.deviceIds", null ) );
		if ( this.accountingEventDeviceIds.size() == 0 )
		{
		    this.accountingEventDeviceIds = null;
		}

		this.connectionSemaphorePermits = deviceConfig.getInt( "sa.terminal.semaphorePermits", 0 );
		this.connectionSemaphore = getConnectionSemaphore();
		
        this.needCancelCreate = deviceConfig.getInt( "sa.terminal.serv.modify.needCancelCreate", 2 );
        
        this.errorPattern = Optional.ofNullable( deviceConfig.get( "sa.terminal.error.pattern" ) )
            .filter( pattern -> Utils.notBlankString( pattern ) )
            .map( pattern -> Pattern.compile( pattern ) )
            .orElse( null );

        this.alarmPattern = Optional.ofNullable( deviceConfig.get( "sa.terminal.alarm.pattern" ) )
            .filter( pattern -> Utils.notBlankString( pattern ) )
            .map( pattern -> Pattern.compile( pattern ) )
            .orElse( this.errorPattern );

        return null;
	}

	private void initWorkingOptions( final int moduleId, final ParameterMap config )
		throws Exception
	{
        if ( this.optionRuntimeMap == null )
        {
            this.optionRuntimeMap = InetOptionRuntimeMap.getInstance( moduleId );
        }

		Set<Integer> rootOptions = Utils.toIntegerSet( config.get( "sa.inetOption.root", null ) );
		this.workingOptions = new HashSet<Integer>();

		for ( Integer rootOption : rootOptions )
		{
			InetOptionRuntime rootOptionRuntime = InetOptionRuntimeMap.getInstance( moduleId ).get( rootOption );
			if ( rootOptionRuntime != null )
			{
				this.workingOptions.addAll( rootOptionRuntime.descendantIds );
			}
		}

		workingOptions.remove( 0 );

		if ( this.workingOptions.size() == 0 )
		{
			this.workingOptions = null;
		}
	}

	private void initHostAndPort( final InetDevice device, final ParameterMap config, final String type )
	{
		List<InetSocketAddress> hosts = device.getHosts();
		if ( hosts.size() > 0 )
		{
			InetSocketAddress socketAddress = hosts.get( 0 );
			this.host = socketAddress.getHostString();
			this.port = socketAddress.getPort();
		}

		this.host = config.get( "sa.terminal.host", config.get( "sa.host", this.host ) );
		this.port = config.getInt( "sa.terminal.port", config.getInt( "sa.port", this.port ) );
	}

	private Semaphore getConnectionSemaphore()
	{
		if ( connectionSemaphorePermits <= 0 )
		{
			return null;
		}

		Semaphore result = SEMAPHORE_MAP.get( deviceId );
		if ( result != null )
		{
			return result;
		}

		final Semaphore newResult = new Semaphore( connectionSemaphorePermits, true );
		result = SEMAPHORE_MAP.putIfAbsent( deviceId, newResult );
		if ( result == null )
		{
			result = newResult;
		}

		return result;
	}

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

		if ( terminalSession != null )
		{
			if ( terminalSession.isConnected() )
			{
				terminalSession.close();
			}

			terminalSession = null;
		}

		return null;
	}

	protected static final Pattern semicolonPattern = Pattern.compile( "\\s*;\\s*" );

	public static List<String> parseCommandArray( final ParameterMap config, final String prefix )
	{
		List<String> list = new ArrayList<String>();

		String param = config.get( prefix );
		// команды заведены через точку с запятой
		if ( Utils.notBlankString( param ) )
		{
			for ( String command : semicolonPattern.split( param ) )
			{
				if ( Utils.notBlankString( command ) )
				{
					list.add( STRING_CACHE.intern( command ) );
				}
			}
		}
		// команды заведены отдельными параметрами 	
		else
		{
			for ( Map.Entry<Integer, ParameterMap> e : config.subIndexed( prefix + "." ).entrySet() )
			{
				if ( e.getKey() == null || e.getKey() <= 0 )
				{
					continue;
				}

				ParameterMap params = e.getValue();

				String command = params.get( "", null );
				if ( Utils.notBlankString( command ) )
				{
					list.add( STRING_CACHE.intern( command ) );
				}
			}
		}

		return list;
	}

	private String[] parseCommandArray0( final ParameterMap config, final String prefix, final boolean addPrefix, final String command )
	{
		List<String> list = parseCommandArray( config, addPrefix ? (prefix + command) : command );

		if ( list.size() == 0 && config != defaults )
		{
			list = parseCommandArray( defaults, command );
		}

		if ( list.size() > 0 )
		{
			logger.info( prefix + command + " commands: " + list );
		}
		else
		{
			logger.trace( prefix + command + " commands: " + list );
		}

		return list.toArray( new String[list.size()] );
	}

	protected Commands parseCommands( ParameterMap config, String prefix, final boolean addPrefix, String command )
	{
		String[] commandsBefore = parseCommandArray0( config, prefix, addPrefix, command + ".before" );
		String[] commands = parseCommandArray0( config, prefix, addPrefix, command );
		String[] commandsAfter = parseCommandArray0( config, prefix, addPrefix, command + ".after" );

		return new Commands( commands, commandsBefore, commandsAfter );
	}

	protected Map<Integer, CommandGroup> parseOptionCommandGroup( ParameterMap config, String prefix )
	{
		Map<Integer, CommandGroup> result = new HashMap<>();

		for ( Map.Entry<Integer, ParameterMap> e : config.subIndexed( prefix ).entrySet() )
		{
			final Integer option = e.getKey();

			if ( option <= 0 )
			{
				continue;
			}

			final ParameterMap params = e.getValue();

			final String prefix2 = prefix + option + ".";

			Commands enableCommands = parseCommands( params, prefix2, false, "enable" );
			Commands disableCommands = parseCommands( params, prefix2, false, "disable" );

			if ( enableCommands != null || disableCommands != null )
			{
				result.put( option, new CommandGroup( null, enableCommands, disableCommands ) );
			}
		}

		// наследуем для дочерних опций те же макросы

		Map<Integer, CommandGroup> ancestorMap = result;
		result = new HashMap<Integer, CommandGroup>( ancestorMap );

		for ( Map.Entry<Integer, CommandGroup> entry : ancestorMap.entrySet() )
		{
			Integer optionId = entry.getKey();
			CommandGroup commands = entry.getValue();
			InetOptionRuntime optioRuntime = optionRuntimeMap.get( optionId );

            if ( optioRuntime == null )
            {
                logger.error( "Found configuration for option " + optionId + " but option with this ID not found" );
                continue;
            }

			for ( Integer o : optioRuntime.descendantIds )
			{
				result.putIfAbsent( o, commands );
			}
		}

		return result;
	}

	protected CommandSet parseCommandSet( ParameterMap config, String prefix )
	{
		CommandSet result = new CommandSet();

		result.prefix = prefix;

		Commands servCreate = parseCommands( config, prefix, true, "serv.create" );
		Commands servCancel = parseCommands( config, prefix, true, "serv.cancel" );

		result.servCommands = new CommandGroup( null, servCreate, servCancel );

		Commands servModify = parseCommands( config, prefix, true, "serv.modify" );
		Commands servEnable = parseCommands( config, prefix, true, "serv.modify.enable" );
		Commands servDisable = parseCommands( config, prefix, true, "serv.modify.disable" );

		result.servModifyCommands = new CommandGroup( servModify, servEnable, servDisable );

		result.servOptionCommands = parseOptionCommandGroup( config, prefix + "serv.inetOption." );

		Commands connectionModify = parseCommands( config, prefix, true, "connection.modify" );
		Commands connectionEnable = parseCommands( config, prefix, true, "connection.modify.enable" );
		Commands connectionDisable = parseCommands( config, prefix, true, "connection.modify.disable" );

		result.connectionModifyCommands = new CommandGroup( connectionModify, connectionEnable, connectionDisable );

		Commands connectionClose = parseCommands( config, prefix, true, "connection.close" );
		Commands connectionStart = parseCommands( config, prefix, true, "connection.onAccountingStart" );
		Commands connectionStop = parseCommands( config, prefix, true, "connection.onAccountingStop" );

		result.connectionCommands = new CommandGroup( connectionClose, connectionStart, connectionStop );

		result.connectionOptionCommands = parseOptionCommandGroup( config, prefix + "connection.inetOption." );

		return result;
	}

	protected CommandConfig parseCommandConfig( ParameterMap config )
	{
		CommandConfig commandConfig = COMMAND_CONFIG_MAP.get( config );

		if ( commandConfig == null )
		{
			commandConfig = new CommandConfig();

			commandConfig.exitCommand = config.get( "sa.terminal.exit", "exit" );

			commandConfig.connectCommands = parseCommandArray0( config, "", false, "sa.terminal.connect" );
			commandConfig.disconnectCommands = parseCommandArray0( config, "", false, "sa.terminal.disconnect" );

			commandConfig.commandSet = parseCommandSet( config, "sa.terminal." );

			commandConfig.commandSetMap = new HashMap<String, CommandSet>();
			commandConfig.servTypeCommandSetMap = new HashMap<Integer, CommandSet>();

			for ( Map.Entry<String, ParameterMap> e : config.subKeyed( "sa.terminal.set." ).entrySet() )
			{
				logger.info( "command set " + e.getKey() + ":" );

				CommandSet commandSet = parseCommandSet( e.getValue(), "" );
				commandConfig.commandSetMap.put( e.getKey(), commandSet );

				Set<Integer> servTypeIds = Utils.toIntegerSet( e.getValue().get( "servTypeIds", "" ) );
				for ( Integer servTypeId : servTypeIds )
				{
					commandConfig.servTypeCommandSetMap.put( servTypeId, commandSet );
				}
			}

			COMMAND_CONFIG_MAP.put( config, commandConfig );
		}

		return commandConfig;
	}

	protected void executeCommands( final ServiceActivatorEvent e, final InetServ serv, final InetConnection connection, final Set<Integer> options, final CommandSet commandSet, final Commands commands )
		throws Exception
	{
		if ( commands == null )
		{
			return;
		}

		executeCommands( e, serv, connection, options, commandSet, commands.beforeCommands );
		executeCommands( e, serv, connection, options, commandSet, commands.commands );
		executeCommands( e, serv, connection, options, commandSet, commands.afterCommands );
	}

	protected void executeCommands( final ServiceActivatorEvent serviceActivatorEvent, final InetServ inetServ, 
	                                final InetConnection connection, Set<Integer> options, final CommandSet commandSet, final String[] commands )
		throws Exception
	{
		if ( commands == null || commands.length == 0 )
		{
			return;
		}

		if ( options != null && this.workingOptions != null )
		{
			options = new HashSet<Integer>( options );
			options.retainAll( this.workingOptions );
		}

		try
		{
			for ( String command : commands )
			{
                skipCommand = false;

                command = String.valueOf( this.macro.eval( command, serviceActivatorEvent, inetServ, connection, options, commandSet ) );
                if ( Utils.notBlankString( command ) && !"null".equals( command ) )
                {
                    if ( skipCommand )
                    {
                        logger.info( "Skipping command: " + command );
                    }
                    else
                    {
                        command = command.trim();
                        if ( command.equals( "\\n" ) )
                        {
                            command = "\n";
                        }

                        executeCommand( command );
                    }
                }
			}
		}
		catch( BGRuntimeException ex )
		{
			if ( ex.getCause() != null && ex.getCause() instanceof Exception )
			{
				throw (Exception)ex.getCause();
			}

			throw ex;
		}

		return;
	}

	/**
	 * Если результат (ответ) выполнения команды подходит под данный шаблон, то отправляется alarm.
	 */
	private static Pattern returnCodePattern = Pattern.compile( "RETURN_CODE=(\\d+)" );

	protected void executeCommand( String command )
		throws Exception
	{
		final TerminalSession session = getTerminalSession();

		logger.info( "[" + device + "] execute: " + command );

		String result;
		try
		{
			result = session.sendCommand( command );
			
			this.lastResponse = result;
		}
		catch( TimeoutException ex )
		{
			logger.error( "Timeout waiting command prompt (endSequence) when executing command." );
			throw ex;
		}

		logger.info( result );

		checkResult( command, result );
	}

	/**
	 * Обработка результата выполнения команды
	 * @param command
	 * @param result
	 * @throws BGException 
	 */
    private void checkResult( final String command, final String result )
        throws BGException
    {
		if ( Utils.isBlankString( result ) )
		{
			return;
		}

		Matcher m = returnCodePattern.matcher( result );
		while( m.find() )
		{
			String returnCode = m.group( 1 );

			if ( Utils.parseInt( returnCode, 0 ) == 0 )
			{
				continue;
			}

			logger.error( "Command \"" + command + "\" executed with RETURN_CODE=" + returnCode );

			String key = "inet.sa.terminal.returnCode";

				String subject = "Inet: Ошибка работы обработчика активации сервисов [" + deviceId + "] " + device.getGuiTitle();
				String message = "Ошибка работы обработчика активации сервисов [" + deviceId + "] " + device.getGuiTitle()
								 + "\n" + device.getHost()
								 + "\n(" + device.getComment() + ")\n\n"
								 + "Исполнение команды \"" + command + "\" завершилось с ошибкой " + returnCode + "!";

				AlarmSender.sendAlarm( key, 600000, subject, message );
		}
		
        if ( alarmPattern != null )
        {
            m = alarmPattern.matcher( result );
            if ( m.find() )
            {
                logger.error( "Device:" + deviceId + " command \"" + command + "\" executed with error (alarm.pattern):\n" + result );

                String key = "inet.sa.terminal.alarm";
                    String subject = "Inet: Ошибка работы обработчика активации сервисов [" + deviceId + "] " + device.getGuiTitle();
                    String message = "Ошибка работы обработчика активации сервисов [" + deviceId + "] " + device.getGuiTitle()
                        + "\n" + device.getHost()
                        + "\n(" + device.getComment() + ")\n\n"
                        + "Исполнение команды \"" + command + "\" завершилось с ошибкой:\n\n" + result;

                    AlarmSender.sendAlarm( key, 600000, subject, message );
            }
        }
		
        if ( errorPattern != null )
        {
            m = errorPattern.matcher( result );
            if ( m.find() )
            {
                throw new BGException( "sa.terminal.error.pattern", "sa.terminal.alarm.pattern" );
            }
        }
    }

	protected String getString( Object[] args, int i, String def )
	{
		if ( args.length <= i )
		{
			return def;
		}

		Object o = args[i];
		if ( o == null )
		{
			return def;
		}

		return o.toString();
	}

	protected int getInt( Object[] args, int i, int def )
	{
		if ( args.length <= i )
		{
			return def;
		}

		Object o = args[i];
		if ( o == null )
		{
			return def;
		}

		if ( o instanceof String )
		{
			return Utils.parseInt( (String)o, def );
		}

		if ( o instanceof Number )
		{
			return ((Integer)o).intValue();
		}

		return def;
	}

	protected double getDouble( Object[] args, int i, double def )
	{
		if ( args.length >= i )
		{
			return def;
		}

		Object o = args[i];
		if ( o == null )
		{
			return def;
		}

		if ( o instanceof String )
		{
			return Utils.parseDouble( (String)o, def );
		}

		if ( o instanceof Number )
		{
			return ((Integer)o).doubleValue();
		}

		return def;
	}

	private IpResourceRuntime getIpResource( final InetServ inetServ, final InetConnection inetConnection )
		throws BGException
	{
        int ipResourceId;
        if ( logger.isDebugEnabled() )
        {
            logger.debug( "getIpResource:inetServ = " + inetServ );
            logger.debug( "getIpResource:inetConnection = " + inetServ );
        }
        if ( inetConnection != null )
        {
            ipResourceId = inetConnection.getIpResourceId();
            if ( logger.isDebugEnabled() )
            {
                logger.debug( "getIpResource:inetConnection.getIpResourceId() = " + ipResourceId );
            }
            if ( ipResourceId <= 0 )
            {
                ipResourceId = inetServ.getIpResourceId();
                if ( logger.isDebugEnabled() )
                {
                    logger.debug( "getIpResource:inetServ.getIpResourceId() = " + ipResourceId );
                }
            }
        }
        else
        {
            ipResourceId = inetServ.getIpResourceId();
            if ( logger.isDebugEnabled() )
            {
                logger.debug( "getIpResource:inetServ.getIpResourceId() = " + ipResourceId );
            }
        }
        

        if ( ipResourceId > 0 )
        {
            if ( logger.isDebugEnabled() )
            {
                logger.debug( "getIpResource:access.ipResourceManager.getResource( " + ipResourceId + " ) = " + access.ipResourceManager.getResource( ipResourceId ) );
            }
            return access.ipResourceManager.getResource( ipResourceId );
        }

        return null;
	}

	private EntityAttr getDeviceAttribute( final int deviceId, final int entitySpecAttrId )
	{
		final InetDeviceRuntime deviceRuntime = access.deviceMap.get( deviceId );
		if ( deviceRuntime == null )
		{
			logger.warn( "Device not found with id=" + deviceId );
			return null;
		}

		final InetDevice device = deviceRuntime.inetDevice;

		return device.getEntityAttributes().get( entitySpecAttrId );
	}

	private String getDeviceIP( final InetDevice device )
	{
		final List<InetSocketAddress> list = device.getHosts();
		if ( list.size() > 0 )
		{
			return IpAddress.toString( list.get( 0 ).getAddress().getAddress() );
		}

		return "null";
	}

	private String getDeviceIP( final int deviceId )
	{
		final InetDeviceRuntime deviceRuntime = access.deviceMap.get( deviceId );
		if ( deviceRuntime == null )
		{
			logger.warn( "Device not found with id=" + deviceId );
			return "null";
		}

		final InetDevice device = deviceRuntime.inetDevice;
		return getDeviceIP( device );
	}

	private String getDeviceIdentifier( final int deviceId )
	{
		final InetDeviceRuntime deviceRuntime = access.deviceMap.get( deviceId );
		if ( deviceRuntime == null )
		{
			logger.warn( "Device not found with id=" + deviceId );
			return "null";
		}

		final InetDevice device = deviceRuntime.inetDevice;

		return device.getIdentifier();
	}
	
    private String getDeviceSvlan( final int deviceId )
    {
        final InetDeviceRuntime deviceRuntime = access.deviceMap.get( deviceId );
        if ( deviceRuntime == null )
        {
            logger.warn( "Device not found with id=" + deviceId );
            return "";
        }

        int svlan = deviceRuntime.getSvlan();
        if ( svlan >= 0 )
        {
            return String.valueOf( svlan );
        }

        return "";
    }

	protected Object invoke( final TGroovyMacro macro, final String name, Object[] args )
		throws Exception
	{
	    if ( logger.isDebugEnabled() )
	    {
	        logger.debug( "name = {}", name );
            if ( args != null )
            {
                for ( int index = 0, len = args.length; index < len; index++ )
                {
                    logger.debug( "args[" + index + "]=" + args[index] );
                }
            }
	    }

	    switch( name )
		{
			case "$":
			{
				final String paramName = (String)args[0];
				final String macroString;
				final Object[] globalArgs = macro.getGlobalArgs();

				if ( paramName.startsWith( ":" ) )
				{
					final String commandPrefix = (String)globalArgs[4];
					macroString = this.config.get( commandPrefix + paramName.substring( 1 ) );
				}
				else
				{
					macroString = this.config.get( paramName );
				}

				try
				{
				    if ( logger.isDebugEnabled() )
				    {
				        logger.debug( "macroString={}, globalArgs={}", macroString, Arrays.toString( globalArgs ) );
				    }
				    return macro.eval( macroString, globalArgs );
				}
                catch( Exception ex )
                {
                    logger.error( "macroString={}, globalArgs={}", macroString, Arrays.toString( globalArgs ) );
                    throw ex;
                }
			}

			case "arg":
			{
//				Object val = args[0];
//				int i;
//
//				if ( val instanceof Number )
//				{
//					i = ((Number)val).intValue();
//				}
//				else
//				{
//					i = Utils.parseInt( String.valueOf( val ), 0 );
//				}

				return macro.getGlobalArgs();
			}

			case "param":
			{
				if ( args.length > 2 )
				{
					Object o = args[0];
					if ( o instanceof InetOptionRuntime )
					{
						return ((InetOptionRuntime)o).inheritedConfig.get( getString( args, 1, "param" ), getString( args, 2, "" ) );
					}
					else
					{
						return getString( args, 2, "" );
					}
				}
				else
				{
					return this.config.get( getString( args, 0, "param" ), getString( args, 1, "" ) );
				}
			}
			
			case "optionParam":
			{
				if ( args.length > 2 )
				{
					Object o = args[0];
					if ( o instanceof InetOptionRuntime )
					{
						return ((InetOptionRuntime)o).inheritedConfig.get( getString( args, 1, "param" ), getString( args, 2, "" ) );
					}
					else
					{
						return getString( args, 2, "" );
					}
				}
				else
				{
					return "";
				}
			}

			case "servParam":
			{
				final InetServ serv = (InetServ)macro.getGlobalArgs()[1];
				final String configString = serv.getConfig();

				if ( Utils.isBlankString( configString ) )
				{
					return getString( args, 1, "" );
				}

				ParameterMap config = new Preferences( configString, "\n" );
				String key = getString( args, 0, "" );
				String result = config.get( key, null );
				if ( result == null )
				{
					result = config.get( key + ".1", null );
					if ( result == null )
					{
						result = getString( args, 1, "" );
					}
				}

				return result;
			}

			case "host":
			{
				List<InetSocketAddress> addressList = this.device.getHosts();

				if ( addressList.size() > 0 )
				{
					InetSocketAddress socketAddress = addressList.get( 0 );
					return socketAddress.getAddress().getHostAddress();
				}
				else
				{
					return null;
				}
			}

			case "option":
			{
				Set<Integer> options = (Set<Integer>)macro.getGlobalArgs()[3];

				if ( args.length > 0 )
				{
					int rootOption = getInt( args, 0, 0 );
					if ( rootOption > 0 )
					{
						InetOptionRuntime rootOptionRuntime = optionRuntimeMap.get( rootOption );
						for ( Integer option : options )
						{
							if ( rootOptionRuntime.descendantIds.contains( option ) )
							{
								return optionRuntimeMap.get( option );
							}
						}

						return null;
					}
				}

				for ( Integer option : options )
				{
					return optionRuntimeMap.get( option );
				}

				return null;
			}
			
            case "empty":
            {
                if ( args.length > 0 )
                {
                    for ( Object o : args )
                    {
                        if ( o != null && Utils.notBlankString( String.valueOf( o ) ) )
                        {
                            return false;
                        }
                    }

                    return true;
                }

                return true;
            }

			case "switch":
			{
				int value = getInt( args, 0, 0 );
				boolean def = args.length % 2 == 0;
				int max = def ? args.length - 1 : args.length;

				for ( int i = 1; i < max; i = i + 2 )
				{
					if ( value == getInt( args, i, 0 ) )
					{
						return args[i + 1];
					}
				}

				// default
				if ( args.length % 2 == 0 )
				{
					return args[args.length - 1];
				}

				return null;
			}

			case "translit":
			{
				if ( args.length > 0 )
				{
					return Utils.toTranslit( String.valueOf( args[0] ) );
				}

				return "null";
			}

			case "nowString":
			case "dateNow":
			{
				return TimeUtils.format( new Date(), "dd.MM.yyyy HH:mm:ss" );
			}

			case "format":
			{
				String pattern = getString( args, 0, "" );
				args = Arrays.copyOfRange( args, 1, args.length );
				return MessageFormat.format( pattern, args );
			}

			case "now":
			{
				if ( args.length > 0 )
				{
					String pattern = getString( args, 0, "dd.MM.yyyy HH:mm:ss" );
					return TimeUtils.format( new Date(), pattern );
				}
				else
				{
					return new Date();
				}
			}

			case "setEndSequence":
			{
				if ( args.length > 0 )
				{
					String endSequence = (String)args[0];
					if ( Utils.notEmptyString( endSequence ) )
					{
						getTerminalSession().setEndSequence( endSequence );
						return "";
					}
				}

				getTerminalSession().setEndSequence( this.endSequence );
				return "";
			}
			
            case "skipIf":
            {
                if ( args.length > 0 )
                {
                    for ( Object o : args )
                    {
                        if ( o instanceof Boolean )
                        {
                            if ( Boolean.FALSE.equals( o ) )
                            {
                                return "";
                            }
                        }
                        else
                        {
                            if ( !Utils.parseBoolean( String.valueOf( o ), false ) )
                            {
                                return "";
                            }
                        }
                    }

                    skipCommand = true;
                }

                return "";
            }
            
            case "skipIfEmpty":
            {
                if ( args.length > 0 )
                {
                    for ( Object o : args )
                    {
                        if ( o != null && !Utils.isEmptyString( String.valueOf( o ) ) )
                        {
                            return "";
                        }
                    }

                    skipCommand = true;
                }

                return "";
            }
            
            case "var":
            {
                switch( args.length )
                {
                    case 0:
                    {
                    }

                    case 1:
                    {
                        return variables.get( args[0] );
                    }

                    case 2:
                    default:
                    {
                        variables.put( (String)args[0], args[1] );
                    }
                }
                
                return "";
            }
            
            case "lastResponse":
            {
                if ( args.length > 0 )
                {
                    Pattern pattern = Pattern.compile( args[0].toString(), Pattern.MULTILINE | Pattern.DOTALL );
                    Matcher matcher = pattern.matcher( this.lastResponse );
                    if ( matcher.find() )
                    {
                        if ( matcher.groupCount() > 0 )
                        {
                            return matcher.group( 1 );
                        }
                        else
                        {
                            return matcher.group();
                        }
                    }
                    else if ( args.length > 1 ) // значение по умолчанию
                    {
                        return args[1];
                    }

                    return "";
                }

                // ${var('svlan', lastResponse().replaceAll( /^8(.+)$/,'$1'))}
                return this.lastResponse;
            }

			case "mikrotikLastIds":
			{
                String lastIds;

                if ( terminalSession instanceof MikrotikApiTerminalSession )
                {
                    lastIds = ((MikrotikApiTerminalSession)terminalSession).getLastIds();
                }
                else if ( terminalSession instanceof MikrotikTerminalSession )
                {
                    lastIds = ((MikrotikTerminalSession)terminalSession).getLastIds();
                }
                else
                {
                    if ( args.length == 0 )
                    {
                        lastIds = "error_non_mikrotik_session";
                    }
                    else
                    {
                        lastIds = null;
                    }
                }

                if ( Utils.isBlankString( lastIds ) && args.length > 0 )
                {
                    lastIds = String.valueOf( args[0] );
                }

                return lastIds;
			}

			default:
			{
			    final Object[] globalArgs = macro.getGlobalArgs();
				return invoke( (ServiceActivatorEvent)globalArgs[0], (InetServ)globalArgs[1], (InetConnection)globalArgs[2], (Set<Integer>)globalArgs[3], name, args, globalArgs );
			}
		}
	}

	protected Object invoke( ServiceActivatorEvent e, InetServ serv, InetConnection connection, Set<Integer> options, String name, Object[] args, Object[] globalArgs )
		throws Exception
	{
		switch( name )
		{
			case "optionsEnable":
			{
				logger.info( "optionsEnable" );
				
				final CommandSet commandSet = (CommandSet)globalArgs[4];
				if ( connection != null )
				{
					optionsSwitch( commandSet, commandSet.connectionOptionCommands, e, serv, connection, null, e.getNewOptions() );
				}
				else
				{
					optionsSwitch( commandSet, commandSet.servOptionCommands, e, serv, null, null, e.getNewOptions() );
				}

				return "";
			}

			case "optionsDisable":
			{
				logger.info( "optionsDisable" );
				
				final CommandSet commandSet = (CommandSet)globalArgs[4];
				if ( connection != null )
				{
					optionsSwitch( commandSet, commandSet.connectionOptionCommands, e, serv, connection, e.getOldOptions(), null );
				}
				else
				{
					optionsSwitch( commandSet, commandSet.servOptionCommands, e, serv, null, e.getOldOptions(), null );
				}

				return "";
			}

			case "optionsSwitch":
			{
				final CommandSet commandSet = (CommandSet)globalArgs[4];
				if ( connection != null )
				{
					optionsSwitch( commandSet, commandSet.connectionOptionCommands, e, serv, connection, e.getOptionsToRemove(), e.getOptionsToAdd() );
				}
				else
				{
					optionsSwitch( commandSet, commandSet.servOptionCommands, e, serv, null, e.getOptionsToRemove(), e.getOptionsToAdd() );
				}

				return "";
			}

			case "serviceEnable":
			{
                logger.info( "serviceEnable" );

                final CommandSet commandSet = (CommandSet)globalArgs[4];
				executeCommands( e, serv, null, e.getNewOptions(), commandSet, commandSet.servModifyCommands.enableCommands );

				return "";
			}

			case "serviceDisable":
			{
				logger.info( "serviceDisable" );
				
				final CommandSet commandSet = (CommandSet)globalArgs[4];
				executeCommands( e, serv, null, e.getOldOptions(), commandSet, commandSet.servModifyCommands.disableCommands );

				return "";
			}

			case "serviceCreate":
			{
				logger.info( "serviceCreate" );
				
				final CommandSet commandSet = (CommandSet)globalArgs[4];
				if ( commandSet.servCommands.enableCommands != null )
				{
					executeCommands( e, e.getNewInetServ(), connection, e.getNewOptions(), commandSet, commandSet.servCommands.enableCommands.commands );
				}

				return "";
			}

			case "serviceCancel":
			{
				logger.info( "serviceCancel" );
				
				final CommandSet commandSet = (CommandSet)globalArgs[4];
				if ( commandSet.servCommands.disableCommands != null )
				{
					executeCommands( e, e.getOldInetServ(), connection, e.getOldOptions(), commandSet, commandSet.servCommands.disableCommands.commands );
				}

				return "";
			}
			
			case "connectionEnable":
			{
				logger.info( "connectionEnable" );
				
				final CommandSet commandSet = (CommandSet)globalArgs[4];
				executeCommands( e, serv, e.getConnection(), e.getNewOptions(), commandSet, commandSet.connectionModifyCommands.enableCommands );

				return "";
			}

			case "connectionDisable":
			{
				logger.info( "connectionDisable" );
				
				final CommandSet commandSet = (CommandSet)globalArgs[4];
				executeCommands( e, serv, e.getConnection(), e.getOldOptions(), commandSet, commandSet.connectionModifyCommands.disableCommands );

				return "";
			}
			
			case "connectionClose":
			{
				logger.info( "connectionClose" );
				
				final CommandSet commandSet = (CommandSet)globalArgs[4];
				executeCommands( e, serv, null, e.getOldOptions(), commandSet, commandSet.connectionCommands.commands );

				return "";
			}

			case "newState":
			{
                logger.info( "newState" );

                return e.getNewState();
			}

			case "oldState":
			{
                logger.info( "oldState" );

                return e.getOldState();
			}

			case "ip":
			{
				if ( connection != null && connection.getInetAddressBytes() != null )
				{
					return IpAddress.toString( connection.getInetAddressBytes() );
				}
				else
				{
					return IpAddress.toString( serv.getAddressFrom() );
				}
			}

			case "net":
			{
				return IpNet.toString( serv.getAddressFrom(), serv.getAddressTo() );
			}

            case "ipList":
            case "iplist":
            {
                switch( args.length )
                {
                    case 1:
                        String delimeter = (String)args[0];
                        return ipList( serv, delimeter );

                    default:
                        return ipList( serv, ", " );
                }
            }
			
			case "ipFrom":
			{
			    return IpAddress.toString( serv.getAddressFrom() );
			}
			
			case "ipTo":
			{
			    return IpAddress.toString( serv.getAddressTo() );
			}

			case "mask":
			case "bitmask":
			{
				return IpNet.getMask( serv.getAddressFrom(), serv.getAddressTo() );
			}

			case "netmask":
			{
				int bitMask = IpNet.getMask( serv.getAddressFrom(), serv.getAddressTo() );
				long mask = (0xFFFFFFFFl << (32 - bitMask)) & 0xFFFFFFFFl;

				return IPUtils.convertLongIpToString( mask );
			}

			case "netmaskWild":
			case "netmaskWildcard":
			{
				int bitMask = IpNet.getMask( serv.getAddressFrom(), serv.getAddressTo() );
				long mask = (0xFFFFFFFFl << (32 - bitMask)) & 0xFFFFFFFFl;
				long maskWild = mask ^ 0xFFFFFFFFL;

				return IPUtils.convertLongIpToString( maskWild );
			}

			case "vlan":
			{
				return serv.getVlan();
			}
			
            case "svlan":
            {
                switch( args.length )
                {
                    case 1:
                        int deviceId = (Integer)args[0];
                        return getDeviceSvlan( deviceId );

                    default:
                        return getDeviceSvlan( this.deviceId );
                }
            }

			case "iface":
			case "port":
			{
				return serv.getInterfaceId();
			}

			case "ifaceTitle":
			{
				final int interfaceId = serv.getInterfaceId();
				return InetInterfaceMap.getInstance( moduleId ).getInterfaceTitle( device.getInvDeviceId(), interfaceId );
			}

			case "mac":
			{
				return MACParser.macAddressToString( serv.getMacAddressListBytes() );
			}

			case "macBytes":
			{
				return Utils.bytesToString( serv.getMacAddressListBytes(), false, null );
			}

			case "servTitle":
			{
				return serv.getTitle();
			}

			case "identifier":
			{
				List<String> list = serv.getIdentifierList();
				if ( list != null && list.size() > 0 )
				{
					return list.get( 0 );
				}
				else
				{
					return "";
				}
			}

			case "contractId":
			{
				return serv.getContractId();
			}

			case "servId":
			{
				return serv.getId();
			}

			case "servTypeId":
			{
				return serv.getTypeId();
			}

			case "deviceId":
			{
				return this.deviceId;
			}

			case "servDeviceId":
			{
				return serv.getDeviceId();
			}

			case "connectionAgentDeviceId":
			case "agentDeviceId":
			{
				return connection != null ? connection.getAgentDeviceId() : serv.getDeviceId();
			}

			case "connectionDeviceId":
			{
				return connection != null ? connection.getDeviceId() : serv.getDeviceId();
			}

			case "ipGate":
			{
				IpResourceRuntime ipResourceRuntime = getIpResource( serv, connection );
				if ( ipResourceRuntime != null )
				{
					return ipResourceRuntime.ipResource.getRouter();
				}
			}

            case "ipDns":
            {
                final IpResourceRuntime ipResourceRuntime = getIpResource( serv, connection );
                if ( ipResourceRuntime != null )
                {
                    return getIpDns( args, ipResourceRuntime );
                }
                else
                {
                    return "";
                }
            }

			case "ipSubnetMask":
			{
				IpResourceRuntime ipResourceRuntime = getIpResource( serv, connection );
				if ( ipResourceRuntime != null )
				{
					return ipResourceRuntime.ipResource.getSubnetMask();
				}
			}

			case "ipParam":
			{
                IpResourceRuntime ipResourceRuntime = getIpResource( serv, connection );
                if ( logger.isDebugEnabled() )
                {
                    logger.debug( "ipParam:serv = " + serv );
                    logger.debug( "ipParam:connection = " + connection );
                    logger.debug( "ipParam:ipResourceRuntime = " + ipResourceRuntime );
                }
                if ( ipResourceRuntime != null )
                {
                    final String def = args.length > 1 ? String.valueOf( args[1] ) : "";
                    logger.debug( "ipParam:args[0] = " + args[0] );
                    logger.debug( "ipParam:default = " + def );
                    logger.debug( "ipParam:ipResourceRuntime.config = " + ipResourceRuntime.config.toString() );
                    logger.debug( "ipParam:ipResourceRuntime.config.get(" + args[0] + ", " + def + " ) = " + ipResourceRuntime.config.get( String.valueOf( args[0] ), def ) );
                    
                    return ipResourceRuntime.config.get( String.valueOf( args[0] ), def );
                }
                else
                {
                    return "";
                }
			}

			case "deviceAttr":
			{
				return deviceAttr( args );
			}

			/** 
			 * может кому пригодится. помогло в EPON OLT от BDCom для добавления статических bind-onu
			 * http://forum.bitel.ru/viewtopic.php?f=44&t=10376
			 */
			/* 
			 * выдаем MAC в формате abcd.ef12.3456
			 */
			case "macBytesDoted":
			case "macDoted":
			{
				return Utils.bytesToString( serv.getMacAddressListBytes(), false, null ).replaceAll( "(.{4})(.{4})(.{4})", "$1.$2.$3" );
			}

			/*
			 * берем название интерфейса из тайтла до : . Например EPON0/1:2 = EPON0/1
			 */
			case "ifaceTitleBeforeColon":
			{
				final int interfaceId = serv.getInterfaceId();
				return InetInterfaceMap.getInstance( moduleId ).getInterfaceTitle( device.getInvDeviceId(), interfaceId ).replaceAll( "(.*):(\\d*)", "$1" );
			}
			/*
			 * берем название интерфейса из тайтла после : . Например EPON0/1:2 = 2
			 */
			case "ifaceTitleAfterColon":
			{
				final int interfaceId = serv.getInterfaceId();
				return InetInterfaceMap.getInstance( moduleId ).getInterfaceTitle( device.getInvDeviceId(), interfaceId ).replaceAll( "(.*):(\\d*)", "$2" );
			}

			case "contractTitle":
			{
				final DefaultContext context = DefaultContext.get();
				final ContractRuntime contractRuntime = ContractRuntimeMap.getInstance().getContractRuntime( context.getConnectionSet(), serv.getContractId() );
				if ( contractRuntime == null )
				{
					return "null";
				}

				return contractRuntime.getContractTitle();
			}

			case "deviceIP":
			{
				switch( args.length )
				{
					case 1:
						int deviceId = (Integer)args[0];
						return getDeviceIP( deviceId );

					default:
						return getDeviceIP( this.device );
				}
			}

			case "deviceIdentifier":
			{
				switch( args.length )
				{
					case 1:
						int deviceId = (Integer)args[0];
						return getDeviceIdentifier( deviceId );

					default:
						return this.device.getIdentifier();
				}
			}

			case "ipNetHostMin":
			{
				IpNet net = IpNet.newInstance( serv.getAddressFrom(), serv.getAddressTo() );
				return IpAddress.toString( net.getHostMin() );
			}

			case "ipNetHostMax":
			{
				IpNet net = IpNet.newInstance( serv.getAddressFrom(), serv.getAddressTo() );
				return IpAddress.toString( net.getHostMax() );
			}

			case "ipNetBroadcast":
			{
				IpNet net = IpNet.newInstance( serv.getAddressFrom(), serv.getAddressTo() );
				return IpAddress.toString( net.getBroadcast() );
			}
			
			// loopServ(macroParamName,needParent,executeOrConcat)
			case "loopServ":
			{
				return loopServ( e, serv, connection, options, args, globalArgs );
			}
			
			case "callingStationId":
			{
			    return connection != null ? connection.getCallingStationId() : "";
			}
			
            case "calledStationId":
            {
                return connection != null ? connection.getCalledStationId() : "";
            }
            
            case "connectionCircuitId":
            {
                return Optional.ofNullable( connection )
                    .map( a -> a.getCircuitId() )
                    .map( a -> a.toString() )
                    .orElse( "" );
            }

			default:
				return null;
		}
	}

    private static final Pattern COMMA = Pattern.compile( "\\s*,\\s*" );
    
    private Object getIpDns( final Object[] args, final IpResourceRuntime ipResourceRuntime )
    {
        final int num;
        final String def;

        switch( args.length )
        {
            case 0:
                return Utils.maskNull( ipResourceRuntime.ipResource.getDns() );

            case 2:
            {
                num = (Integer)args[0];
                def = String.valueOf( args[1] != null ? args[1] : "" );

                break;
            }

            case 1:
            default:
            {
                if ( args[0] instanceof Integer )
                {
                    num = (Integer)args[0];
                    def = "";
                }
                else
                {
                    num = -1;
                    def = String.valueOf( args[0] != null ? args[0] : "" );
                }
            }
        }

        if ( num <= 0 )
        {
            if ( Utils.isBlankString( ipResourceRuntime.ipResource.getDns() ) )
            {
                return def;
            }
            else
            {
                return ipResourceRuntime.ipResource.getDns();
            }
        }

        String[] dnss = COMMA.split( Utils.maskNull( ipResourceRuntime.ipResource.getDns() ) );

        return dnss.length >= num
            ? dnss[num - 1].trim()
            : def;
    }

	private Object loopServ( ServiceActivatorEvent e, InetServ serv, InetConnection connection, Set<Integer> options, Object[] args, Object[] globalArgs )
	{
		String macr = this.config.get( getString( args, 0, "n_2356" ), null );
		if ( Utils.isEmptyString( macr ) )
		{
			return "";
		}
		
		// цикл вместе с родительским сервисом или без него
		boolean needParent = Utils.parseBoolean( getString( args, 1, "false" ) );
		// выполнить или сгенерировать строку
		Object exec = args.length > 2 ? args[2] : Boolean.FALSE;
		
		final CommandSet commandSet = (CommandSet)globalArgs[4];

		if ( Boolean.TRUE.equals( exec ) )
		{
			final String[] commands = semicolonPattern.split( macr );
			
			if ( needParent )
			{
				try
				{
					executeCommands( e, serv, connection, options, commandSet, commands );
				}
				catch( Exception ex )
				{
					throw new BGRuntimeException( ex );
				}
			}

			final List<InetServ> children = serv.getChildren();
			if ( children != null && children.size() > 0 )
			{
			    Date now = new Date();
				for ( InetServ child : children )
				{
					try
					{
                        if ( !TimeUtils.dateInRange( now, child.getDateFrom(), child.getDateTo() ) )
                        {
                            continue;
                        }
					    
						executeCommands( e, child, connection, options, commandSet, commands );
					}
					catch( Exception ex )
					{
						throw new BGRuntimeException( ex );
					}
				}
			}
			
			return "";
		}
		else
		{
			final String delimeter = (Boolean.FALSE.equals( exec ) || exec == null) ? "" : exec.toString();

			final StringBuilder sb = new StringBuilder();

			if ( options != null && this.workingOptions != null )
			{
				options = new HashSet<Integer>( options );
				options.retainAll( this.workingOptions );
			}

			if ( needParent )
			{
				String evalResult = String.valueOf( this.macro.eval( macr, e, serv, connection, options, commandSet ) );
				sb.append( evalResult );
			}

			final List<InetServ> children = serv.getChildren();
			if ( children != null && children.size() > 0 )
			{
			    Date now = new Date();
				for ( InetServ child : children )
				{
                    if ( !TimeUtils.dateInRange( now, child.getDateFrom(), child.getDateTo() ) )
                    {
                        continue;
                    }
				    
					if ( sb.length() > 0 )
					{
						sb.append( delimeter );
					}
					
					String evalResult = String.valueOf( this.macro.eval( macr, e, child, connection, options, commandSet ) );
					sb.append( evalResult );
				}
			}
			
			return sb.toString();
		}
	}

	private Object deviceAttr( final Object[] args )
	{
		final int deviceId;
		final int entitySpecAttrId;
		final Object def;

		switch( args.length )
		{
			case 1:
				deviceId = this.deviceId;
				entitySpecAttrId = (Integer)args[0];
				def = null;
				break;

			case 2:
				deviceId = (Integer)args[0];
				entitySpecAttrId = (Integer)args[1];
				def = null;
				break;

			case 3:
				deviceId = (Integer)args[0];
				entitySpecAttrId = (Integer)args[1];
				def = args[2];
				break;

			default:
				deviceId = this.deviceId;
				entitySpecAttrId = 0;
				def = null;
				break;
		}

		final Object result = getDeviceAttribute( deviceId, entitySpecAttrId );
		if ( result != null )
		{
			return result;
		}

		return def;
	}

    private String ipList( final InetServ serv, String delimeter )
    {
        if ( delimeter == null )
        {
            delimeter = ", ";
        }

        final byte[] addressFrom = serv.getAddressFrom();
		byte[] addressTo = serv.getAddressTo();

		if ( addressFrom == null )
		{
			return "";
		}

		if ( addressTo == null )
		{
			addressTo = addressFrom;
		}

		final byte[] ip = new byte[addressFrom.length];
		System.arraycopy( addressFrom, 0, ip, 0, addressFrom.length );

		StringBuilder sb = new StringBuilder();

		for ( int i = 0; i < 255 && IpAddress.compare( ip, addressTo ) <= 0; i++ )
		{
			sb.append( IpAddress.toString( ip ) );
			
			sb.append( delimeter );

			if ( !IpAddress.increment( ip ) )
			{
				break;
			}
		}

        if ( sb.length() > 0 )
        {
            sb.setLength( sb.length() - delimeter.length() );
        }

		return sb.toString();
	}

	protected TerminalSession getTerminalSession()
		throws Exception
	{
		if ( terminalSession != null )
		{
			if ( !terminalSession.isConnected() )
			{
				try
				{
					if ( connectionSemaphore != null )
					{
						connectionSemaphoreAcquired = connectionSemaphore.tryAcquire( 3, TimeUnit.MINUTES );
					}

					terminalSession.setEndSequence( endSequence );
					terminalSession.connect();

					logger.info( "Connected to " + device.getGuiTitle() );
				}
				catch( Exception ex )
				{
					logger.error( "Can't connect to " + device.getGuiTitle() + " to " + terminalSession.getHost() + ":" + terminalSession.getPort() + ". Check host/port, username/password and sa.endSequence (command prompt)" );
					throw ex;
				}
				
				executeCommands( null, null, null, null, null, this.commandConfig.connectCommands );
			}
		}

		return terminalSession;
	}

	@Override
	public Object connect()
		throws Exception
	{
	    this.variables.clear();
	    
		if ( !lazyConnect )
		{
			getTerminalSession();
		}

		return null;
	}

	@Override
	public Object disconnect()
		throws Exception
	{
		try
		{
		    this.variables.clear();
		    
			if ( terminalSession == null || !terminalSession.isConnected() )
			{
				logger.debug( "disconnect: not connected - skip disconnection (deviceId={})", deviceId );
				return null;
			}

			try
			{
				executeCommands( null, null, null, null, null, this.commandConfig.disconnectCommands );
			}
			catch( Exception ex )
			{
				logger.error( "disconnect: error disconnect commands (deviceId="+deviceId+")", ex );
			}

			try
			{
				if ( Utils.notBlankString( this.commandConfig.exitCommand ) )
				{
					logger.info( "disconnect: [" + device + "] executeAsync: " + this.commandConfig.exitCommand );
					getTerminalSession().sendCommand( this.commandConfig.exitCommand, TerminalSession.NOWAIT );
				}
			}
			catch( Exception ex )
			{
				logger.error( "disconnect: error exit commands (deviceId="+deviceId+")", ex );
			}

			terminalSession.close();

			logger.debug( "disconnect: disconnected (deviceId={})", deviceId );
		}
		finally
		{
			if ( connectionSemaphore != null && connectionSemaphoreAcquired )
			{
				connectionSemaphore.release();
				if ( logger.isDebugEnabled() )
				{
				    logger.debug( "disconnect: (deviceId={}) connectionSemaphore={}", deviceId, connectionSemaphore.getQueueLength() );
				}
			}
		}

		return null;
	}

	protected CommandSet getCommandSet( final ServiceActivatorEvent serviceActivatorEvent )
	{
        this.variables.clear();

        InetServ inetServ = null;

        logger.debug( "Ищем inetServ в serviceActivatorEvent.getInetServRuntime()..." );

        if ( serviceActivatorEvent.getInetServRuntime() != null )
        {
            inetServ = serviceActivatorEvent.getInetServRuntime().getInetServ();
        }

        if ( inetServ == null )
        {
            logger.debug( "Не нашли, попробуем взять из serviceActivatorEvent.getNewInetServ()..." );

            inetServ = serviceActivatorEvent.getNewInetServ();

            if ( inetServ == null )
            {
                logger.debug( "Нет, попробуем взять из serviceActivatorEvent.getOldInetServ()..." );
                
                inetServ = serviceActivatorEvent.getOldInetServ();

                if ( inetServ == null )
                {
                    logger.debug( "Нет, не зная inetServ, не можем получить его тип, а следовательно набор команд для этого типа, возращаем набор команд по умолчанию..." );
                    logger.debug( "Набор найден, commandSet = " + this.commandConfig.commandSet );
                    return this.commandConfig.commandSet;
                }
            }
        }
        
        if ( logger.isDebugEnabled() )
        {
            logger.debug( "Нашли inetServ = " + inetServ );
        }

		final int servTypeId = inetServ.getTypeId();
		
        if ( logger.isDebugEnabled() )
        {
            logger.debug( "Определяем его тип inetServTypeId = " + servTypeId );
            logger.debug( "Ищем набор команд для этого типа..." );
        }

		final CommandSet commandSet = this.commandConfig.servTypeCommandSetMap.get( servTypeId );
		if ( commandSet != null )
		{
	        if ( logger.isDebugEnabled() )
	        {
	            logger.debug( "Набор найден, commandSet = " + commandSet );
	        }
		    
			return commandSet;
		}

        logger.debug( "Набор не задан, возращаем набор команд по умолчанию..." );
        logger.debug( "Набор найден, commandSet = " + this.commandConfig.commandSet );
		
		return this.commandConfig.commandSet;
	}

	protected void optionsSwitch( final CommandSet commandSet, Map<Integer, CommandGroup> optionCommands, ServiceActivatorEvent e, InetServ serv, InetConnection connection, Set<Integer> optionsDisable, Set<Integer> optionsEnable )
		throws Exception
	{
		logger.info( "switchOptions" );
		if ( optionsDisable != null )
		{
			if ( this.workingOptions != null )
			{
				optionsDisable = new HashSet<Integer>( optionsDisable );
				optionsDisable.retainAll( this.workingOptions );
			}

			for ( Integer option : optionsDisable )
			{
				CommandGroup commandGroup = optionCommands.get( option );
				if ( commandGroup != null && commandGroup.disableCommands != null )
				{
					executeCommands( e, serv, connection, Collections.singleton( option ), commandSet, commandGroup.disableCommands );
				}
			}
		}

		if ( optionsEnable != null )
		{
			if ( this.workingOptions != null )
			{
				optionsEnable = new HashSet<Integer>( optionsEnable );
				optionsEnable.retainAll( this.workingOptions );
			}

			for ( Integer option : optionsEnable )
			{
				CommandGroup commandGroup = optionCommands.get( option );
				if ( commandGroup != null && commandGroup.enableCommands != null )
				{
					executeCommands( e, serv, connection, Collections.singleton( option ), commandSet, commandGroup.enableCommands );
				}
			}
		}
	}

	@Override
	public Object serviceCreate( ServiceActivatorEvent e )
		throws Exception
	{
	    logger.info( "serviceCreate" );
	    
		CommandSet commandSet = getCommandSet( e );
		executeCommands( e, e.getNewInetServ(), null, e.getNewOptions(), commandSet, commandSet.servCommands.enableCommands );

		return null;
	}

	@Override
	public Object serviceCancel( ServiceActivatorEvent serviceActivatorEvent )
		throws Exception
	{
	    logger.info( "serviceCancel" );
	    
		CommandSet commandSet = getCommandSet( serviceActivatorEvent );
		executeCommands( serviceActivatorEvent, serviceActivatorEvent.getOldInetServ(), null, serviceActivatorEvent.getOldOptions(), commandSet, commandSet.servCommands.disableCommands );

		return null;
	}

    private boolean serviceChanged( InetServ serviceOld, InetServ serviceNew, boolean checkPeriod )
    {
        return (serviceOld.getInterfaceId() != serviceNew.getInterfaceId())
            || (serviceOld.getVlan() != serviceNew.getVlan())
            || !addressEquals( serviceOld.getAddressFrom(), serviceNew.getAddressFrom() )
            || !addressEquals( serviceOld.getAddressTo(), serviceNew.getAddressTo() )
            || (checkPeriod && (!TimeUtils.dateEqual( serviceOld.getDateFrom(), serviceNew.getDateFrom() )
                || !TimeUtils.dateEqual( serviceOld.getDateTo(), serviceNew.getDateTo() )));
    }

	private boolean addressEquals( byte[] addrr1, byte[] addr2 )
	{
		return ((addrr1 == null && addr2 == null) || IpAddress.equals( addrr1, addr2 ));
	}

	@Override
	public Object serviceModify( ServiceActivatorEvent e )
		throws Exception
	{
		logger.info( "serviceModify" );

        if ( needCancelCreate( e ) )
        {
            logger.info( "Iface/Vlan or children was modified, forcing cancel-create" );

            serviceCancel( e );
            serviceCreate( e );

            return null;
        }

		CommandSet commandSet = getCommandSet( e );
		executeCommands( e, e.getNewInetServ(), null, e.getNewOptions(), commandSet, commandSet.servModifyCommands.commands );

		return null;
	}
	
    private boolean needCancelCreate( final ServiceActivatorEvent e )
    {
        if ( this.needCancelCreate <= 0 )
        {
            return false;
        }
        
        if ( e.getOldInetServ() == null && e.getNewInetServ() == null )
        {
            return false;
        }

        // изменение в дочерних сервисах
        if ( this.needCancelCreate >= 2 )
        {
            if ( e.getOldInetServ().getChildren().size() == e.getNewInetServ().getChildren().size() )
            {
                for ( int i = 0; i < e.getOldInetServ().getChildren().size(); i++ )
                {
                    InetServ oldChild = e.getOldInetServ().getChildren().get( i );
                    InetServ newChild = e.getNewInetServ().getChildren().get( i );

                    if ( serviceChanged( oldChild, newChild, true ) )
                    {
                        return true;
                    }
                }
            }
            else
            {
                return true;
            }
        }

        // смена интерфейса/VLAN/ip в родительском
        if ( serviceChanged( e.getOldInetServ(), e.getNewInetServ(), false ) )
        {
            return true;
        }

        return false;
    }

	@Override
	public Object connectionModify( ServiceActivatorEvent e )
		throws Exception
	{
		logger.info( "connectionModify" );

		if ( e.getNewState() == InetServState.STATE_DISABLE.getCode() || e.getOldState() == InetServState.STATE_DISABLE.getCode() )
		{
			if ( needConnectionStateModify )
			{
				e.setConnectionStateModified( true );
			}
		}

		CommandSet commandSet = getCommandSet( e );
		executeCommands( e, e.getOldInetServ(), e.getConnection(), e.getNewOptions(), commandSet, commandSet.connectionModifyCommands.commands );

		return null;
	}

	@Override
	public Object connectionClose( ServiceActivatorEvent e )
		throws Exception
	{
		CommandSet commandSet = getCommandSet( e );
		executeCommands( e, e.getOldInetServ(), e.getConnection(), e.getOldOptions(), commandSet, commandSet.connectionCommands.commands );

		return null;
	}

	@Override
	public Object onAccountingStart( ServiceActivatorEvent e )
		throws Exception
	{
		final InetConnection connection = e.getConnection();

		if ( skipServiceAccountingEvents && connection.getParentConnectionId() > 0L )
		{
			logger.debug( "Skip service connection" );
			return null;
		}

		if ( accountingEventDeviceIds != null && !accountingEventDeviceIds.contains( connection.getDeviceId() ) )
		{
			logger.debug( "Skip connection with deviceId=" + connection.getDeviceId() );
			return null;
		}

		CommandSet commandSet = getCommandSet( e );
		executeCommands( e, e.getNewInetServ(), e.getConnection(), e.getNewOptions(), commandSet, commandSet.connectionCommands.enableCommands );

		return null;
	}

	@Override
	public Object onAccountingStop( ServiceActivatorEvent e )
		throws Exception
	{
		final InetConnection connection = e.getConnection();

		if ( skipServiceAccountingEvents && connection.getParentConnectionId() > 0L )
		{
			logger.debug( "Skip service connection" );
			return null;
		}

		if ( accountingEventDeviceIds != null && !accountingEventDeviceIds.contains( connection.getDeviceId() ) )
		{
			logger.debug( "Skip connection with deviceId=" + connection.getDeviceId() );
			return null;
		}

		CommandSet commandSet = getCommandSet( e );
		executeCommands( e, e.getNewInetServ(), e.getConnection(), e.getNewOptions(), commandSet, commandSet.connectionCommands.disableCommands );

		return null;
	}
}
