"""The processing engine."""

import os

from artifacts import errors as artifacts_errors
from artifacts import reader as artifacts_reader
from artifacts import registry as artifacts_registry

from dfvfs.lib import errors as dfvfs_errors
from dfvfs.path import factory as path_spec_factory
from dfvfs.resolver import resolver as path_spec_resolver

from plaso.containers import artifacts
from plaso.containers import sessions
from plaso.engine import artifact_filters
from plaso.engine import knowledge_base
from plaso.engine import logger
from plaso.engine import path_filters
from plaso.engine import processing_status
from plaso.engine import profilers
from plaso.engine import yaml_filter_file
from plaso.lib import errors
from plaso.preprocessors import manager as preprocess_manager
from plaso.preprocessors import mediator as preprocess_mediator


class BaseEngine:
    """Processing engine interface.

    Attributes:
      knowledge_base (KnowledgeBase): knowledge base.
    """

    _WINDOWS_REGISTRY_FILES_ARTIFACT_NAMES = [
        "WindowsSystemRegistryFiles",
        "WindowsUserRegistryFiles",
    ]

    def __init__(self):
        """Initializes an engine."""
        super().__init__()
        self._abort = False
        self._analyzers_profiler = None
        self._artifacts_registry = None
        self._artifacts_trie = None
        self._excluded_file_system_find_specs = None
        self._included_file_system_find_specs = None
        self._memory_profiler = None
        self._name = "Main"
        self._processing_status = processing_status.ProcessingStatus()
        self._processing_profiler = None
        self._registry_find_specs = None
        self._serializers_profiler = None
        # The interval of status updates in number of seconds.
        self._status_update_interval = 0.5
        self._storage_profiler = None
        self._task_queue_profiler = None

        self.knowledge_base = knowledge_base.KnowledgeBase()

    def _StartProfiling(self, configuration):
        """Starts profiling.

        Args:
          configuration (ProfilingConfiguration): profiling configuration.
        """
        if not configuration:
            return

        if configuration.HaveProfileMemory():
            self._memory_profiler = profilers.MemoryProfiler(self._name, configuration)
            self._memory_profiler.Start()

        if configuration.HaveProfileAnalyzers():
            self._analyzers_profiler = profilers.AnalyzersProfiler(
                f"{self._name:s}-analyzers", configuration
            )
            self._analyzers_profiler.Start()

        if configuration.HaveProfileProcessing():
            self._processing_profiler = profilers.ProcessingProfiler(
                f"{self._name:s}-processing", configuration
            )
            self._processing_profiler.Start()

        if configuration.HaveProfileSerializers():
            self._serializers_profiler = profilers.SerializersProfiler(
                f"{self._name:s}-serializers", configuration
            )
            self._serializers_profiler.Start()

        if configuration.HaveProfileStorage():
            self._storage_profiler = profilers.StorageProfiler(
                self._name, configuration
            )
            self._storage_profiler.Start()

        if configuration.HaveProfileTaskQueue():
            self._task_queue_profiler = profilers.TaskQueueProfiler(
                self._name, configuration
            )
            self._task_queue_profiler.Start()

    def _StopProfiling(self):
        """Stops profiling."""
        if self._memory_profiler:
            self._memory_profiler.Stop()
            self._memory_profiler = None

        if self._analyzers_profiler:
            self._analyzers_profiler.Stop()
            self._analyzers_profiler = None

        if self._processing_profiler:
            self._processing_profiler.Stop()
            self._processing_profiler = None

        if self._serializers_profiler:
            self._serializers_profiler.Stop()
            self._serializers_profiler = None

        if self._storage_profiler:
            self._storage_profiler.Stop()
            self._storage_profiler = None

        if self._task_queue_profiler:
            self._task_queue_profiler.Stop()
            self._task_queue_profiler = None

    def BuildArtifactsRegistry(self, artifact_definitions_path, custom_artifacts_path):
        """Builds an artificats definition registry.

        Args:
          artifact_definitions_path (str): path to artifact definitions directory
              or file.
          custom_artifacts_path (str): path to custom artifact definitions
              directory or file.

        Raises:
          BadConfigOption: if artifact definitions cannot be read.
        """
        if not artifact_definitions_path:
            raise errors.BadConfigOption("Missing artifact definitions path.")

        registry = artifacts_registry.ArtifactDefinitionsRegistry()
        reader = artifacts_reader.YamlArtifactsReader()

        try:
            if os.path.isdir(artifact_definitions_path):
                registry.ReadFromDirectory(reader, artifact_definitions_path)
            else:
                registry.ReadFromFile(reader, artifact_definitions_path)

        except (KeyError, artifacts_errors.FormatError) as exception:
            raise errors.BadConfigOption(
                (
                    f"Unable to read artifact definitions from: "
                    f"{artifact_definitions_path:s} with error: {exception!s}"
                )
            )

        if custom_artifacts_path:
            try:
                if os.path.isdir(custom_artifacts_path):
                    registry.ReadFromDirectory(reader, custom_artifacts_path)
                else:
                    registry.ReadFromFile(reader, custom_artifacts_path)

            except (KeyError, artifacts_errors.FormatError) as exception:
                raise errors.BadConfigOption(
                    (
                        f"Unable to read custom artifact definitions from: "
                        f"{custom_artifacts_path:s} with error: {exception!s}"
                    )
                )

        self._artifacts_registry = registry

    def BuildCollectionFilters(
        self,
        environment_variables,
        user_accounts,
        artifact_filter_names=None,
        filter_file_path=None,
        enable_artifacts_map=False,
    ):
        """Builds collection filters from artifacts or filter file if available.

        Args:
          environment_variables (list[EnvironmentVariableArtifact]):
              environment variables.
          user_accounts (list[UserAccountArtifact]): user accounts.
          artifact_filter_names (Optional[list[str]]): names of artifact
              definitions that are used for filtering file system and Windows
              Registry key paths.
          filter_file_path (Optional[str]): path of filter file.
          enable_artifacts_map (Optional[bool]): True if the artifacts path map
              should be generated. Defaults to False.

        Raises:
          InvalidFilter: if no valid file system find specifications are built.
        """
        filters_helper = None

        if artifact_filter_names:
            names = ", ".join(artifact_filter_names)
            logger.debug(f"building find specification based on artifacts: {names:s}")

            filters_helper = artifact_filters.ArtifactDefinitionsFiltersHelper(
                self._artifacts_registry
            )
            filters_helper.BuildFindSpecs(
                artifact_filter_names,
                environment_variables=environment_variables,
                user_accounts=user_accounts,
                enable_artifacts_map=enable_artifacts_map,
            )

            # If the user selected Windows Registry artifacts we have to ensure
            # the Windows Registry files are parsed.
            if filters_helper.registry_find_specs:
                filters_helper.BuildFindSpecs(
                    self._WINDOWS_REGISTRY_FILES_ARTIFACT_NAMES,
                    environment_variables=environment_variables,
                    user_accounts=user_accounts,
                    enable_artifacts_map=enable_artifacts_map,
                    original_registry_artifact_filter_names=(
                        filters_helper.registry_find_specs_artifact_names
                    ),
                )

            if not filters_helper.file_system_find_specs:
                raise errors.InvalidFilter(
                    "No valid file system find specifications were built from "
                    "artifacts."
                )

            self._included_file_system_find_specs = (
                filters_helper.file_system_find_specs
            )
            self._registry_find_specs = filters_helper.registry_find_specs

            self._artifacts_trie = filters_helper.artifacts_trie

        elif filter_file_path:
            logger.debug(
                (
                    f"building find specification based on filter file: "
                    f"{filter_file_path:s}"
                )
            )

            filter_file_object = yaml_filter_file.YAMLFilterFile()
            filter_file_path_filters = filter_file_object.ReadFromFile(filter_file_path)

            filters_helper = path_filters.PathCollectionFiltersHelper()
            filters_helper.BuildFindSpecs(
                filter_file_path_filters, environment_variables=environment_variables
            )

            if (
                not filters_helper.excluded_file_system_find_specs
                and not filters_helper.included_file_system_find_specs
            ):
                raise errors.InvalidFilter(
                    f"No valid file system find specifications were built from filter "
                    f"file: {filter_file_path:s}."
                )

            self._excluded_file_system_find_specs = (
                filters_helper.excluded_file_system_find_specs
            )
            self._included_file_system_find_specs = (
                filters_helper.included_file_system_find_specs
            )

    # pylint: disable=too-many-arguments
    @classmethod
    def CreateSession(
        cls,
        artifact_filter_names=None,
        command_line_arguments=None,
        debug_mode=False,
        filter_file_path=None,
        preferred_encoding="utf-8",
    ):
        """Creates a session attribute container.

        Args:
          artifact_filter_names (Optional[list[str]]): names of artifact definitions
              that are used for filtering file system and Windows Registry
              key paths.
          command_line_arguments (Optional[str]): the command line arguments.
          debug_mode (Option[bool]): True if debug mode was enabled.
          filter_file_path (Optional[str]): path to a file with find specifications.
          preferred_encoding (Optional[str]): preferred encoding.

        Returns:
          Session: session attribute container.
        """
        session = sessions.Session()

        session.artifact_filters = artifact_filter_names
        session.command_line_arguments = command_line_arguments
        session.debug_mode = debug_mode
        session.filter_file = filter_file_path
        session.preferred_encoding = preferred_encoding

        return session

    def GetArtifactsTrie(self):
        """Retrieves the artifacts trie.

        Returns:
          ArtifactsTrie: artifacts trie.
        """
        return self._artifacts_trie

    def GetCollectionExcludedFindSpecs(self):
        """Retrieves find specifications to exclude from collection.

        Returns:
          list[dfvfs.FindSpec]: find specifications to exclude from collection.
        """
        return self._excluded_file_system_find_specs or []

    def GetCollectionIncludedFindSpecs(self):
        """Retrieves find specifications to include in collection.

        Returns:
          list[dfvfs.FindSpec]: find specifications to include in collection.
        """
        return self._included_file_system_find_specs or []

    def GetSourceFileSystem(self, file_system_path_spec, resolver_context=None):
        """Retrieves the file system of the source.

        Args:
          file_system_path_spec (dfvfs.PathSpec): path specifications of
              the source file system to process.
          resolver_context (dfvfs.Context): resolver context.

        Returns:
          tuple[dfvfs.FileSystem, path.PathSpec]: file system and mount point path
              specification. The mount point path specification refers to either a
              directory or a volume on a storage media device or image. It is
              needed by the dfVFS file system to indicate the base location of the
              file system.

        Raises:
          RuntimeError: if source file system path specification is not set.
        """
        if not file_system_path_spec:
            raise RuntimeError("Missing source file system path specification.")

        file_system = path_spec_resolver.Resolver.OpenFileSystem(
            file_system_path_spec, resolver_context=resolver_context
        )

        type_indicator = file_system_path_spec.type_indicator
        if path_spec_factory.Factory.IsSystemLevelTypeIndicator(type_indicator):
            mount_point = file_system_path_spec
        else:
            mount_point = file_system_path_spec.parent

        return file_system, mount_point

    def PreprocessSource(
        self, file_system_path_specs, storage_writer, resolver_context=None
    ):
        """Preprocesses a source.

        Args:
          file_system_path_specs (list[dfvfs.PathSpec]): path specifications of
              the source file systems to process.
          storage_writer (StorageWriter): storage writer.
          resolver_context (Optional[dfvfs.Context]): resolver context.

        Returns:
          list[SystemConfigurationArtifact]: system configurations found in
              the source.
        """
        mediator = preprocess_mediator.PreprocessMediator(storage_writer)

        detected_operating_systems = []
        system_configurations = []
        for path_spec in file_system_path_specs:
            try:
                file_system, mount_point = self.GetSourceFileSystem(
                    path_spec, resolver_context=resolver_context
                )
            except (RuntimeError, dfvfs_errors.BackEndError) as exception:
                logger.error(exception)
                continue

            preprocess_manager.PreprocessPluginsManager.RunPlugins(
                self._artifacts_registry, file_system, mount_point, mediator
            )

            operating_system = mediator.GetValue("operating_system")
            if not operating_system:
                continue

            detected_operating_systems.append(operating_system)

            system_configuration = artifacts.SystemConfigurationArtifact(
                code_page=mediator.code_page, language=mediator.language
            )
            # Ensure environment_variables is a list otherwise serialization will
            # fail.
            system_configuration.environment_variables = list(
                mediator.GetEnvironmentVariables()
            )
            system_configuration.hostname = mediator.hostname
            system_configuration.keyboard_layout = mediator.GetValue("keyboard_layout")
            system_configuration.operating_system = mediator.GetValue(
                "operating_system"
            )
            system_configuration.operating_system_product = mediator.GetValue(
                "operating_system_product"
            )
            system_configuration.operating_system_version = mediator.GetValue(
                "operating_system_version"
            )
            # TODO: add support for multi file system system configurations.
            system_configuration.path_specs = [path_spec]

            if mediator.time_zone:
                system_configuration.time_zone = mediator.time_zone.zone

            system_configurations.append(system_configuration)

            mediator.Reset()

        if system_configurations:
            # TODO: kept for backwards compatibility.
            self.knowledge_base.ReadSystemConfigurationArtifact(
                system_configurations[0]
            )
            for environment_variable in system_configuration.environment_variables:
                self.knowledge_base.AddEnvironmentVariable(environment_variable)

        return system_configurations

    def SetStatusUpdateInterval(self, status_update_interval):
        """Sets the status update interval.

        Args:
          status_update_interval (float): status update interval.
        """
        self._status_update_interval = status_update_interval
