# -*- encoding: utf-8 -*-

import os, sys, time, signal
import random, string

import daemon
from daemon.pidlockfile import PIDLockFile
from multiprocessing import Pool as ProcessPool
from threading import Lock
import tempfile

from logger import *
from task_manager import TaskManager
import method_pickling

class ServiceManager:
	"""This class is a service manager - responsible for using other classes to run the
		main service loop, ask the tasker for new tasks and run new task processes."""
	def __init__(self, service, config, logfile_path=None, logging_lvl=INFO, run_as_daemon=False, pidfile=None):
		"""Constructor
			service - instance of Service class, which has `process` function
			config - dictionary of options, see service.py or config.ini for details
			logger - instance of logger, see logger.py for details
			run_as_daemon - boolean flag. Set to True if you want the service to run in background
							(daemon mode)
			pidfile - path to PIDfile
		"""
		self.service = service
		self.config = config

		if logfile_path is None:
			logfile_path = self.config['error_dir']
		logfile_path = os.path.join(logfile_path, 'service.log')
		self.logger = Logger(logging_lvl, logfile_path, self.config['error_dir'], not run_as_daemon)

		self.task_manager = TaskManager(self.config, self.logger)
		self.daemon = run_as_daemon
		self._pidfile = pidfile

		self._active_processes = 0
		self._active_proc_lock = Lock()

	def run(self):
		"""Runs the service and ensures logs are written to main logfile."""
		self.logger.log(INFO, "Service is starting.")
		try:
			if self.daemon:
				self.logger.log(DEBUG, "Starting as daemon")
				with self._create_daemon_context(self._pidfile) as ctx:
					self._run_loop()
			else:
				self._run_loop()
		except Exception as e:
			self.logger.log(ERROR, e)
		finally:
			self.logger.log(INFO, "Service is exiting.")

	def _run_loop(self):
		"""Main service loop. It checks for new tasks repetitively, and if new task is available
			schedules a task to take care of bussiness ;)
		"""
		# ProcessPool is initialized here, because daemon context
		# closes ALL file descriptors on __enter__
		pool = ProcessPool(processes=self.config['workers'])

		while True:
			token = None
			if self._active_processes < pool._processes:
				task_file, task_options = self.task_manager.get_new_task()
				if task_file is not None:
					token = self.task_manager.get_task_token(task_file)
					self.logger.log(INFO, "New task :" + token)
					self._schedule_task(pool, task_file, task_options)
			if token is None:
				time.sleep(self.config['sleep'])

	def _schedule_task(self, process_pool, task_file, task_options):
		"""Task scheduling method - takes task file and options and runs a process
			from process pool.
			Also if the subprocess raises an exception it is logged and not thrown further.
		"""
		def _process_callback(result):
			if isinstance(result, Exception):
				finished_token = self.task_manager.get_task_token(result.task_file)
				self.logger.task_log(finished_token, ERROR, "Task " + finished_token + " raised an exception!")
				self.logger.task_log(finished_token, ERROR, result)
			else:
				finished_token = self.task_manager.get_task_token(result)
			self.task_manager.finish_task(finished_token)
			self._decrease_active_count()

		output_file = self.task_manager.get_task_done_path(task_file)
		process_pool.apply_async(self.service._process_handler,
						(task_file, task_options, output_file), {},
						_process_callback)
		self._increase_active_count()

	def _increase_active_count(self):
		self._update_active_count(1)
	def _decrease_active_count(self):
		self._update_active_count(-1)
	def _update_active_count(self, amount):
		self._active_proc_lock.acquire()
		self._active_processes += amount
		self._active_proc_lock.release()

	def _create_daemon_context(self, pidfile=None):
		"""Creates daemon context as sepcified in daemon package. Needed when running in background"""
		pidfile = self._create_pidfile_path(pidfile)
		self.logger.log(DEBUG, "PID file name: " + pidfile)

		ctx = daemon.DaemonContext()
		ctx.pidfile = PIDLockFile(pidfile)
		ctx.detach_process = True
		ctx.umask = 0o022
		ctx.stdin = sys.stdin
		ctx.stdout = sys.stdout
		ctx.stderr = sys.stderr
		ctx.working_directory = os.path.dirname(os.path.abspath(__file__))
		ctx.files_preserve = [self.logger.main_logfile_descr]

		ctx.signal_map = {
			signal.SIGTERM: exit,
			signal.SIGHUP: exit,
			signal.SIGTTOU: exit,
			signal.SIGTTIN: exit,
			signal.SIGTSTP: exit
		}
		return ctx

	def _create_pidfile_path(self, pidfile):
		"""Checks if there is a pidfile given, and if not creates almost-random filename for it."""
		if pidfile is None:
			self.logger.log(DEBUG, "Creating tempfile name for pidfile")
			tempfile_prefix = "/tmp/service_" + self.config['tool'] + "_"
			tempfile_sufix = ".pid"
			while pidfile is None or pidfile in os.listdir("/tmp"):
				random_letters = [random.choice(string.ascii_letters + string.digits) for _ in range(10)]
				random_string = ''.join(random_letters)
				pidfile = tempfile_prefix + random_string + tempfile_sufix

		return pidfile
