"""The multi-process analysis worker process."""

import threading

from plaso.analysis import mediator as analysis_mediator
from plaso.containers import tasks
from plaso.lib import definitions
from plaso.lib import errors
from plaso.multi_process import logger
from plaso.multi_process import plaso_queue
from plaso.multi_process import task_process


class AnalysisProcess(task_process.MultiProcessTaskProcess):
    """Multi-processing analysis worker process."""

    # Number of seconds to wait for the completion status to be queried
    # by the foreman process.
    _FOREMAN_STATUS_WAIT = 5 * 60

    def __init__(
        self,
        event_queue,
        analysis_plugin,
        processing_configuration,
        user_accounts,
        data_location=None,
        event_filter_expression=None,
        **kwargs,
    ):
        """Initializes an analysis worker process.

        Non-specified keyword arguments (kwargs) are directly passed to
        multiprocessing.Process.

        Args:
          event_queue (plaso_queue.Queue): event queue.
          analysis_plugin (AnalysisPlugin): plugin running in the process.
          processing_configuration (ProcessingConfiguration): processing
              configuration.
          user_accounts (list[UserAccountArtifact]): user accounts.
          data_location (Optional[str]): path to the location that data files
              should be loaded from.
          event_filter_expression (Optional[str]): event filter expression.
        """
        super().__init__(processing_configuration, **kwargs)
        self._abort = False
        self._analysis_mediator = None
        self._analysis_plugin = analysis_plugin
        self._data_location = data_location
        self._event_filter_expression = event_filter_expression
        self._event_queue = event_queue
        self._foreman_status_wait_event = None
        self._number_of_consumed_events = 0
        self._status = definitions.STATUS_INDICATOR_INITIALIZED
        self._task = None
        self._user_accounts = user_accounts

    def _GetStatus(self):
        """Retrieves status information.

        Returns:
          dict[str, object]: status attributes, indexed by name.
        """
        logger.debug("Status update requested")

        if self._analysis_mediator:
            number_of_produced_event_tags = (
                self._analysis_mediator.number_of_produced_event_tags
            )
            number_of_produced_reports = (
                self._analysis_mediator.number_of_produced_analysis_reports
            )
        else:
            number_of_produced_event_tags = None
            number_of_produced_reports = None

        if self._process_information:
            used_memory = self._process_information.GetUsedMemory() or 0
        else:
            used_memory = 0

        if self._memory_profiler:
            self._memory_profiler.Sample("main", used_memory)

        status = {
            "display_name": "",
            "identifier": self._name,
            "number_of_consumed_event_data": None,
            "number_of_consumed_event_tags": None,
            "number_of_consumed_events": self._number_of_consumed_events,
            "number_of_consumed_reports": None,
            "number_of_consumed_sources": None,
            "number_of_produced_event_data": None,
            "number_of_produced_event_tags": number_of_produced_event_tags,
            "number_of_produced_events": None,
            "number_of_produced_reports": number_of_produced_reports,
            "number_of_produced_sources": None,
            "processing_status": self._status,
            "task_identifier": None,
            # XML RPC does not support integer values > 2 GiB so we format them
            # as a string.
            "used_memory": f"{used_memory:d}",
        }

        if self._status in (
            definitions.STATUS_INDICATOR_ABORTED,
            definitions.STATUS_INDICATOR_COMPLETED,
        ):
            logger.debug("Set foreman status wait event")
            self._foreman_status_wait_event.set()

        return status

    def _Main(self):
        """The main loop."""
        self._StartProfiling(self._processing_configuration.profiling)

        logger.debug(f"Analysis plugin: {self._name:s} (PID: {self._pid:d}) started.")

        # Creating the threading event in the constructor will cause a pickle
        # error on Windows when an analysis process is created.
        self._foreman_status_wait_event = threading.Event()
        self._status = definitions.STATUS_INDICATOR_ANALYZING

        task = tasks.Task()
        task.storage_format = definitions.STORAGE_FORMAT_SQLITE
        # TODO: temporary solution.
        task.identifier = self._analysis_plugin.plugin_name

        self._task = task

        task_storage_writer = self._storage_factory.CreateTaskStorageWriter(
            definitions.STORAGE_FORMAT_SQLITE
        )

        if self._serializers_profiler:
            task_storage_writer.SetSerializersProfiler(self._serializers_profiler)

        if self._storage_profiler:
            task_storage_writer.SetStorageProfiler(self._storage_profiler)

        storage_file_path = self._GetTaskStorageFilePath(
            definitions.STORAGE_FORMAT_SQLITE, task
        )
        task_storage_writer.Open(path=storage_file_path)

        self._analysis_mediator = analysis_mediator.AnalysisMediator(
            data_location=self._data_location, user_accounts=self._user_accounts
        )

        # TODO: move into analysis process.
        self._analysis_mediator.SetStorageWriter(task_storage_writer)

        # TODO: set event_filter_expression in mediator.

        task_storage_writer.AddAttributeContainer(task)

        try:
            logger.debug(
                (
                    f"{self._name:s} (PID: {self._pid:d}) started monitoring event "
                    f"queue."
                )
            )

            while not self._abort:
                try:
                    queued_object = self._event_queue.PopItem()

                except (errors.QueueClose, errors.QueueEmpty) as exception:
                    exception_type = type(exception)
                    logger.debug(
                        f"ConsumeItems exiting with exception {exception_type!s}."
                    )
                    break

                if isinstance(queued_object, plaso_queue.QueueAbort):
                    logger.debug("ConsumeItems exiting, dequeued QueueAbort object.")
                    break

                self._ProcessEventTripple(self._analysis_mediator, queued_object)

                self._number_of_consumed_events += 1

            logger.debug(
                (
                    f"{self._name:s} (PID: {self._pid:d}) stopped monitoring event "
                    f"queue."
                )
            )

            if not self._abort:
                self._status = definitions.STATUS_INDICATOR_REPORTING

                self._analysis_mediator.ProduceAnalysisReport(self._analysis_plugin)

        # All exceptions need to be caught here to prevent the process
        # from being killed by an uncaught exception.
        except Exception as exception:  # pylint: disable=broad-except
            logger.warning(
                (
                    f"Unhandled exception in process: {self._name:s} (PID: "
                    f"{self._pid:d})."
                )
            )
            logger.exception(exception)

            self._abort = True

        finally:
            task.aborted = self._abort
            task_storage_writer.UpdateAttributeContainer(task)

            task_storage_writer.Close()

            if self._serializers_profiler:
                task_storage_writer.SetSerializersProfiler(None)

            if self._storage_profiler:
                task_storage_writer.SetStorageProfiler(None)

        try:
            self._FinalizeTaskStorageWriter(definitions.STORAGE_FORMAT_SQLITE, task)
        except OSError as exception:
            logger.warning(f"Unable to finalize task storage with error: {exception!s}")

        if self._abort:
            self._status = definitions.STATUS_INDICATOR_ABORTED
        else:
            self._status = definitions.STATUS_INDICATOR_COMPLETED

        logger.debug("Wait for foreman status wait event")
        self._foreman_status_wait_event.clear()
        self._foreman_status_wait_event.wait(self._FOREMAN_STATUS_WAIT)

        logger.debug(f"Analysis plugin: {self._name:s} (PID: {self._pid:d}) stopped.")

        self._StopProfiling()

        self._analysis_mediator = None
        self._foreman_status_wait_event = None
        self._task = None

        try:
            self._event_queue.Close(abort=self._abort)
        except errors.QueueAlreadyClosed:
            logger.error(f"Queue for {self.name:s} was already closed.")

    def _ProcessEventTripple(self, mediator, event_tripple):
        """Processes an event tripple.

        Args:
          mediator (AnalysisMediator): mediates interactions between
              analysis plugins and other components, such as storage and dfVFS.
          event_tripple (EventTripple): event tripple.
        """
        try:
            self._analysis_plugin.ExamineEvent(
                mediator,
                event_tripple.event,
                event_tripple.event_data,
                event_tripple.event_data_stream,
            )

        except Exception as exception:  # pylint: disable=broad-except
            # TODO: write analysis error and change logger to debug only.

            logger.warning("Unhandled exception while processing event object.")
            logger.exception(exception)

    def SignalAbort(self):
        """Signals the process to abort."""
        self._abort = True

        if self._foreman_status_wait_event:
            logger.debug("Abort foreman status wait event")
            self._foreman_status_wait_event.set()

        if self._analysis_mediator:
            self._analysis_mediator.SignalAbort()
