"""Windows Registry plugin to parse the AMCache.hve Root key."""

import re

from dfdatetime import filetime as dfdatetime_filetime
from dfdatetime import posix_time as dfdatetime_posix_time
from dfdatetime import time_elements as dfdatetime_time_elements

from dfwinreg import errors as dfwinreg_errors

from plaso.containers import events
from plaso.parsers import winreg_parser
from plaso.parsers.winreg_plugins import interface


class AMCacheFileEventData(events.EventData):
    """AMCache file event data.

    Attributes:
      application_key_last_written_time (dfdatetime.DateTimeValues): last written date
          and time of the application key.
      company_name (str): company name that created product file belongs to.
      file_creation_time (dfdatetime.DateTimeValues): file entry creation date and time.
      file_description (str): description of file.
      file_identifier (str): identifier of file (SHA-1 of the first 30 MiB (31457280
          bytes) of file, preceded by "0000").
      file_modification_time (dfdatetime.DateTimeValues): file entry last modification
          date and time.
      file_reference (str): file system file reference, for example 9-1 (MFT entry -
          sequence number).
      file_size (int): size of file in bytes.
      file_version (str): version of file.
      filename (str): name of the file.
      full_path (str): full path of file.
      installation_time (dfdatetime.DateTimeValues): installation date and time.
      language_code (int): language code of file.
      last_written_time (dfdatetime.DateTimeValues): entry last written date and time.
      link_time (dfdatetime.DateTimeValues): link date and time.
      msi_installation_time (dfdatetime.DateTimeValues): MSI installation date and time.
      product_name (str): product name file belongs to.
      program_identifier (str): GUID of entry under Root/Program key file belongs to.
      sha1 (str): SHA-1.
    """

    DATA_TYPE = "windows:registry:amcache"

    def __init__(self):
        """Initializes event data."""
        super().__init__(data_type=self.DATA_TYPE)
        self.application_key_last_written_time = None
        self.company_name = None
        self.file_creation_time = None
        self.file_description = None
        self.file_identifier = None
        self.file_modification_time = None
        self.file_reference = None
        self.file_size = None
        self.file_version = None
        self.filename = None
        self.full_path = None
        self.installation_time = None
        self.language_code = None
        self.last_written_time = None
        self.link_time = None
        self.msi_installation_time = None
        self.product_name = None
        self.program_identifier = None
        self.sha1 = None


class AMCacheProgramEventData(events.EventData):
    """AMCache programs event data.

    Attributes:
      entry_type (str): type of entry (usually AddRemoveProgram).
      file_paths (str): file paths of installed program.
      files (str): list of files belonging to program.
      installation_time (dfdatetime.DateTimeValues): installation date and time.
      language_code (int): language_code of program.
      msi_package_code (str): MSI package code of program.
      msi_product_code (str): MSI product code of program.
      name (str): name of installed program.
      package_code (str): package code of program.
      product_code (str): product code of program.
      publisher (str): publisher of program.
      uninstall_key (str): unicode string of uninstall registry key for program.
      version (str): version of program.
    """

    DATA_TYPE = "windows:registry:amcache:programs"

    def __init__(self):
        """Initializes event data."""
        super().__init__(data_type=self.DATA_TYPE)
        self.entry_type = None
        self.file_paths = None
        self.files = None
        self.installation_time = None
        self.language_code = None
        self.msi_package_code = None
        self.msi_product_code = None
        self.name = None
        self.package_code = None
        self.product_code = None
        self.publisher = None
        self.uninstall_key = None
        self.version = None


class AMCachePlugin(interface.WindowsRegistryPlugin):
    """AMCache.hve Windows Registry plugin."""

    NAME = "amcache"
    DATA_FORMAT = "AMCache (AMCache.hve)"

    FILTERS = frozenset([interface.WindowsRegistryKeyPathFilter("\\Root")])

    # Contains: {value name: attribute name}
    _APPLICATION_SUB_KEY_VALUES = {
        "FileId": "file_identifier",
        "LowerCaseLongPath": "full_path",
        "Name": "filename",
        "ProductName": "product_name",
        "ProductVersion": "file_version",
        "ProgramId": "program_identifier",
        "Publisher": "company_name",
        "Size": "file_size",
    }

    _FILE_REFERENCE_KEY_VALUES = {
        "0": "product_name",
        "1": "company_name",
        "3": "language_code",
        "5": "file_version",
        "6": "file_size",
        "c": "file_description",
        "15": "full_path",
        "100": "program_identifier",
        "101": "sha1",
    }

    _AMCACHE_LINK_TIME = "f"
    _AMCACHE_FILE_MODIFICATION_TIME = "11"
    _AMCACHE_FILE_CREATION_TIME = "12"
    _AMCACHE_ENTRY_WRITE_TIME = "17"

    _AMCACHE_P_INSTALLATION_TIME = "a"

    _AMCACHE_P_FILES = "Files"

    # Date and time string formatted as: "MM/DD/YYYY hh:mm:ss"
    # for example "04/07/2014 15:18:49"
    # TODO: determine if this is true for other locales.
    _LINK_DATE_TIME_RE = re.compile(
        r"([0-9][0-9])/([0-9][0-9])/([0-9][0-9][0-9][0-9]) "
        r"([0-9][0-9]):([0-9][0-9]):([0-9][0-9])"
    )

    _PRODUCT_KEY_VALUES = {
        "0": "name",
        "1": "version",
        "2": "publisher",
        "3": "language_code",
        "6": "entry_type",
        "7": "uninstall_key",
        "d": "file_paths",
        "f": "product_code",
        "10": "package_code",
        "11": "msi_product_code",
        "12": "msi_package_code",
    }

    def _GetValueDataAsObject(
        self, parser_mediator, key_path, value_name, registry_value
    ):
        """Retrieves the value data as an object from a Windows Registry value.

        Args:
          parser_mediator (ParserMediator): mediates interactions between parsers and
              other components, such as storage and dfVFS.
          key_path (str): key path.
          value_name (str): name of the value.
          registry_value (dfwinreg.WinRegistryValue): Windows Registry value.

        Returns:
          object: value data or None when the value data cannot be determined.
        """
        if registry_value.data is None:
            return "(empty)"

        try:
            value_object = registry_value.GetDataAsObject()

            if registry_value.DataIsMultiString():
                value_object = list(value_object)

            elif (
                not registry_value.DataIsInteger() and not registry_value.DataIsString()
            ):
                # Represent remaining types like REG_BINARY and
                # REG_RESOURCE_REQUIREMENT_LIST.
                value_object = registry_value.data

        except dfwinreg_errors.WinRegistryValueError as exception:
            parser_mediator.ProduceWarning(
                f"Unable to retrieve value data of type: "
                f"{registry_value.data_type_string,:s} as object from value: "
                f"{value_name:s} in key: {key_path:s} with error: {exception!s}"
            )
            value_object = None

        return value_object

    def _ParseApplicationSubKey(self, parser_mediator, application_sub_key):
        """Parses a Root\\InventoryApplicationFile\\%NAME%|%IDENTIFIER% key.

        Args:
          parser_mediator (ParserMediator): mediates interactions between parsers and
              other components, such as storage and dfVFS.
          application_sub_key (dfwinreg.WinRegistryKey): application sub key of the
              InventoryApplicationFile Windows Registry key.
        """
        corrupted = False
        event_data = AMCacheFileEventData()

        for value_name, attribute_name in self._APPLICATION_SUB_KEY_VALUES.items():
            value = application_sub_key.GetValueByName(value_name)
            if value:
                value_data = self._GetValueDataAsObject(
                    parser_mediator, application_sub_key.path, value_name, value
                )
                setattr(event_data, attribute_name, value_data)

        install_date_value = application_sub_key.GetValueByName("InstallDate")
        if install_date_value:
            event_data.installation_time, value_corrupted = self._ParseDateStringValue(
                parser_mediator, application_sub_key.path, install_date_value
            )
            corrupted = corrupted or value_corrupted

        install_date_msi_value = application_sub_key.GetValueByName("InstallDateMsi")
        if install_date_msi_value:
            event_data.msi_installation_time, value_corrupted = (
                self._ParseDateStringValue(
                    parser_mediator, application_sub_key.path, install_date_msi_value
                )
            )
            corrupted = corrupted or value_corrupted

        link_date_value = application_sub_key.GetValueByName("LinkDate")
        if link_date_value:
            event_data.link_time, value_corrupted = self._ParseDateStringValue(
                parser_mediator, application_sub_key.path, link_date_value
            )
            corrupted = corrupted or value_corrupted

        event_data.application_key_last_written_time = (
            application_sub_key.last_written_time
        )
        parser_mediator.ProduceEventData(event_data, corrupted=corrupted)

    def _ParseDateStringValue(self, parser_mediator, key_path, registry_value):
        """Parses a date and time string value.

        Args:
          parser_mediator (ParserMediator): mediates interactions between parsers and
              other components, such as storage and dfVFS.
          key_path (str): key path.
          registry_value (dfwinreg.WinRegistryValue): Windows Registry value.

        Returns:
          tuple: containing:

              dfdatetime_time_elements.TimeElements: date and time value or None
                  if not available.
              bool: value to indicate the date and time string value was corrupted.
        """
        if not registry_value.DataIsString():
            parser_mediator.ProduceWarning(
                f"unsupported {registry_value.name:s} with value data type: "
                f"{registry_value.data_type_string:s} in key: {key_path:s}"
            )
            return None, False

        date_time_string = registry_value.GetDataAsObject()
        if not date_time_string:
            parser_mediator.ProduceWarning(
                f"missing {registry_value.name:s} value data in key: {key_path:s}"
            )
            return None, False

        re_match = self._LINK_DATE_TIME_RE.match(date_time_string)
        if not re_match:
            parser_mediator.ProduceWarning(
                f"unsupported {registry_value.name:s} value data: "
                f"{date_time_string!s} in key: {key_path:s}"
            )
            return None, False

        month, day_of_month, year, hours, minutes, seconds = re_match.groups()

        try:
            year = int(year, 10)
            month = int(month, 10)
            day_of_month = int(day_of_month, 10)
            hours = int(hours, 10)
            minutes = int(minutes, 10)
            seconds = int(seconds, 10)
        except (TypeError, ValueError):
            parser_mediator.ProduceWarning(
                f"invalid {registry_value.name:s} date time value: "
                f"{date_time_string!s} in key: {key_path:s}"
            )
            return None, True

        time_elements_tuple = (year, month, day_of_month, hours, minutes, seconds)

        try:
            date_time = dfdatetime_time_elements.TimeElements(
                time_elements_tuple=time_elements_tuple
            )
        except ValueError:
            parser_mediator.ProduceWarning(
                f"invalid {registry_value.name:s} date time value: "
                f"{time_elements_tuple!s} in key: {key_path:s}"
            )
            return None, True

        return date_time, False

    def _ParseFileKey(self, parser_mediator, file_key):
        """Parses a Root\\File key.

        Args:
          parser_mediator (ParserMediator): mediates interactions between parsers and
              other components, such as storage and dfVFS.
          file_key (dfwinreg.WinRegistryKey): the File Windows Registry key.
        """
        for volume_key in file_key.GetSubkeys():
            for file_reference_key in volume_key.GetSubkeys():
                self._ParseFileReferenceKey(parser_mediator, file_reference_key)

    def _ParseFileReferenceKey(self, parser_mediator, file_reference_key):
        """Parses a file reference key (sub key of Root\\File\\%VOLUME%) for events.

        Args:
          parser_mediator (ParserMediator): mediates interactions between parsers and
              other components, such as storage and dfVFS.
          file_reference_key (dfwinreg.WinRegistryKey): file reference Windows
              Registry key.
        """
        event_data = AMCacheFileEventData()

        try:
            if "0000" in file_reference_key.name:
                # A NTFS file reference is a combination of MFT entry and sequence
                # number.
                sequence_number, mft_entry = file_reference_key.name.split("0000")
                mft_entry = int(mft_entry, 16)
                sequence_number = int(sequence_number, 16)
                event_data.file_reference = f"{mft_entry:d}-{sequence_number:d}"
            else:
                # A FAT file reference is the offset of the corresponding directory
                # entry.
                file_reference = int(file_reference_key.name, 16)
                event_data.file_reference = f"{file_reference:d}"

        except (ValueError, TypeError):
            pass

        for value_name, attribute_name in self._FILE_REFERENCE_KEY_VALUES.items():
            value = file_reference_key.GetValueByName(value_name)
            if not value:
                continue

            value_data = self._GetValueDataAsObject(
                parser_mediator, file_reference_key.path, value_name, value
            )
            if attribute_name == "sha1" and value_data.startswith("0000"):
                # Strip off the 4 leading zero's from the sha1 hash.
                value_data = value_data[4:]

            setattr(event_data, attribute_name, value_data)

        write_time_value = file_reference_key.GetValueByName(
            self._AMCACHE_ENTRY_WRITE_TIME
        )
        if write_time_value:
            timestamp = write_time_value.GetDataAsObject()
            event_data.last_written_time = dfdatetime_filetime.Filetime(
                timestamp=timestamp
            )

        creation_time_value = file_reference_key.GetValueByName(
            self._AMCACHE_FILE_CREATION_TIME
        )
        if creation_time_value:
            timestamp = creation_time_value.GetDataAsObject()
            event_data.file_creation_time = dfdatetime_filetime.Filetime(
                timestamp=timestamp
            )

        modification_time_value = file_reference_key.GetValueByName(
            self._AMCACHE_FILE_MODIFICATION_TIME
        )
        if modification_time_value:
            timestamp = modification_time_value.GetDataAsObject()
            event_data.file_modification_time = dfdatetime_filetime.Filetime(
                timestamp=timestamp
            )

        link_time_value = file_reference_key.GetValueByName(self._AMCACHE_LINK_TIME)
        if link_time_value:
            timestamp = link_time_value.GetDataAsObject()
            event_data.link_time = dfdatetime_posix_time.PosixTime(timestamp=timestamp)

        parser_mediator.ProduceEventData(event_data)

    def _ParseInventoryApplicationFileKey(
        self, parser_mediator, inventory_application_file_key
    ):
        """Parses a Root\\InventoryApplicationFile key.

        Args:
          parser_mediator (ParserMediator): mediates interactions between parsers and
              other components, such as storage and dfVFS.
          inventory_application_file_key (dfwinreg.WinRegistryKey): the
              InventoryApplicationFile Windows Registry key.
        """
        for application_sub_key in inventory_application_file_key.GetSubkeys():
            self._ParseApplicationSubKey(parser_mediator, application_sub_key)

    def _ParseProgramKey(self, parser_mediator, program_key):
        """Parses a program key (a sub key of Root\\Programs) for events.

        Args:
          parser_mediator (ParserMediator): mediates interactions between parsers and
              other components, such as storage and dfVFS.
          program_key (dfwinreg.WinRegistryKey): program Windows Registry key.
        """
        event_data = AMCacheProgramEventData()

        for value_name, attribute_name in self._PRODUCT_KEY_VALUES.items():
            value = program_key.GetValueByName(value_name)
            if value:
                value_data = self._GetValueDataAsObject(
                    parser_mediator, program_key.path, value_name, value
                )
                setattr(event_data, attribute_name, value_data)

        installation_time_value = program_key.GetValueByName(
            self._AMCACHE_P_INSTALLATION_TIME
        )
        if installation_time_value:
            timestamp = installation_time_value.GetDataAsObject()
            event_data.installation_time = dfdatetime_posix_time.PosixTime(
                timestamp=timestamp
            )

        parser_mediator.ProduceEventData(event_data)

    def _ParseProgramsKey(self, parser_mediator, programs_key):
        """Parses a Root\\Programs key.

        Args:
          parser_mediator (ParserMediator): mediates interactions between parsers and
              other components, such as storage and dfVFS.
          programs_key (dfwinreg.WinRegistryKey): the Programs Windows Registry key.
        """
        for program_key in programs_key.GetSubkeys():
            self._ParseProgramKey(parser_mediator, program_key)

    def _ParseRootKey(self, parser_mediator, root_key):
        """Parses a Root key.

        Args:
          parser_mediator (ParserMediator): mediates interactions between parsers and
              other components, such as storage and dfVFS.
          root_key (dfwinreg.WinRegistryKey): the Root Windows Registry key.
        """
        self._ProduceDefaultWindowsRegistryEvent(parser_mediator, root_key)

        for sub_key in root_key.GetSubkeys():
            self._ParseSubKey(parser_mediator, sub_key)

            if sub_key.name == "File":
                self._ParseFileKey(parser_mediator, sub_key)

            elif sub_key.name == "InventoryApplicationFile":
                self._ParseInventoryApplicationFileKey(parser_mediator, sub_key)

            elif sub_key.name == "Programs":
                self._ParseProgramsKey(parser_mediator, sub_key)

    def _ParseSubKey(self, parser_mediator, registry_key):
        """Parses a sub key.

        Args:
          parser_mediator (ParserMediator): mediates interactions between parsers and
              other components, such as storage and dfVFS.
          registry_key (dfwinreg.WinRegistryKey): the Windows Registry key.
        """
        self._ProduceDefaultWindowsRegistryEvent(parser_mediator, registry_key)

        for sub_key in registry_key.GetSubkeys():
            self._ParseSubKey(parser_mediator, sub_key)

    def ExtractEvents(self, parser_mediator, registry_key, **kwargs):
        """Extracts events from a Windows Registry key.

        Args:
          parser_mediator (ParserMediator): mediates interactions between parsers and
              other components, such as storage and dfVFS.
          registry_key (dfwinreg.WinRegistryKey): Windows Registry key.
        """
        self._ParseRootKey(parser_mediator, registry_key)


winreg_parser.WinRegistryParser.RegisterPlugin(AMCachePlugin)
