"""MacOS preprocessor plugins."""

import abc
import plistlib

from plaso.containers import artifacts
from plaso.lib import errors
from plaso.lib import plist
from plaso.preprocessors import interface
from plaso.preprocessors import manager


class PlistFileArtifactPreprocessorPlugin(
    interface.FileArtifactPreprocessorPlugin):
  """Plist file artifact preprocessor plugin interface.

  Retrieves values from a plist file artifact using names of keys defined in
  _PLIST_KEYS.
  """

  # The key that's value should be returned back. It is an ordered list
  # of preference. If the first value is found it will be returned and no
  # others will be searched.
  _PLIST_KEYS = ['']

  def _FindKeys(self, key, names, matches):
    """Searches the plist key hierarchy for keys with matching names.

    If a match is found a tuple of the key name and value is added to
    the matches list.

    Args:
      key (dict[str, object]): plist key.
      names (list[str]): names of the keys to match.
      matches (list[str]): keys with matching names.
    """
    for name, subkey in key.items():
      if name in names:
        matches.append((name, subkey))

      if isinstance(subkey, dict):
        self._FindKeys(subkey, names, matches)

  def _ParseFileData(self, mediator, file_object):
    """Parses file content (data) for a preprocessing attribute.

    Args:
      mediator (PreprocessMediator): mediates interactions between preprocess
          plugins and other components, such as storage.
      file_object (dfvfs.FileIO): file-like object that contains the artifact
          value data.

    Raises:
      errors.PreProcessFail: if the preprocessing fails.
    """
    plist_file = plist.PlistFile()

    try:
      plist_file.Read(file_object)

    except OSError as exception:
      raise errors.PreProcessFail((
          f'Unable to read: {self.ARTIFACT_DEFINITION_NAME:s} with error: '
          f'{exception!s}'))

    if not plist_file.root_key:
      raise errors.PreProcessFail((
          f'Unable to read: {self.ARTIFACT_DEFINITION_NAME:s} with error: '
          f'missing root key'))

    matches = []

    self._FindKeys(plist_file.root_key, self._PLIST_KEYS, matches)
    if not matches:
      plist_keys_string = ', '.join(self._PLIST_KEYS)
      raise errors.PreProcessFail((
          f'Unable to read: {self.ARTIFACT_DEFINITION_NAME:s} with error: no '
          f'such keys: {plist_keys_string:s}.'))

    name = None
    value = None
    for name, value in matches:
      if value:
        break

    if value is None:
      plist_keys_string = ', '.join(self._PLIST_KEYS)
      raise errors.PreProcessFail((
          f'Unable to read: {self.ARTIFACT_DEFINITION_NAME:s} with error: no '
          f'values found for keys: {plist_keys_string:s}.'))

    self._ParsePlistKeyValue(mediator, name, value)

  @abc.abstractmethod
  def _ParsePlistKeyValue(self, mediator, name, value):
    """Parses a plist key value.

    Args:
      mediator (PreprocessMediator): mediates interactions between preprocess
          plugins and other components, such as storage.
      name (str): name of the plist key.
      value (str): value of the plist key.
    """


class MacOSHostnamePlugin(PlistFileArtifactPreprocessorPlugin):
  """MacOS hostname plugin."""

  ARTIFACT_DEFINITION_NAME = 'MacOSSystemConfigurationPreferencesPlistFile'

  _PLIST_KEYS = ['ComputerName', 'LocalHostName']

  def _ParsePlistKeyValue(self, mediator, name, value):
    """Parses a plist key value.

    Args:
      mediator (PreprocessMediator): mediates interactions between preprocess
          plugins and other components, such as storage.
      name (str): name of the plist key.
      value (str): value of the plist key.
    """
    if name in self._PLIST_KEYS:
      hostname_artifact = artifacts.HostnameArtifact(name=value)
      mediator.AddHostname(hostname_artifact)


class MacOSKeyboardLayoutPlugin(PlistFileArtifactPreprocessorPlugin):
  """MacOS keyboard layout plugin."""

  ARTIFACT_DEFINITION_NAME = 'MacOSKeyboardLayoutPlistFile'

  _PLIST_KEYS = ['AppleCurrentKeyboardLayoutInputSourceID']

  def _ParsePlistKeyValue(self, mediator, name, value):
    """Parses a plist key value.

    Args:
      mediator (PreprocessMediator): mediates interactions between preprocess
          plugins and other components, such as storage.
      name (str): name of the plist key.
      value (str): value of the plist key.
    """
    if name in self._PLIST_KEYS:
      if isinstance(value, (list, tuple)):
        value = value[0]

      _, _, keyboard_layout = value.rpartition('.')

      mediator.SetValue('keyboard_layout', keyboard_layout)


class MacOSSystemVersionPlugin(PlistFileArtifactPreprocessorPlugin):
  """MacOS system version information plugin."""

  ARTIFACT_DEFINITION_NAME = 'MacOSSystemVersionPlistFile'

  _PLIST_KEYS = ['ProductUserVisibleVersion']

  def _ParsePlistKeyValue(self, mediator, name, value):
    """Parses a plist key value.

    Args:
      mediator (PreprocessMediator): mediates interactions between preprocess
          plugins and other components, such as storage.
      name (str): name of the plist key.
      value (str): value of the plist key.
    """
    if name in self._PLIST_KEYS:
      mediator.SetValue('operating_system_version', value)


class MacOSTimeZonePlugin(interface.FileEntryArtifactPreprocessorPlugin):
  """MacOS time zone plugin."""

  ARTIFACT_DEFINITION_NAME = 'MacOSLocalTime'

  def _ParseFileEntry(self, mediator, file_entry):
    """Parses artifact file system data for a preprocessing attribute.

    Args:
      mediator (PreprocessMediator): mediates interactions between preprocess
          plugins and other components, such as storage.
      file_entry (dfvfs.FileEntry): file entry that contains the artifact
          value data.

    Raises:
      errors.PreProcessFail: if the preprocessing fails.
    """
    if not file_entry or not file_entry.link:
      raise errors.PreProcessFail((
          f'Unable to read: {self.ARTIFACT_DEFINITION_NAME:s} with error: not '
          f'a symbolic link'))

    _, _, time_zone = file_entry.link.partition('zoneinfo/')
    if time_zone:
      try:
        mediator.SetTimeZone(time_zone)
      except ValueError:
        mediator.ProducePreprocessingWarning(
            self.ARTIFACT_DEFINITION_NAME,
            'Unable to set time zone in knowledge base.')


class MacOSUserAccountsPlugin(interface.FileEntryArtifactPreprocessorPlugin):
  """MacOS user accounts plugin."""

  ARTIFACT_DEFINITION_NAME = 'MacOSUserPasswordHashesPlistFiles'

  _KEYS = frozenset(['gid', 'home', 'name', 'realname', 'shell', 'uid'])

  def _GetTopLevelKeys(self, top_level, keys):
    """Retrieves top-level plist keys.

    Args:
      top_level (plistlib._InternalDict): top level plist object.
      keys (set[str]): names of the top-level keys that should be retrieved.

    Returns:
      dict[str, str]: values of the requested keys or an empty dictionary if
          no corresponding top-level keys were found.
    """
    match = {}
    for key in set(keys):
      value = top_level.get(key)
      if value is not None:
        match[key] = value

    return match

  def _ParseFileEntry(self, mediator, file_entry):
    """Parses artifact file system data for a preprocessing attribute.

    Args:
      mediator (PreprocessMediator): mediates interactions between preprocess
          plugins and other components, such as storage.
      file_entry (dfvfs.FileEntry): file entry that contains the artifact
          value data.

    Raises:
      errors.PreProcessFail: if the preprocessing fails.
    """
    file_object = file_entry.GetFileObject()

    try:
      plist_file = plist.PlistFile()
      plist_file.Read(file_object)
      match = self._GetTopLevelKeys(plist_file.root_key, self._KEYS)

    except (OSError, plistlib.InvalidFileException) as exception:
      mediator.ProducePreprocessingWarning(
          self.ARTIFACT_DEFINITION_NAME,
          f'Unable to read plist with error: {exception!s}')
      return

    name = match.get('name', [None])[0]
    uid = match.get('uid', [None])[0]

    if not name or not uid:
      mediator.ProducePreprocessingWarning(
          self.ARTIFACT_DEFINITION_NAME, 'Missing name or user identifier')
      return

    user_account = artifacts.UserAccountArtifact(
        identifier=uid, username=name)
    user_account.group_identifier = match.get('gid', [None])[0]
    user_account.full_name = match.get('realname', [None])[0]
    user_account.shell = match.get('shell', [None])[0]
    user_account.user_directory = match.get('home', [None])[0]

    try:
      mediator.AddUserAccount(user_account)
    except KeyError:
      mediator.ProducePreprocessingWarning(
          self.ARTIFACT_DEFINITION_NAME,
          f'Unable to add user account: {name:s} to knowledge base.')


manager.PreprocessPluginsManager.RegisterPlugins([
    MacOSHostnamePlugin, MacOSKeyboardLayoutPlugin, MacOSSystemVersionPlugin,
    MacOSTimeZonePlugin, MacOSUserAccountsPlugin])
