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

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.URI;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.regex.Pattern;

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

import bitel.billing.server.util.telnet.TelnetSession;
import ru.bitel.bgbilling.common.BGIllegalArgumentException;
import ru.bitel.bgbilling.kernel.network.mikrotik.MikrotikApiSession;
import ru.bitel.bgbilling.kernel.network.snmp.SnmpClient;
import ru.bitel.bgbilling.server.util.ssh.SSHSession;
import ru.bitel.common.ParameterMap;
import ru.bitel.common.Utils;
import ru.bitel.common.io.IOUtils;
import ru.bitel.oss.systems.inventory.resource.common.bean.Device;

public abstract class TerminalSession
	implements AutoCloseable
{
	private static final Logger logger = LogManager.getLogger();

	protected String host;
	protected int port;
	
	protected String username;
	protected String password;

	/**
	 * Не ожидаем ответа
	 */
	public static int NOWAIT = 1;

	protected long timeout = 60000;
	protected long readWait = 50;

	protected String endSequence;
	
	protected String sourceHost;
	protected int sourcePort;

	public String getHost()
	{
		return host;
	}

	public int getPort()
	{
		return port;
	}

	public String getUsername()
	{
		return username;
	}

	public void setUsername( String username )
	{
		this.username = username;
	}

	public String getPassword()
	{
		return password;
	}

	public void setPassword( String password )
	{
		this.password = password;
	}

	public void setTimeout( long timeout )
	{
		this.timeout = timeout;
	}

	public void setReadWait( long readWait )
	{
		this.readWait = readWait;
	}

	public void setEndSequence( String endSequence )
	{
		this.endSequence = endSequence;
	}

	public String getSourceHost()
	{
		return sourceHost;
	}

	public void setSourceHost( String sourceHost )
	{
		this.sourceHost = sourceHost;
	}

	public int getSourcePort()
	{
		return sourcePort;
	}

	public void setSourcePort( int sourcePort )
	{
		this.sourcePort = sourcePort;
	}

	public abstract void connect( String username, String password )
		throws Exception;

	public final void connect()
		throws Exception
	{
		connect( username, password );
	}

	public abstract boolean isConnected();

	public String sendCommand( String command )
		throws Exception
	{
		return sendCommand( command, 0 );
	}

	public String sendCommand( String command, int flags )
		throws Exception
	{
		logger.debug( "sendCommand: " + command );
		String result = sendCommandImpl( command, flags );
		logger.debug( "result: " + result );
		return result;
	}

	protected abstract String sendCommandImpl( String command, int flags )
		throws Exception;
	
	public String defaultExitCommand()
	{
		return null;
	}

	static class TelnetTerminalSession
		extends TerminalSession
	{
		private static final Logger logger = LogManager.getLogger();

		private TelnetSession session;

		public TelnetTerminalSession( String host, int port )
		{
			this.host = host;

			if ( port > 0 )
			{
				this.port = port;
			}
			else
			{
				this.port = 23;
			}
		}

		@Override
		public void setEndSequence( String endSequence )
		{
			if ( endSequence == null )
			{
				endSequence = "#";
			}

			super.setEndSequence( endSequence );

			if ( session != null )
			{
				session.setEndString( endSequence );
			}
		}

		@Override
		public void connect( String username, String password )
			throws Exception
		{
			if ( this.session != null )
			{
				logger.error( "Already connected!" );
				return;
			}

			TelnetSession session = new TelnetSession( host, port );

			session.setTimeout( (int)timeout );
			session.setReadWait( readWait );

			session.setEndString( ":" );

			session.connect();
			logger.info( "Connected" );

			this.session = session;

			logger.info( session.doCommand( username ) );
			logger.info( "Login entered" );

			if ( endSequence == null )
			{
				endSequence = "#";
			}

			session.setEndString( endSequence );

			logger.info( session.doCommand( password ) );
			logger.info( "Password entered" );
		}

		@Override
		protected String sendCommandImpl( String command, int flags )
			throws Exception
		{
			if ( flags == NOWAIT )
			{
				session.doCommandAsync( command );
				return null;
			}
			else
			{
				return session.doCommand( command );
			}
		}

		@Override
		public void close()
			throws Exception
		{
			if ( session != null )
			{
				session.disconnect();
				session = null;

				logger.debug( "Disconnected" );
			}
		}

		@Override
		public boolean isConnected()
		{
			return session != null;
		}

		@Override
		public String defaultExitCommand()
		{
			return "exit";
		}
	}

	static class SshTerminalSession
		extends TerminalSession
	{
		private static final Logger logger = LogManager.getLogger();

		private String privateKeyFile;

		private SSHSession session;

		public SshTerminalSession( String host, int port )
		{
			this.host = host;

			if ( port > 0 )
			{
				this.port = port;
			}
			else
			{
				this.port = 22;
			}
        }

        @Override
        public void setConfiguration( ParameterMap config )
        {
            if ( config == null )
            {
                logger.info( "setConfiguration: config is null" );
                return;
            }

            this.privateKeyFile = config.get( "privateKeyFile", null );
        }

        @Override
		public void setEndSequence( String endSequence )
		{
			if ( endSequence == null )
			{
				endSequence = "#";
			}

			super.setEndSequence( endSequence );

			if ( session != null )
			{
				if ( endSequence.startsWith( "\\" ) && endSequence.endsWith( "\\" ) )
				{
					session.setRegexp( endSequence.substring( 1, endSequence.length() - 1 ) );
				}
				else
				{
					session.setEndString( endSequence );
				}
			}
		}

		@Override
		public void connect( String username, String password )
			throws Exception
		{
			SSHSession session = new SSHSession( host, port, username, password );
			session.setTimeout( (int)timeout );
			session.setReadWait( readWait );
			session.setPrivateKeyFile( privateKeyFile );

			if ( Utils.notBlankString( endSequence ) )
			{
                if ( endSequence.startsWith( "\\" ) && endSequence.endsWith( "\\" ) )
                {
                    session.setRegexp( endSequence.substring( 1, endSequence.length() - 1 ) );
                }
                else
                {
                    session.setEndString( endSequence );
                }
			}

			session.connect();
			logger.info( "Connected" );

			this.session = session;
		}

		@Override
		protected String sendCommandImpl( String command, int flags )
			throws Exception
		{
			if ( flags == NOWAIT )
			{
				session.doCommandAsync( command );
				return null;
			}
			else
			{
				return session.doCommand( command );
			}
		}

		@Override
		public void close()
			throws Exception
		{
			if ( session != null )
			{
				session.disconnect();
				session = null;

				logger.debug( "Disconnected" );
			}
		}

		@Override
		public boolean isConnected()
		{
			return session != null;
		}

		@Override
		public String defaultExitCommand()
		{
			return "exit";
		}
	}

	static class ExecTerminalSession
		extends TerminalSession
	{
		private boolean connected = false;

		public ExecTerminalSession()
		{}

		@Override
		public void connect( String username, String password )
			throws Exception
		{
			this.connected = true;
		}

		@Override
		protected String sendCommandImpl( String command, int flags )
			throws Exception
		{
			Process process = Runtime.getRuntime().exec( command );

			if ( flags == NOWAIT )
			{
				return null;
			}

			if ( !process.waitFor( 1, TimeUnit.MINUTES ) )
			{
				throw new TimeoutException( "Timeout waiting when executing command." );
			}

			final ByteArrayOutputStream baos = new ByteArrayOutputStream();

			IOUtils.transfer( process.getInputStream(), baos, 256 );

			byte[] resultBytes = baos.toByteArray();

			String result = new String( resultBytes, "UTF-8" );

			return result;
		}

		@Override
		public void close()
			throws Exception
		{
			this.connected = false;
		}

		@Override
		public boolean isConnected()
		{
			return this.connected;
		}
	}

	static class TcpTerminalSession
		extends TerminalSession
	{
		private static final Logger logger = LogManager.getLogger();

		private Socket socket;
		private PrintWriter out;
		private BufferedReader in;

		private Scanner scanner;

		private Pattern pattern;

		public TcpTerminalSession( String host, int port )
		{
			this.host = host;
			this.port = port;
		}

		@Override
		public void connect( String username, String password )
			throws Exception
		{
			socket = new Socket(  );

			socket.setSoTimeout( (int)timeout );

			socket.connect( new InetSocketAddress( host, port ), (int)timeout );

			out = new PrintWriter( socket.getOutputStream(), true );

			InputStreamReader isr = new InputStreamReader( socket.getInputStream() );
			in = new BufferedReader( isr );

			scanner = new Scanner( in );

			logger.info( "Connected" );
		}

		@Override
		public void setEndSequence( String endSequence )
		{
			super.setEndSequence( endSequence );

			pattern = Pattern.compile( Pattern.quote( endSequence ), Pattern.DOTALL );
		}

		@Override
		protected String sendCommandImpl( String command, int flags )
			throws Exception
		{
			out.println( command );
			out.flush();

			if ( flags == TerminalSession.NOWAIT || Utils.isEmptyString( endSequence ) )
			{
				return "";
			}

			scanner.useDelimiter( pattern );

			return scanner.next();
		}

		@Override
		public void close()
			throws Exception
		{
			if ( out != null )
			{
				out.close();
				socket.close();

				scanner.close();

				logger.info( "Disconnected" );

				out = null;
				socket = null;
			}
		}

		@Override
		public boolean isConnected()
		{
			return out != null;
		}
	}
	
	static class MikrotikTerminalSession
		extends TerminalSession
	{
		private static final Logger logger = LogManager.getLogger();

		private MikrotikApiSession session;

		public MikrotikTerminalSession( String host, int port )
		{
			this.host = host;

			if ( port > 0 )
			{
				this.port = port;
			}
			else
			{
				this.port = 8728;
			}
		}

		@Override
		public void connect( String username, String password )
			throws Exception
		{
			if ( this.session != null )
			{
				logger.error( "Already connected!" );
				return;
			}

			MikrotikApiSession session = new MikrotikApiSession( host, port, username, password, super.sourceHost, super.sourcePort );

			session.setTimeout( (int)timeout );

			session.connect();
			logger.info( "Connected" );

			this.session = session;
		}

		@Override
		protected String sendCommandImpl( String command, int flags )
			throws Exception
		{
			return session.doCommand( command.replace( "\\n", "\n" ) );
		}

		@Override
		public void close()
			throws Exception
		{
			if ( session != null )
			{
				// ничего не надо  запускать . на "/quit" приходит ответ c ошибкой !fatal. А у кого-то просто висло на "exit"
				
			    session.disconnect();

				session = null;

				logger.debug( "Disconnected" );
			}
		}

		@Override
		public boolean isConnected()
		{
			return session != null;
		}

		public String getLastIds()
		{
			return session.getLastIds();
		}

		@Override
		public String defaultExitCommand()
		{
			return null;
		}
	}

	static class MikrotikApiTerminalSession
        extends TerminalSession
    {
        private static final Logger logger = LogManager.getLogger();

        private ru.bitel.bgbilling.kernel.network.mikrotik.MikrotikApiSession session;
        private boolean ssl;

        public MikrotikApiTerminalSession( String host, int port )
        {
            this( host, port, false );
        }

        public MikrotikApiTerminalSession( String host, int port, boolean ssl )
        {
            this.host = host;

            if ( port > 0 )
            {
                this.port = port;
            }
            else
            {
                if ( ssl )
                {
                    this.port = 8729;
                }
                else
                {
                    this.port = 8728;
                }
            }
            
            this.ssl = ssl;
        }

        @Override
        public void connect( String username, String password )
            throws Exception
        {
            if ( this.session != null )
            {
                logger.error( "Already connected!" );
                return;
            }

            ru.bitel.bgbilling.kernel.network.mikrotik.MikrotikApiSession session = new ru.bitel.bgbilling.kernel.network.mikrotik.MikrotikApiSession( host, port, username, password, super.sourceHost, super.sourcePort, ssl );

            session.setTimeout( (int)timeout );

            session.connect();
            logger.info( "Connected" );

            this.session = session;
        }

        @Override
        protected String sendCommandImpl( String command, int flags )
            throws Exception
        {
            return session.doCommand( command.replace( "\\n", "\n" ) );
        }

        @Override
        public void close()
            throws Exception
        {
            if ( session != null )
            {
                // ничего не надо  запускать . на "/quit" все падает 
                session.disconnect();

                session = null;

                logger.debug( "Disconnected" );                
            }
        }

        @Override
        public boolean isConnected()
        {
            return session != null;
        }

		public String getLastIds()
		{
			return session.getLastIds();
		}

		@Override
		public String defaultExitCommand()
		{
			return null;
		}
    }

    static class SnmpTerminalSession
        extends TerminalSession
    {
        protected SnmpClient snmpClient;

        private final int snmpVersion;

        public SnmpTerminalSession( String host, int port, int snmpVersion )
        {
            this.host = host;
            this.port = port;
            this.snmpVersion = snmpVersion;
        }

        @Override
        public void connect( String username, String password )
            throws Exception
        {
            snmpClient = new SnmpClient( host, port, snmpVersion, password );
        }

        @Override
        public void close()
            throws Exception
        {
            if ( snmpClient != null )
            {
                snmpClient.destroy();
                snmpClient = null;
            }
        }

        @Override
        public boolean isConnected()
        {
            return snmpClient != null;
        }

        @Override
        protected String sendCommandImpl( final String command, final int flags )
            throws Exception
        {
            final String[] cmd = command.split( "\\s+" );

            if ( cmd.length < 1 )
            {
                throw new BGIllegalArgumentException( "snmp" );
            }

            switch( cmd[0] )
            {
                case "get":
                {
                    if ( cmd.length < 2 )
                    {
                        throw new BGIllegalArgumentException( "snmp.get" );
                    }
                    System.out.println( cmd[1] );
                    return snmpClient.get( cmd[1], String.class );
                }

                case "set":
                default:
                {
                    if ( (cmd.length - 1) % 3 != 0 )
                    {
                        throw new BGIllegalArgumentException( "snmp.set" );
                    }

                    final Map<String, Object> map = new HashMap<>();

                    for ( int i = 1; i < cmd.length; i += 3 )
                    {
                        String oid = cmd[i];
                        String type = cmd[i + 1];
                        String value = cmd[i + 2];

                        map.put( oid, value( value, type ) );
                    }

                    return String.valueOf( snmpClient.set( map ) );
                }
            }
        }

        private Object value( String value, String type )
        {
            switch( type )
            {
                case "i":
                    return Utils.parseInt( value, 0 );

                case "u":
                    return Utils.parseLong( value, 0 );

                case "x":
                    return Utils.stringToBytes( value, null );

                case "n":
                    return null;

                case "s":
                default:
                    return value;
            }
        }
    }

	public static TerminalSession newSshSession( String host, int port )
	{
		return new SshTerminalSession( host, port );
	}

	public static TerminalSession newTelnetSession( String host, int port )
	{
		return new TelnetTerminalSession( host, port );
	}

	public static TerminalSession newTcpSession( String host, int port )
	{
		return new TcpTerminalSession( host, port );
	}

	public static TerminalSession newTerminalSession( String type, String host, int port )
	{
		return newTerminalSession( type, host, port, null, null );
	}

	public static TerminalSession newTerminalSession( String type, String host, int port, String username, String password )
	{
		TerminalSession result;

		switch( type )
		{
			case "ssh":
				result = new SshTerminalSession( host, port );
				break;

			case "shell":
				result = new ExecTerminalSession();
				break;

			case "tcp":
				result = new TcpTerminalSession( host, port );
				break;
				
			case "mikrotik":
				result = new MikrotikTerminalSession( host, port );
				break;
            
			case "mikrotik-api":
                result = new MikrotikApiTerminalSession( host, port );
                break;

            case "mikrotik-api-ssl":
                result = new MikrotikApiTerminalSession( host, port, true );
                break;

            case "snmp":
                result = new SnmpTerminalSession( host, port, 1 );
                break;

            case "snmp-v2":
            case "snmp-v2c":
                result = new SnmpTerminalSession( host, port, 2 );
                break;

			case "telnet":
			default:
				result = new TelnetTerminalSession( host, port );
				break;
		}

		result.setUsername( username );
		result.setPassword( password );

		return result;
	}

	public static TerminalSession newTerminalSession( URI uri )
	{
		return newTerminalSession( uri.getScheme(), uri.getHost(), uri.getPort() );
	}

	public static TerminalSession newTerminalSession( Device<?, ?> device, ParameterMap config, String prefix )
	{
		String host = null;
		int port = 0;

		if ( device != null )
		{
			List<InetSocketAddress> hosts = device.getHosts();
			if ( hosts != null && hosts.size() > 0 )
			{
				InetSocketAddress socketAddress = hosts.get( 0 );
				host = socketAddress.getHostName();
				port = socketAddress.getPort();
			}
		}

		host = config.get( prefix + "host", host );
		port = config.getInt( prefix + "port", port );
		
		host = host.trim();

		String type = config.get( prefix + "protocol", "telnet" );

		String username;
		String password;

		if ( device != null )
		{
			username = device.getUsername();
			password = device.getPassword();
		}

		username = config.get( prefix + "username", device.getUsername() );
		password = config.get( prefix + "password", device.getPassword() );

		TerminalSession result = TerminalSession.newTerminalSession( type, host, port, username, password );
		
		result.setSourceHost( config.get( prefix + "sourceHost", null ) );
		result.setSourcePort( config.getInt( prefix + "sourcePort", 0 ) );

		String endSequence = config.get( prefix + "endSequence", null );

		result.setEndSequence( endSequence );

		return result;
	}

	/**
	 * Дополнительная конфигурация.
	 * @param config
	 */
    public void setConfiguration( ParameterMap config )
    {
    }
}
