# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*-
# vi: set ft=python sts=4 ts=4 sw=4 noet :
#
# This file is part of Fail2Ban.
#
# Fail2Ban is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# Fail2Ban is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Fail2Ban; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
__author__ = "Fail2Ban Developers"
__copyright__ = "Copyright (c) 2004-2008 Cyril Jaquier, 2012-2014 Yaroslav Halchenko, 2014-2016 Serg G. Brester"
__license__ = "GPL"

import getopt
import logging
import os
import sys

from ..version import version, normVersion
from ..protocol import printFormatted
from ..helpers import getLogger, str2LogLevel, getVerbosityFormat, BrokenPipeError

# Gets the instance of the logger.
logSys = getLogger("fail2ban")

def output(s): # pragma: no cover
	try:
		print(s)
	except (BrokenPipeError, IOError) as e: # pragma: no cover
		if e.errno != 32: # closed / broken pipe
			raise

# Config parameters required to start fail2ban which can be also set via command line (overwrite fail2ban.conf),
CONFIG_PARAMS = ("socket", "pidfile", "logtarget", "loglevel", "syslogsocket")
# Used to signal - we are in test cases (ex: prevents change logging params, log capturing, etc)
PRODUCTION = True

MAX_WAITTIME = 30


class Fail2banCmdLine():

	def __init__(self):
		self._argv = self._args = None
		self._configurator = None
		self.cleanConfOnly = False
		self.resetConf()

	def resetConf(self):
		self._conf = {
		  "async": False,
			"conf": "/etc/fail2ban",
			"force": False,
			"background": True,
			"verbose": 1,
			"socket": None,
			"pidfile": None,
			"timeout": MAX_WAITTIME
		}

	@property
	def configurator(self):
		if self._configurator:
			return self._configurator
		# New configurator
		from .configurator import Configurator
		self._configurator = Configurator()
		# Set the configuration path
		self._configurator.setBaseDir(self._conf["conf"])
		return self._configurator


	def applyMembers(self, obj):
		for o in obj.__dict__:
			self.__dict__[o] = obj.__dict__[o]

	def dispVersion(self, short=False):
		if not short:
			output("Fail2Ban v" + version)
		else:
			output(normVersion())

	def dispUsage(self):
		""" Prints Fail2Ban command line options and exits
		"""
		caller = os.path.basename(self._argv[0])
		output("Usage: "+caller+" [OPTIONS]" + (" <COMMAND>" if not caller.endswith('server') else ""))
		output("")
		output("Fail2Ban v" + version + " reads log file that contains password failure report")
		output("and bans the corresponding IP addresses using firewall rules.")
		output("")
		output("Options:")
		output("    -c, --conf <DIR>        configuration directory")
		output("    -s, --socket <FILE>     socket path")
		output("    -p, --pidfile <FILE>    pidfile path")
		output("    --pname <NAME>          name of the process (main thread) to identify instance (default fail2ban-server)")
		output("    --loglevel <LEVEL>      logging level")
		output("    --logtarget <TARGET>    logging target, use file-name or stdout, stderr, syslog or sysout.")
		output("    --syslogsocket auto|<FILE>")
		output("    -d                      dump configuration. For debugging")
		output("    --dp, --dump-pretty     dump the configuration using more human readable representation")
		output("    -t, --test              test configuration (can be also specified with start parameters)")
		output("    -i                      interactive mode")
		output("    -v                      increase verbosity")
		output("    -q                      decrease verbosity")
		output("    -x                      force execution of the server (remove socket file)")
		output("    -b                      start server in background (default)")
		output("    -f                      start server in foreground")
		output("    --async                 start server in async mode (for internal usage only, don't read configuration)")
		output("    --timeout               timeout to wait for the server (for internal usage only, don't read configuration)")
		output("    --str2sec <STRING>      convert time abbreviation format to seconds")
		output("    -h, --help              display this help message")
		output("    -V, --version           print the version (-V returns machine-readable short format)")

		if not caller.endswith('server'):
			output("")
			output("Command:")
			# Prints the protocol
			printFormatted()

		output("")
		output("Report bugs to https://github.com/fail2ban/fail2ban/issues")

	def __getCmdLineOptions(self, optList):
		""" Gets the command line options
		"""
		for opt in optList:
			o = opt[0]
			if o in ("-c", "--conf"):
				self._conf["conf"] = opt[1]
			elif o in ("-s", "--socket"):
				self._conf["socket"] = opt[1]
			elif o in ("-p", "--pidfile"):
				self._conf["pidfile"] = opt[1]
			elif o in ("-d", "--dp", "--dump-pretty"):
				self._conf["dump"] = True if o == "-d" else 2
			elif o in ("-t", "--test"):
				self.cleanConfOnly = True
				self._conf["test"] = True
			elif o == "-v":
				self._conf["verbose"] += 1
			elif o == "-q":
				self._conf["verbose"] -= 1
			elif o == "-x":
				self._conf["force"] = True
			elif o == "-i":
				self._conf["interactive"] = True
			elif o == "-b":
				self._conf["background"] = True
			elif o == "-f":
				self._conf["background"] = False
			elif o == "--async":
				self._conf["async"] = True
			elif o == "--timeout":
				from ..server.mytime import MyTime
				self._conf["timeout"] = MyTime.str2seconds(opt[1])
			elif o == "--str2sec":
				from ..server.mytime import MyTime
				output(MyTime.str2seconds(opt[1]))
				return True
			elif o in ("-h", "--help"):
				self.dispUsage()
				return True
			elif o in ("-V", "--version"):
				self.dispVersion(o == "-V")
				return True
			elif o.startswith("--"): # other long named params (see also resetConf)
				self._conf[ o[2:] ] = opt[1]
		return None

	def initCmdLine(self, argv):
		verbose = 1
		try:
			# First time?
			initial = (self._argv is None)

			# Command line options
			self._argv = argv
			logSys.info("Using start params %s", argv[1:])

			# Reads the command line options.
			try:
				cmdOpts = 'hc:s:p:xfbdtviqV'
				cmdLongOpts = ['loglevel=', 'logtarget=', 'syslogsocket=', 'test', 'async',
					'conf=', 'pidfile=', 'pname=', 'socket=',
					'timeout=', 'str2sec=', 'help', 'version', 'dp', 'dump-pretty']
				optList, self._args = getopt.getopt(self._argv[1:], cmdOpts, cmdLongOpts)
			except getopt.GetoptError:
				self.dispUsage()
				return False

			ret = self.__getCmdLineOptions(optList)
			if ret is not None:
				return ret

			logSys.debug("  conf: %r, args: %r", self._conf, self._args)

			if initial and PRODUCTION: # pragma: no cover - can't test
				verbose = self._conf["verbose"]
				if verbose <= 0:
					logSys.setLevel(logging.ERROR)
				elif verbose == 1:
					logSys.setLevel(logging.WARNING)
				elif verbose == 2:
					logSys.setLevel(logging.INFO)
				elif verbose == 3:
					logSys.setLevel(logging.DEBUG)
				else:
					logSys.setLevel(logging.HEAVYDEBUG)
				# Add the default logging handler to dump to stderr
				logout = logging.StreamHandler(sys.stderr)

				# Custom log format for the verbose run (-1, because default verbosity here is 1):
				fmt = getVerbosityFormat(verbose-1)
				formatter = logging.Formatter(fmt)
				# tell the handler to use this format
				logout.setFormatter(formatter)
				logSys.addHandler(logout)

			# Set expected parameters (like socket, pidfile, etc) from configuration,
			# if those not yet specified, in which read configuration only if needed here:
			conf = None
			for o in CONFIG_PARAMS:
				if self._conf.get(o, None) is None:
					if not conf:
						self.configurator.readEarly()
						conf = self.configurator.getEarlyOptions()
					if o in conf:
						self._conf[o] = conf[o]

			logSys.info("Using socket file %s", self._conf["socket"])

			# Check log-level before start (or transmit to server), to prevent error in background:
			llev = str2LogLevel(self._conf["loglevel"])
			logSys.info("Using pid file %s, [%s] logging to %s",
				self._conf["pidfile"], logging.getLevelName(llev), self._conf["logtarget"])

			readcfg = True
			if self._conf.get("dump", False):
				if readcfg:
					ret, stream = self.readConfig()
					readcfg = False
				if stream is not None:
					self.dumpConfig(stream, self._conf["dump"] == 2)
				else: # pragma: no cover
					output("ERROR: The configuration stream failed because of the invalid syntax.")
				if not self._conf.get("test", False):
					return ret

			if self._conf.get("test", False):
				if readcfg:
					readcfg = False
					ret, stream = self.readConfig()
				if not ret:
					raise ServerExecutionException("ERROR: test configuration failed")
				# exit after test if no commands specified (test only):
				if not len(self._args):
					output("OK: configuration test is successful")
					return ret

			# Nothing to do here, process in client/server
			return None
		except ServerExecutionException:
			raise
		except Exception as e:
			output("ERROR: %s" % (e,))
			if verbose > 2:
				logSys.exception(e)
			return False

	def readConfig(self, jail=None):
		# Read the configuration
		# TODO: get away from stew of return codes and exception
		# handling -- handle via exceptions
		stream = None
		try:
			self.configurator.Reload()
			self.configurator.readAll()
			ret = self.configurator.getOptions(jail, self._conf, 
				ignoreWrong=not self.cleanConfOnly)
			self.configurator.convertToProtocol(
				allow_no_files=self._conf.get("dump", False))
			stream = self.configurator.getConfigStream()
		except Exception as e:
			logSys.error("Failed during configuration: %s" % e)
			ret = False
		return ret, stream

	@staticmethod
	def dumpConfig(cmd, pretty=False):
		if pretty:
			from pprint import pformat
			def _output(s):
				output(pformat(s, width=1000, indent=2))
		else:
			_output = output
		for c in cmd:
			_output(c)
		return True

	#
	# _exit is made to ease mocking out of the behaviour in tests,
	# since method is also exposed in API via globally bound variable
	@staticmethod
	def _exit(code=0):
		# implicit flush without to produce broken pipe error (32):
		sys.stderr.close()
		try:
			sys.stdout.flush()
			# exit:
			if hasattr(sys, 'exit') and sys.exit:
				sys.exit(code)
			else:
				os._exit(code)
		except (BrokenPipeError, IOError) as e: # pragma: no cover
			if e.errno != 32: # closed / broken pipe
				raise

	@staticmethod
	def exit(code=0):
		logSys.debug("Exit with code %s", code)
		# because of possible buffered output in python, we should flush it before exit:
		logging.shutdown()
		# exit
		Fail2banCmdLine._exit(code)


# global exit handler:
exit = Fail2banCmdLine.exit


class ExitException(Exception):
	pass


class ServerExecutionException(Exception):
	pass
