"""The output mediator object."""

import glob
import os
import pytz

from plaso.engine import path_helper
from plaso.formatters import manager as formatters_manager
from plaso.formatters import yaml_formatters_file
from plaso.helpers.windows import languages
from plaso.lib import definitions
from plaso.output import winevt_rc


class OutputMediator:
    """Output mediator.

    Attributes:
      data_location (Optional[str]): path of the formatter data files.
      use_fallback_path_spec (bool): use the event data stream path specification as
          fallback for the filename and inode fields.
    """

    _DEFAULT_ENCODING = "utf-8"

    # The default LCID 0x0409, which represents en-US.
    _DEFAULT_LCID = 0x0409

    _DEFAULT_TIME_ZONE = pytz.UTC

    _WINEVT_RC_DATABASE = "winevt-rc.db"

    def __init__(
        self,
        storage_reader,
        data_location=None,
        dynamic_time=False,
        preferred_encoding="utf-8",
        use_fallback_hostname=False,
        use_fallback_path_spec=False,
    ):
        """Initializes an output mediator.

        Args:
          storage_reader (StorageReader): storage reader.
          data_location (Optional[str]): path of the formatter data files.
          dynamic_time (Optional[bool]): True if date and time values should be
              represented in their granularity or semantically.
          preferred_encoding (Optional[str]): preferred encoding to output.
          use_fallback_hostname (Optional[bool]): use the hostname value derived
              pre-processing as fallback for the hostname field.
          use_fallback_path_spec (Optional[bool]): use the event data stream path
              specification as fallback for the filename and inode fields.
        """
        super().__init__()
        self._dynamic_time = dynamic_time
        self._hostname = None
        self._language_tag = None
        self._lcid = None
        self._message_formatters = {}
        self._preferred_encoding = preferred_encoding
        self._source_mappings = {}
        self._storage_reader = storage_reader
        self._system_configurations = None
        self._time_zone = None
        self._use_fallback_hostname = use_fallback_hostname
        self._username_by_identifier = {}

        self.data_location = data_location
        self.use_fallback_path_spec = use_fallback_path_spec

    @property
    def dynamic_time(self):
        """bool: True if dynamic time should be used."""
        return self._dynamic_time

    @property
    def encoding(self):
        """str: preferred encoding to output."""
        return self._preferred_encoding or self._DEFAULT_ENCODING

    @property
    def time_zone(self):
        """datetime.tzinfo: time zone."""
        return self._time_zone or self._DEFAULT_TIME_ZONE

    def _ReadMessageFormattersFile(self, path, override_existing=False):
        """Reads a message formatters configuration file.

        Args:
          path (str): path of file that contains the message formatters configuration.
          override_existing (bool): True if existing message formatters should be
              overridden.

        Raises:
          KeyError: if the message formatter is already set for the corresponding
              data type and override existing is False.
        """
        message_formatters_file = yaml_formatters_file.YAMLFormattersFile()
        for message_formatter in message_formatters_file.ReadFromFile(path):
            if (
                message_formatter.data_type in self._message_formatters
                and not override_existing
            ):
                raise KeyError(
                    f"Message formatter for data type: {message_formatter.data_type:s} "
                    f"already exists"
                )

            for identifier in message_formatter.custom_helpers:
                custom_formatter_helper = (
                    formatters_manager.FormattersManager.GetEventFormatterHelper(
                        identifier
                    )
                )
                if custom_formatter_helper:
                    message_formatter.AddHelper(custom_formatter_helper)

            self._message_formatters[message_formatter.data_type] = message_formatter
            self._source_mappings[message_formatter.data_type] = (
                message_formatter.source_mapping
            )

    def _ReadUserAccount(self, user_identifier):
        """Reads a specific user account from the storage.

        Args:
          user_identifier (str): user identifier (UID or SID).

        Returns:
          UserAccountArtifact: user account or None if not available.
        """
        # TODO: get username related to the source.
        filter_expression = f'identifier == "{user_identifier:s}"'

        user_accounts = list(
            self._storage_reader.GetAttributeContainers(
                "user_account", filter_expression=filter_expression
            )
        )
        if not user_accounts:
            return None

        return user_accounts[0]

    def GetDisplayNameForPathSpec(self, path_spec):
        """Retrieves the display name for a path specification.

        Args:
          path_spec (dfvfs.PathSpec): path specification.

        Returns:
          str: human readable version of the path specification.
        """
        return path_helper.PathHelper.GetDisplayNameForPathSpec(path_spec)

    def GetHostname(self, event_data, default_hostname="-"):
        """Retrieves the hostname related to the event.

        Args:
          event_data (EventData): event data.
          default_hostname (Optional[str]): default hostname.

        Returns:
          str: hostname.
        """
        hostname = getattr(event_data, "hostname", None)
        if hostname:
            return hostname

        if not self._use_fallback_hostname:
            return default_hostname

        if self._system_configurations is None:
            self._system_configurations = list(
                self._storage_reader.GetAttributeContainers("system_configuration")
            )

        # TODO: get hostname related to the source.
        if not self._hostname and self._system_configurations:
            hostname_artifact = self._system_configurations[-1].hostname
            if hostname_artifact:
                self._hostname = hostname_artifact.name

        return self._hostname or default_hostname

    def GetMACBRepresentation(self, event, event_data):
        """Retrieves the MACB representation.

        Args:
          event (EventObject): event.
          event_data (EventData): event data.

        Returns:
          str: MACB representation.
        """
        data_type = getattr(event_data, "data_type", None)
        if not data_type:
            return "...."

        # The filestat parser is somewhat limited.
        if data_type == "fs:stat":
            descriptions = event.timestamp_desc.split(";")

            return_characters = [".", ".", ".", "."]
            for description in descriptions:
                if description in ("mtime", definitions.TIME_DESCRIPTION_MODIFICATION):
                    return_characters[0] = "M"
                elif description in ("atime", definitions.TIME_DESCRIPTION_LAST_ACCESS):
                    return_characters[1] = "A"
                elif description in (
                    "ctime",
                    definitions.TIME_DESCRIPTION_METADATA_MODIFICATION,
                ):
                    return_characters[2] = "C"
                elif description in ("crtime", definitions.TIME_DESCRIPTION_CREATION):
                    return_characters[3] = "B"

            return "".join(return_characters)

        # Access time.
        if event.timestamp_desc in [
            definitions.TIME_DESCRIPTION_LAST_ACCESS,
            definitions.TIME_DESCRIPTION_ACCOUNT_CREATED,
            definitions.TIME_DESCRIPTION_LAST_VISITED,
            definitions.TIME_DESCRIPTION_START,
            definitions.TIME_DESCRIPTION_LAST_SHUTDOWN,
            definitions.TIME_DESCRIPTION_LAST_LOGIN,
            definitions.TIME_DESCRIPTION_LAST_PASSWORD_RESET,
            definitions.TIME_DESCRIPTION_LAST_CONNECTED,
            definitions.TIME_DESCRIPTION_LAST_RUN,
            definitions.TIME_DESCRIPTION_LAST_PRINTED,
        ]:
            return ".A.."

        # Content modification.
        if event.timestamp_desc in [
            definitions.TIME_DESCRIPTION_MODIFICATION,
            definitions.TIME_DESCRIPTION_WRITTEN,
            definitions.TIME_DESCRIPTION_DELETED,
        ]:
            return "M..."

        # Content creation time.
        if event.timestamp_desc in [
            definitions.TIME_DESCRIPTION_CREATION,
            definitions.TIME_DESCRIPTION_ADDED,
            definitions.TIME_DESCRIPTION_FILE_DOWNLOADED,
            definitions.TIME_DESCRIPTION_FIRST_CONNECTED,
        ]:
            return "...B"

        # Metadata modification.
        if event.timestamp_desc == (definitions.TIME_DESCRIPTION_METADATA_MODIFICATION):
            return "..C."

        return "...."

    def GetMACBRepresentationFromDescriptions(self, timestamp_descriptions):
        """Determines the MACB representation from the timestamp descriptions.

        MACB representation is a shorthand for representing one or more of
        modification, access, change, birth timestamp descriptions as the letters
        "MACB" or a "." if the corresponding timestamp is not set.

        Note that this is an output format shorthand and does not guarantee that
        the timestamps represent the same occurrence.

        Args:
          timestamp_descriptions (list[str]): timestamp descriptions, which are
              defined in definitions.TIME_DESCRIPTIONS.

        Returns:
          str: MACB representation.
        """
        macb_representation = []

        if (
            "mtime" in timestamp_descriptions
            or definitions.TIME_DESCRIPTION_MODIFICATION in timestamp_descriptions
        ):
            macb_representation.append("M")
        else:
            macb_representation.append(".")

        if (
            "atime" in timestamp_descriptions
            or definitions.TIME_DESCRIPTION_LAST_ACCESS in timestamp_descriptions
        ):
            macb_representation.append("A")
        else:
            macb_representation.append(".")

        if (
            "ctime" in timestamp_descriptions
            or definitions.TIME_DESCRIPTION_METADATA_MODIFICATION
            in (timestamp_descriptions)
        ):
            macb_representation.append("C")
        else:
            macb_representation.append(".")

        if (
            "crtime" in timestamp_descriptions
            or definitions.TIME_DESCRIPTION_CREATION in timestamp_descriptions
        ):
            macb_representation.append("B")
        else:
            macb_representation.append(".")

        return "".join(macb_representation)

    def GetMessageFormatter(self, data_type):
        """Retrieves the message formatter for a specific data type.

        Args:
          data_type (str): data type.

        Returns:
          EventFormatter: corresponding message formatter or the default message
              formatter if not available.
        """
        data_type = data_type.lower()
        return self._message_formatters.get(data_type)

    def GetRelativePathForPathSpec(self, path_spec):
        """Retrieves the relative path for a path specification.

        Args:
          path_spec (dfvfs.PathSpec): path specification.

        Returns:
          str: relateive path of the path specification.
        """
        return path_helper.PathHelper.GetRelativePathForPathSpec(path_spec)

    def GetSourceMapping(self, data_type):
        """Retrieves the source mapping for a specific data type.

        Args:
          data_type (str): data type.

        Returns:
          tuple[str, str]: short and (long) source mappings or (None, None) if not
              available.
        """
        data_type = data_type.lower()
        return self._source_mappings.get(data_type, (None, None))

    def GetUsername(self, event_data, default_username="-"):
        """Retrieves the username related to the event data.

        Args:
          event_data (EventData): event data.
          default_username (Optional[str]): default username.

        Returns:
          str: username.
        """
        username = getattr(event_data, "username", None)
        if username and username != "-":
            return username

        username = default_username

        user_identifier = getattr(event_data, "user_sid", None)
        if user_identifier and user_identifier not in self._username_by_identifier:
            user_account = self._ReadUserAccount(user_identifier)
            if user_account:
                username = user_account.username
                self._username_by_identifier[user_identifier] = username

        return username or default_username

    def GetWinevtResourcesHelper(self):
        """Retrieves a Windows EventLog resources helper.

        Returns:
          WinevtResourcesHelper: Windows EventLog resources helper.
        """
        lcid = self._lcid

        # TODO: determine LCID from system configurations
        if not lcid:
            lcid = self._DEFAULT_LCID

        return winevt_rc.WinevtResourcesHelper(
            self._storage_reader, self.data_location, lcid
        )

    def ReadMessageFormattersFromDirectory(self, path):
        """Reads message formatters from a directory.

        Args:
          path (str): path of directory that contains the message formatters
              configuration files.

        Raises:
          KeyError: if the message formatter is already set for the corresponding
              data type.
        """
        for formatters_file_path in glob.glob(os.path.join(path, "*.yaml")):
            self._ReadMessageFormattersFile(formatters_file_path)

    def ReadMessageFormattersFromFile(self, path, override_existing=False):
        """Reads message formatters from a file.

        Args:
          path (str): path of file that contains the message formatters configuration.
          override_existing (bool): True if existing message formatters should be
              overridden.

        Raises:
          KeyError: if the message formatter is already set for the corresponding
              data type.
        """
        self._ReadMessageFormattersFile(path, override_existing=override_existing)

    def SetPreferredLanguageIdentifier(self, language_tag):
        """Sets the preferred language identifier.

        Args:
          language_tag (str): language tag such as "en-US" for US English or "is-IS"
              for Icelandic.

        Raises:
          ValueError: if the language tag is not a string type or no LCID can
              be determined that corresponds with the language tag.
        """
        lcid = None
        if language_tag:
            if not isinstance(language_tag, str):
                raise ValueError(f"Language tag: {language_tag!s} is not a string.")

            lcid = languages.WindowsLanguageHelper.GetLCIDForLanguageTag(language_tag)
            if not lcid:
                raise ValueError(f"No LCID found for language tag: {language_tag:s}.")

        self._language_tag = language_tag
        self._lcid = lcid

    def SetTimeZone(self, time_zone):
        """Sets the time zone.

        Args:
          time_zone (str): time zone.

        Raises:
          ValueError: if the time zone is not supported.
        """
        if time_zone:
            try:
                time_zone = pytz.timezone(time_zone)
            except pytz.UnknownTimeZoneError:
                raise ValueError(f"Unsupported time zone: {time_zone:s}")

        self._time_zone = time_zone
