"""Parser for Safari Binary Cookie files."""

import os

from dfdatetime import cocoa_time as dfdatetime_cocoa_time

from dtfabric.runtime import data_maps as dtfabric_data_maps

from plaso.containers import events
from plaso.lib import cookie_plugins_helper
from plaso.lib import dtfabric_helper
from plaso.lib import errors
from plaso.lib import specification
from plaso.parsers import interface
from plaso.parsers import manager


class SafariBinaryCookieEventData(events.EventData):
    """Safari binary cookie event data.

    Attributes:
      cookie_name (str): cookie name.
      cookie_value (str): cookie value.
      creation_time (dfdatetime.DateTimeValues): date and time the cookie was created.
      expiration_time (dfdatetime.DateTimeValues): date and time the cookie expires.
      flags (int): cookie flags.
      path (str): path of the cookie.
      url (str): URL where this cookie is valid.
    """

    DATA_TYPE = "safari:cookie:entry"

    def __init__(self):
        """Initializes event data."""
        super().__init__(data_type=self.DATA_TYPE)
        self.cookie_name = None
        self.cookie_value = None
        self.creation_time = None
        self.expiration_time = None
        self.flags = None
        self.path = None
        self.url = None


class BinaryCookieParser(
    interface.FileObjectParser,
    cookie_plugins_helper.CookiePluginsHelper,
    dtfabric_helper.DtFabricHelper,
):
    """Parser for Safari Binary Cookie files."""

    NAME = "binary_cookies"
    DATA_FORMAT = "Safari Binary Cookie file"

    _DEFINITION_FILE = os.path.join(os.path.dirname(__file__), "safari_cookies.yaml")

    def _ParseCString(self, page_data, string_offset):
        """Parses a C string from the page data.

        Args:
          page_data (bytes): page data.
          string_offset (int): offset of the string relative to the start of the page.

        Returns:
          str: string.

        Raises:
          ParseError: when the string cannot be parsed.
        """
        cstring_map = self._GetDataTypeMap("cstring")

        try:
            value_string = self._ReadStructureFromByteStream(
                page_data[string_offset:], string_offset, cstring_map
            )
        except (ValueError, errors.ParseError) as exception:
            raise errors.ParseError(
                f"Unable to map string data at offset: 0x{string_offset:08x} with "
                f"error: {exception!s}"
            )

        return value_string.rstrip("\x00")

    def _ParsePage(self, parser_mediator, file_offset, page_data):
        """Parses a page.

        Args:
          parser_mediator (ParserMediator): mediates interactions between parsers and
              other components, such as storage and dfVFS.
          file_offset (int): offset of the data relative from the start of the
              file-like object.
          page_data (bytes): page data.

        Raises:
          ParseError: when the page cannot be parsed.
        """
        page_header_map = self._GetDataTypeMap("binarycookies_page_header")

        try:
            page_header = self._ReadStructureFromByteStream(
                page_data, file_offset, page_header_map
            )
        except (ValueError, errors.ParseError) as exception:
            raise errors.ParseError(
                f"Unable to map page header data at offset: 0x{file_offset:08x} with "
                f"error: {exception!s}"
            )

        for record_offset in page_header.offsets:
            if parser_mediator.abort:
                break

            self._ParseRecord(parser_mediator, page_data, record_offset)

    def _ParseRecord(self, parser_mediator, page_data, record_offset):
        """Parses a record from the page data.

        Args:
          parser_mediator (ParserMediator): mediates interactions between parsers and
              other components, such as storage and dfVFS.
          page_data (bytes): page data.
          record_offset (int): offset of the record relative to the start of the page.

        Raises:
          ParseError: when the record cannot be parsed.
        """
        record_header_map = self._GetDataTypeMap("binarycookies_record_header")

        try:
            record_header = self._ReadStructureFromByteStream(
                page_data[record_offset:], record_offset, record_header_map
            )
        except (ValueError, errors.ParseError) as exception:
            raise errors.ParseError(
                f"Unable to map record header data at offset: 0x{record_offset:08x} "
                f"with error: {exception!s}"
            )

        event_data = SafariBinaryCookieEventData()
        event_data.flags = record_header.flags

        if record_header.url_offset:
            data_offset = record_offset + record_header.url_offset
            event_data.url = self._ParseCString(page_data, data_offset)

        if record_header.name_offset:
            data_offset = record_offset + record_header.name_offset
            event_data.cookie_name = self._ParseCString(page_data, data_offset)

        if record_header.path_offset:
            data_offset = record_offset + record_header.path_offset
            event_data.path = self._ParseCString(page_data, data_offset)

        if record_header.value_offset:
            data_offset = record_offset + record_header.value_offset
            event_data.cookie_value = self._ParseCString(page_data, data_offset)

        if record_header.creation_time:
            event_data.creation_time = dfdatetime_cocoa_time.CocoaTime(
                timestamp=record_header.creation_time
            )
        if record_header.expiration_time:
            event_data.expiration_time = dfdatetime_cocoa_time.CocoaTime(
                timestamp=record_header.expiration_time
            )

        parser_mediator.ProduceEventData(event_data)

        self._ParseCookie(
            parser_mediator,
            event_data.cookie_name,
            event_data.cookie_value,
            event_data.cookie_name,
        )

    @classmethod
    def GetFormatSpecification(cls):
        """Retrieves the format specification for parser selection.

        Returns:
          FormatSpecification: format specification.
        """
        format_specification = specification.FormatSpecification(cls.NAME)
        format_specification.AddNewSignature(b"cook\x00", offset=0)
        return format_specification

    def ParseFileObject(self, parser_mediator, file_object):
        """Parses a Safari binary cookie file-like object.

        Args:
          parser_mediator (ParserMediator): mediates interactions between parsers and
              other components, such as storage and dfVFS.
          file_object (dfvfs.FileIO): file-like object to be parsed.

        Raises:
          ParseError: when the page sizes array cannot be parsed.
          WrongParser: when the file cannot be parsed, this will signal
              the event extractor to apply other parsers.
        """
        file_header_map = self._GetDataTypeMap("binarycookies_file_header")

        try:
            file_header, file_header_data_size = self._ReadStructureFromFileObject(
                file_object, 0, file_header_map
            )
        except (ValueError, errors.ParseError) as exception:
            raise errors.WrongParser(
                f"Unable to read file header with error: {exception!s}."
            )

        file_offset = file_header_data_size

        # TODO: move page sizes array into file header, this will require dtFabric
        # to compare signature as part of data map.
        page_sizes_data_size = file_header.number_of_pages * 4

        page_sizes_data = file_object.read(page_sizes_data_size)

        context = dtfabric_data_maps.DataTypeMapContext(
            values={"binarycookies_file_header": file_header}
        )
        page_sizes_map = self._GetDataTypeMap("binarycookies_page_sizes")

        try:
            page_sizes_array = self._ReadStructureFromByteStream(
                page_sizes_data, file_offset, page_sizes_map, context=context
            )
        except (ValueError, errors.ParseError) as exception:
            raise errors.ParseError(
                f"Unable to map page sizes data at offset: 0x{file_offset:08x} "
                f"with error: {exception!s}"
            )

        file_offset += page_sizes_data_size

        for page_number, page_size in enumerate(page_sizes_array):
            if parser_mediator.abort:
                break

            page_data = file_object.read(page_size)
            if len(page_data) != page_size:
                parser_mediator.ProduceWarning(f"unable to read page: {page_number:d}")
                break

            self._ParsePage(parser_mediator, file_offset, page_data)

            file_offset += page_size

        # TODO: check file footer.


manager.ParsersManager.RegisterParser(BinaryCookieParser)
