/*
 * Copyright 2016 KairosDB Authors
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package com.max256.abhot.core.telnet;

import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.inject.Inject;
import com.google.inject.name.Named;
import org.jboss.netty.bootstrap.ServerBootstrap;
import org.jboss.netty.channel.*;
import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
import org.jboss.netty.handler.codec.frame.DelimiterBasedFrameDecoder;
import org.jboss.netty.handler.codec.frame.Delimiters;
import org.jboss.netty.handler.codec.string.StringEncoder;
import com.max256.abhot.core.KairosDBService;
import com.max256.abhot.core.exception.KairosDBException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.concurrent.Executors;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;

public class TelnetServer extends SimpleChannelUpstreamHandler implements ChannelPipelineFactory,
		KairosDBService
{
	private static final Logger logger = LoggerFactory.getLogger(TelnetServer.class);

	private final int port;
	private final CommandProvider commandProvider;
	private final int maxCommandLength;

	private InetAddress address;
	private ServerBootstrap serverBootstrap;

	public TelnetServer(int port,
			int maxCommandLength,
			CommandProvider commandProvider)
			throws UnknownHostException
	{
		this(null, port, maxCommandLength, commandProvider);
	}

	@Inject
	public TelnetServer(@Named("kairosdb.telnetserver.address") String address,
			@Named("kairosdb.telnetserver.port") int port,
			@Named("kairosdb.telnetserver.max_command_size") int maxCommandLength,
			CommandProvider commandProvider)
			throws UnknownHostException
	{
		checkArgument(maxCommandLength > 0, "command length must be greater than zero");

		this.port = port;
		this.maxCommandLength = maxCommandLength;
		this.commandProvider = checkNotNull(commandProvider);
		this.address = InetAddress.getByName(address);
	}

	@Override
	public ChannelPipeline getPipeline() throws Exception
	{
		ChannelPipeline pipeline = Channels.pipeline();

		// Add the text line codec combination first,
		DelimiterBasedFrameDecoder frameDecoder = new DelimiterBasedFrameDecoder(
				maxCommandLength, Delimiters.lineDelimiter());
		pipeline.addLast("framer", frameDecoder);
		pipeline.addLast("decoder", new WordSplitter());
		pipeline.addLast("encoder", new StringEncoder());

		// and then business logic.
		pipeline.addLast("handler", this);

		return pipeline;
	}

	private String formatMessage(String[] msg)
	{
		StringBuilder sb = new StringBuilder();
		for (String s : msg)
			sb.append(s).append(" ");

		return (sb.toString());
	}

	@Override
	public void messageReceived(final ChannelHandlerContext ctx,
	                            final MessageEvent msgevent)
	{
		final Object message = msgevent.getMessage();
		if (message instanceof String[])
		{
			String[] command = (String[]) message;

			String cmd = "";
			if (command.length >= 1)
				cmd = command[0];

			TelnetCommand telnetCommand = commandProvider.getCommand(cmd);
			if (telnetCommand != null)
			{
				try
				{
					telnetCommand.execute(msgevent.getChannel(), command);
				}
				catch (Exception e)
				{
					log("Message: '" + formatMessage(command) + "'", ctx);
					log("Failed to execute command: " + formatCommand(command) + " Reason: " + e.getMessage(), ctx, e);
				}
			}
			else
			{
				log("Message: '" + formatMessage(command) + "'", ctx);
				log("Unknown command: '" + cmd + "'", ctx);
			}
		}
		else
		{
			log("Message: '" + message.toString() + "'", ctx);
			log("Invalid message. Must be of type String.", ctx);
		}
	}

	private static void log(String message, ChannelHandlerContext ctx)
	{
		log(message, ctx, null);
	}

	private static void log(String message, ChannelHandlerContext ctx, Exception e)
	{
		message += " From: " + ((InetSocketAddress) ctx.getChannel().getRemoteAddress()).getAddress().getHostAddress();
		if (logger.isDebugEnabled())
			if (e != null)
				logger.debug(message, e);
			else
				logger.debug(message);
		else
		{
			logger.warn(message);
		}
	}

	@Override
	public void start() throws KairosDBException
	{
		// Configure the server.
		serverBootstrap = new ServerBootstrap(
				new NioServerSocketChannelFactory(
						Executors.newCachedThreadPool(new ThreadFactoryBuilder().setNameFormat("telnet-boss-%d").build()),
						Executors.newCachedThreadPool(new ThreadFactoryBuilder().setNameFormat("telnet-worker-%d").build())));

		// Configure the pipeline factory.
		serverBootstrap.setPipelineFactory(this);
		serverBootstrap.setOption("child.tcpNoDelay", true);
		serverBootstrap.setOption("child.keepAlive", true);
		serverBootstrap.setOption("reuseAddress", true);

		// Bind and start to accept incoming connections.
		serverBootstrap.bind(new InetSocketAddress(address, port));
	}

	public InetAddress getAddress()
	{
		return address;
	}

	@Override
	public void stop()
	{
		if (serverBootstrap != null)
			serverBootstrap.shutdown();
	}

	private static String formatCommand(String[] command)
	{
		StringBuilder builder = new StringBuilder();
		for (String s : command)
		{
			builder.append(s).append(" ");
		}

		return builder.toString();
	}

	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception
	{
		logger.error("Error in TelnetServer", e.getCause());
	}
}