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

import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.naming.NamingException;

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

import ru.bitel.bgbilling.apps.inet.access.Access;
import ru.bitel.bgbilling.common.BGException;
import ru.bitel.bgbilling.kernel.container.managed.ServerContext;
import ru.bitel.bgbilling.kernel.event.EventProcessor;
import ru.bitel.bgbilling.modules.inet.common.bean.InetDevice;
import ru.bitel.bgbilling.modules.inet.common.bean.InetServ;
import ru.bitel.bgbilling.modules.inet.common.event.manage.InetDeviceManageEvent;
import ru.bitel.bgbilling.modules.inet.common.event.sa.InetSaDeviceInitEvent;
import ru.bitel.bgbilling.modules.inet.common.service.InetServService;
import ru.bitel.bgbilling.modules.inet.dyn.device.snmp.SnmpDeviceManager;
import ru.bitel.bgbilling.modules.inet.dyn.device.terminal.TerminalServiceActivator;
import ru.bitel.bgbilling.modules.inet.dyn.device.terminal.TerminalSession;
import ru.bitel.bgbilling.modules.inet.server.runtime.InetInterfaceMap;
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.Preferences;
import ru.bitel.common.Utils;
import ru.bitel.common.util.MacrosFormat;
import ru.bitel.common.util.RangedIntegerSet;
import ru.bitel.oss.systems.inventory.resource.common.bean.Device;
import ru.bitel.oss.systems.inventory.resource.common.bean.DeviceType;
import ru.bitel.oss.systems.inventory.resource.common.bean.VlanResource;
import ru.bitel.oss.systems.inventory.resource.common.service.ResourceService;

/**
 * <pre> manage.terminal.protocol=telnet
 * manage.terminal.host=<по умолчанию из поля хост/порт>
 * manage.terminal.port=<по умолчанию из поля хост/порт|дефолтное значения для указаного протокола>
 * 
 * manage.terminal.username=<по умолчанию из поля Username>
 * manage.terminal.password=<по умолчанию из поля Password>
 * 
 * # отключаем стандартный вызов синхронизации, чтобы можно было выполнять команды перед синхронизацией
 * sa.device.sync.onReboot=0
 * # включаем вызов синхронизации в TerminalDeviceManager
 * manage.onReboot.sync=1
 * 
 * # команды, которые выполнятся перед синхронизацией (отдельное telnet|ssh соединение), через точку с запятой
 * manage.onReboot.command=
 * 
 * # команды, которые выполнятся после синхронизации (отдельное telnet|ssh соединение), через точку с запятой
 * manage.onDeviceSyncFinish.command=
 * 
 * или можно еще так (вместо точки с запятой):
 * manage.onReboot.command.1=
 * manage.onReboot.command.2=
 * manage.onReboot.command.3=</pre>
 * 
 * @author amir
 *
 */
public class TerminalDeviceManager
	extends SnmpDeviceManager
{
	private static final Logger logger = LogManager.getLogger();

	protected Access access;
	protected int moduleId;
	
	/**
	 * invDeviceId
	 */
	private int deviceId;

	protected ParameterMap deviceConfig;

	private TerminalSession terminalSession;

	protected String username;
	protected String password;

	protected boolean syncOnReboot;

	protected List<String> onRebootCommands;
	protected List<String> onDeviceSyncFinishCommands;
	
	private MacrosFormat macrosFormat;

	@Override
	public Object init( Setup setup, int moduleId, Device<?, ?> device, DeviceType deviceType, ParameterMap config )
	{
		super.init( setup, moduleId, device, deviceType, config );

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

		try
		{
			// <bean name="access" class="ru.bitel.bgbilling.modules.inet.access.Access" />
			this.access = (Access)Setup.getEnvironment().lookup( "access" );
		}
		catch( NamingException ex )
		{
			logger.error( ex.getMessage(), ex );
		}

		this.deviceConfig = config;

		String terminalProtocol = config.get( "manage.terminal.protocol", null );

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

		if ( Utils.notBlankString( terminalProtocol ) )
		{
			String host = device.getHost();
			int port = 22;

			if ( "telnet".equals( terminalProtocol ) )
			{
				port = 23;
			}

			List<InetSocketAddress> hosts = device.getHosts();
			if ( hosts.size() > 0 )
			{
				InetSocketAddress socketAddress = hosts.get( 0 );

				host = socketAddress.getAddress().getHostAddress();
				port = socketAddress.getPort();
			}

			host = selfConfig.get( "manage.terminal.host", host );
			port = selfConfig.getInt( "manage.terminal.port", port );

			terminalSession = TerminalSession.newTerminalSession( terminalProtocol, host, port );

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

			this.username = selfConfig.get( "manage.terminal.username", device.getUsername() );
			this.password = selfConfig.get( "manage.terminal.password", device.getPassword() );
		}

		this.syncOnReboot = selfConfig.getInt( "manage.onReboot.sync", 0 ) > 0;

		if ( syncOnReboot && config.getInt( "sa.device.sync.onReboot", 0 ) > 0 )
		{
			syncOnReboot = false;
			logger.error( "Both manage.onReboot.sync and manage.onReboot.sync are enabled for" + device + ". Disable manage.onReboot.sync" );
		}

		this.onRebootCommands = TerminalServiceActivator.parseCommandArray( config, "manage.onReboot.command" );
		if ( this.onRebootCommands != null && this.onRebootCommands.size() == 0 )
		{
			this.onRebootCommands = null;
		}

		this.onDeviceSyncFinishCommands = TerminalServiceActivator.parseCommandArray( config, "manage.onDeviceSyncFinish.command" );
		if ( this.onDeviceSyncFinishCommands != null && this.onDeviceSyncFinishCommands.size() == 0 )
		{
			this.onDeviceSyncFinishCommands = null;
		}
		
		this.macrosFormat = createMacrosFormat();

		return null;
	}
	
	private Set<Integer> vlanSet;
	
	protected MacrosFormat createMacrosFormat()
	{
		return new MacrosFormat()
		{
			@Override
			protected Object invoke( String macros, Object[] args, Object[] globalArgs )
			{
				try
				{
					return invokeImpl( macros, args, globalArgs );
				}
				catch( Exception e )
				{
					logger.error( e.getMessage(), e );
				}

				return null;
			}

			private Object invokeImpl( String macros, Object[] args, Object[] globalArgs )
				throws Exception
			{
				if ( "arg".equals( macros ) )
				{
					return globalArgs[getInt( args, 0, 0 )];
				}
				else if ( "macros".equals( macros ) )
				{
					String pattern = getString( args, 0, null );
					if ( Utils.isEmptyString( pattern ) )
					{
						return "";
					}

					return this.format( pattern, globalArgs );
				}
				else if ( "concat".equals( macros ) )
				{
					StringBuilder result = new StringBuilder();
					for ( Object arg : args )
					{
						result.append( arg );
					}

					return result.toString();
				}
				else if ( "switch".equals( macros ) )
				{
					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;
				}
				else if ( "vlanList".equals( macros ) )
				{
					if ( vlanSet == null )
					{
						ServerContext context = ServerContext.get();
						InetServService servService = context.getService( InetServService.class, moduleId );

						ResourceService resourceService = context.getService( ResourceService.class, moduleId );
						
						Set<Integer> categoryIds = new HashSet<>();
						
						for ( InetDeviceRuntime deviceRuntime : access.deviceMap.values() )
						{
							final InetDevice device = deviceRuntime.inetDevice;
							if ( device.getInvDeviceId() == TerminalDeviceManager.this.deviceId)
							{
								Set<Integer> ids = servService.vlanResourceCategoryIds( device.getId() );
								categoryIds.addAll( ids );
							}
						}

						List<int[]> ranges = new ArrayList<>();

						for ( Integer categoryId : categoryIds )
						{
							List<VlanResource> list = resourceService.vlanResourceList( categoryId );
							for ( VlanResource resource : list )
							{
								ranges.add( new int[] { resource.getVlanFrom(), resource.getVlanTo() } );
							}
						}

						RangedIntegerSet rangedIntegerSet = new RangedIntegerSet( ranges );
						TerminalDeviceManager.this.vlanSet = rangedIntegerSet;
					}

					return Utils.toString( TerminalDeviceManager.this.vlanSet );
				}
				else
				{
					return null;
				}
			}
		};
	}

	/**
	 * Метод вызывается при обнаружении перезагрузки устройства.
	 * @param e
	 * @return
	 * @throws Exception
	 */
	public Object onReboot( InetDeviceManageEvent e )
		throws Exception
	{
		logger.info( "onReboot" );

		// если что-то нужно сделать перед синхронизацией на событие перезагрузки коммутатора, то можно отключить эту автоматическую синхронизацию: 
		// sa.device.sync.onReboot=0
		// , выполнить нужные действия в этом методе (onReboot) и вручную вызвать в этом же методе синхронизацию:
		// EventProcessor.getInstance().publish( new InetSaDeviceInitEvent( access.moduleId, 0, e.getDeviceId(), 0 ) );

		if ( this.onRebootCommands != null )
		{
			for ( String command : onRebootCommands )
			{
				command = this.macrosFormat.format( command, e );
				
				getTerminalSession().sendCommand( command );
			}
		}

		if ( syncOnReboot )
		{
			for ( InetDeviceRuntime deviceRuntime : access.deviceMap.values() )
			{
				final InetDevice device = deviceRuntime.inetDevice;
				if ( device.getInvDeviceId() == e.getDeviceId() )
				{
					EventProcessor.getInstance().publish( new InetSaDeviceInitEvent( access.moduleId, 0, device.getId(), 0 ) );
				}
			}
		}

		return null;
	}

	/**
	 * Метод вызывается после обработки команды (полной) синхронизации устройства
	 * (которая была вызвана вручную или по событию перезагрузки устройства).
	 * @return
	 * @throws Exception
	 */
	public Object onDeviceSyncFinish()
		throws Exception
	{
		logger.info( "onDeviceSyncFinish" );

		if ( this.onDeviceSyncFinishCommands != null )
		{
			for ( String command : onDeviceSyncFinishCommands )
			{
				command = this.macrosFormat.format( command );
				
				getTerminalSession().sendCommand( command );
			}
		}

		return null;
	}

	/**
	 * Получение сервиса (для которого было вызвано событие).
	 * @param e
	 * @return
	 */
	protected InetServ getInetServ( InetDeviceManageEvent e )
	{
		final int servId = e.getServId();

		final InetServRuntime servRuntime = access.getInetServRuntimeMap().get( servId );
		return servRuntime.getInetServ();
	}

	/**
	 * Получение имени интерфейса.
	 * @param e
	 * @param ifaceId
	 * @return
	 * @throws BGException
	 */
	protected String getIfaceTitle( InetDeviceManageEvent e, int ifaceId )
		throws BGException
	{
		final int invDeviceId = e.getDeviceId();

		final InetInterfaceMap ifaceMap = InetInterfaceMap.getInstance( access.moduleId );

		return ifaceMap.getInterfaceTitle( invDeviceId, ifaceId );
	}

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

		terminalSession = null;

		return null;
	}

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

		/*if ( terminalSession != null )
		{
			terminalSession.connect( username, password );
		}*/

		return null;
	}

	public TerminalSession getTerminalSession()
		throws Exception
	{
		if ( terminalSession != null )
		{
			if ( !terminalSession.isConnected() )
			{
				terminalSession.connect( username, password );
			}
		}

		return terminalSession;
	}

	@Override
	public Object disconnect()
		throws Exception
	{
		try
		{
			if ( terminalSession != null )
			{
				terminalSession.close();
			}
		}
		finally
		{
			super.disconnect();
		}

		return null;
	}

	/*
	
	@DeviceManagerMethod(title = "Монитор", types = { DeviceManagerMethodType.DEVICE, DeviceManagerMethodType.ACCOUNT })
	public Object monitor()
	{
		return "browse:" + deviceConfig.get( "monitor.url", "http://zabbix.intranet.provider.ru/latest.php?hostid=$monitorHostId" )
									   .replaceAll( "\\$monitorHostId", deviceConfig.get( "monitor.hostId", "" ) );
	}
	
	@DeviceManagerMethod(title = "Telnet")
	public Object telnet()
	{
		return "telnet:" + deviceConfig.get( "telnet.host", deviceConfig.get( "manage.terminal.host", this.host ) ) + " "
			   + deviceConfig.getInt( "telnet.port", deviceConfig.getInt( "manage.terminal.port", 23 ) );
	}
	
	@DeviceManagerMethod(title = "TestMAC", types = { DeviceManagerMethodType.DEVICE })
	public String testMac()
		throws Exception
	{
		final StringBuilder result = new StringBuilder();
	
		snmpClient.walk( SnmpClient.oidFromString( "1.3.6.1.2.1.17.4.3.1.1" ), String.class, ( oid, value ) -> result.append( value ).append( '\n' ) );
	
		return result.toString();
	}
	
	@DeviceManagerMethod(title = "TestPORT", types = { DeviceManagerMethodType.DEVICE })
	public String testPort()
		throws Exception
	{
		final StringBuilder result = new StringBuilder();
	
		snmpClient.walk( SnmpClient.oidFromString( "1.3.6.1.2.1.17.4.3.1.2" ), String.class, ( oid, value ) -> result.append( value ).append( '\n' ) );
	
		return result.toString();
	}
	
	*/
}
