"""This module handles all the output generated by the Rook.

The output generated by Rook into the following types of messages:
- Rook information describing the rook and the application, usually at startup.
- User messages describing the application's behaviour.
- Log messages describing the rook's behaviour.

Output (except rook information) is batched and forwarded asynchronously to the Agent."""

import sys
import os
import threading
import time
import logging
import six
import traceback

import platform
# calling platform.platform to avoid dead-lock on secondary thread (only Windows OS)
platform.platform()

from rook.config import OutputConfiguration, VersionConfiguration

from .services.bdb_location_service import BdbLocationService

from rook.logger import logger
from rook.logger.namespace_serializer_handler import NamespaceSerializerHandler

from rook import git

from rook.machine import add_info

from rook.processor.namespace_serializer import NamespaceSerializer

from rook.processor.namespaces.container_namespace import ContainerNamespace
from rook.processor.namespaces.python_object_namespace import PythonObjectNamespace

from rook.protobuf import rook_pb2


class Output(object):
    """The Output class manages all Rook's output.

    The object is initialized to a passive state, collecting all the data written to it.
    Only after the object is given a connection to the agent does message sending begin."""

    def __init__(self, trigger_services):
        """Build the output object in passive state."""
        self._trigger_services = trigger_services
        self._rook_id = ''

        self._lock = threading.RLock()
        self._messages = rook_pb2.RookMessages()

        self._agent_com = None

        self._closing = False
        self._thread = None
        self._thread_lock = threading.RLock()

        self._log_limit_reached = False
        self._message_limit_reached = False
        self._status_update_limit_reached = False

        NamespaceSerializerHandler.output = self

        self.tags = []

    def close(self):
        NamespaceSerializerHandler.output = None

        self._stop_thread()

    def set_rook_id(self, rook_id):
        self._rook_id = rook_id

    def set_agent_com(self, agent_com):
        self._agent_com = agent_com

    def start_sending_messages(self):
        """Start the background thread sending data to agent"""
        self._start_thread()

    def stop_sending_messages(self):
        self._stop_thread()

    def get_rook_info(self):
        message = rook_pb2.RookInformation()
        message.rook_id = NamespaceSerializer.normalize_string(self._rook_id)

        # Everything else is basically optional and so we ignore errors in it
        try:
            message.version = NamespaceSerializer.normalize_string(VersionConfiguration.VERSION)
            message.commit = NamespaceSerializer.normalize_string(VersionConfiguration.COMMIT)

            message.platform = "python"
            message.platform_type = NamespaceSerializer.normalize_string(platform.python_implementation())
            message.platform_version = NamespaceSerializer.normalize_string(sys.version)
            message.executable = NamespaceSerializer.normalize_string(sys.argv[0])

            for arg in sys.argv:
                message.command_arguments.append(NamespaceSerializer.normalize_string(arg))

            message.tags.extend(self.tags)

            # Try to get the proper git tag
            try:
                user_commit = os.environ.get('ROOKOUT_COMMIT')
                if not user_commit:
                    git_root = os.environ.get('ROOKOUT_GIT')
                    if not git_root:
                        git_root = git.find_root(os.path.dirname(os.path.abspath(sys.argv[0])))
                    if git_root:
                        user_commit = git.get_revision(git_root)
            except:
                logger.exception("Failed to get git tag")
                user_commit = ""

            if user_commit:
                message.user_commit = NamespaceSerializer.normalize_string(user_commit)
            else:
                message.user_commit = ""

            message.process_id = str(os.getpid())

            add_info(message)
        except:
            logger.exception("Unexpected exception when building rook info")

        return message

    def send_warning(self, rule_id, error):
        self.send_rule_status(rule_id, "Warning", error)

    def send_rule_status(self, rule_id, active, error):
        try:
            if len(self._messages.rule_status_updates) > OutputConfiguration.MAX_STATUS_UPDATES:
                if not self._status_update_limit_reached:
                    logger.error("Limit reached (%d), dropping status updates", OutputConfiguration.MAX_STATUS_UPDATES)
                    self._message_limit_reached = True
                return

            rule_status = self._messages.rule_status_updates.add()
            rule_status.rook_id = NamespaceSerializer.normalize_string(self._rook_id)
            rule_status.rule_id = NamespaceSerializer.normalize_string(rule_id)
            rule_status.active = NamespaceSerializer.normalize_string(active)
            rule_status.time.GetCurrentTime()

            if error:
                error.dump(rule_status.error)
        except:
            logger.exception("Failed to report rule status")

    def send_user_message(self, aug_id, arguments):
        with self._lock:
            if len(self._messages.user_messages) > OutputConfiguration.MAX_ITEMS:
                if not self._message_limit_reached:
                    logger.error("Limit reached (%d), dropping user messages", OutputConfiguration.MAX_ITEMS)
                    self._message_limit_reached = True
                return

            message = self._messages.user_messages.add()
            message.aug_id = NamespaceSerializer.normalize_string(aug_id)
            message.time.GetCurrentTime()

            if arguments and arguments.size(''):
                NamespaceSerializer().dump(arguments, message.arguments)

    def send_log_message(self, level, time, filename, lineno, text, formatted_message, arguments):
        with self._lock:
            if len(self._messages.log_messages) >= OutputConfiguration.MAX_LOG_ITEMS:
                if not self._log_limit_reached:
                    self._log_limit_reached = True

                    try:
                        # We cannot preform a recursive call, so we override original message
                        level = logging.ERROR
                        filename = __file__
                        lineno = 0
                        text = "Limit reached (%d), dropping log messages"
                        formatted_message = text
                        arguments = ContainerNamespace({"args": PythonObjectNamespace(OutputConfiguration.MAX_ITEMS)})
                    except:
                        six.print_("[Rookout] Error when handling output queue size limit", file=sys.stderr)
                        traceback.print_exc()
                        return
                else:
                    return

            message = self._messages.log_messages.add()
            message.level = int(level)
            message.time.FromMilliseconds(int(time * 1000))
            message.filename = NamespaceSerializer.normalize_string(filename)
            message.lineno = int(lineno)
            message.text = NamespaceSerializer.normalize_string(text)
            message.formatted_message = NamespaceSerializer.normalize_string(formatted_message)
            if arguments and arguments.size(''):
                NamespaceSerializer().dump(arguments, message.arguments, log_errors=False)

    def flush_messages(self):
        # Copy messages aside while holding the lock
        with self._lock:
            messages = self._messages
            self._messages = rook_pb2.RookMessages()

        # If we got any messages, send them
        if messages.user_messages or messages.log_messages or messages.rule_status_updates:
            try:
                messages.rook_id = self._rook_id
                self._agent_com.send_rook_messages(messages)

                self._log_limit_reached = False
                self._message_limit_reached = False
                self._status_update_limit_reached = False
            except:
                # Logger might not exist in interpreter shutdown
                if logger:
                    logger.exception("Sending messages to agent failed")

    def _start_thread(self):
        with self._thread_lock:
            if self._thread:
                return

            self._closing = False
            self._thread = threading.Thread(target=self._output_thread, name=OutputConfiguration.THREAD_NAME)
            self._thread.daemon = True
            self._thread.start()

    def _stop_thread(self):
        # If lock is already dead, just return
        if not self._thread_lock:
            return

        with self._thread_lock:
            if self._thread:
                self._closing = True

                # If threading was monkey patched by gevent waiting on thread will likely throw an exception
                try:
                    from gevent.monkey import is_module_patched
                    if is_module_patched("threading"):
                        time.sleep(OutputConfiguration.FLUSH_TIME_INTERVAL)
                except:
                    pass

                self._thread.join()
                self._thread = None

    def _output_thread(self):
        """This is the thread communication sending the batched messages to the agent."""

        # Don't allow this thread to be monitored
        try:
            self._trigger_services.get_service(BdbLocationService.NAME).ignore_current_thread()
        except KeyError:
            pass

        # Until we are asked to terminate
        while not self._closing:
            # Flush messages
            self.flush_messages()

            # If interpreter is not in shutdown
            if time:
                # Wait for next check
                time.sleep(OutputConfiguration.FLUSH_TIME_INTERVAL)
            else:
                break
