from __future__ import unicode_literals

import binascii
from collections import namedtuple
import six
import struct
import sys
from base64 import urlsafe_b64encode

from pymacaroons.utils import (
    convert_to_bytes,
    convert_to_string,
    raw_b64decode,
)
from pymacaroons.serializers.base_serializer import BaseSerializer
from pymacaroons.exceptions import MacaroonSerializationException


PacketV2 = namedtuple('PacketV2', ['field_type', 'data'])


class BinarySerializer(BaseSerializer):

    PACKET_PREFIX_LENGTH = 4
    _LOCATION = 1
    _IDENTIFIER = 2
    _VID = 4
    _SIGNATURE = 6
    _EOS = 0

    def serialize(self, macaroon):
        return urlsafe_b64encode(
            self.serialize_raw(macaroon)).decode('ascii').rstrip('=')

    def serialize_raw(self, macaroon):
        from pymacaroons.macaroon import MACAROON_V1
        if macaroon.version == MACAROON_V1:
            return self._serialize_v1(macaroon)
        return self._serialize_v2(macaroon)

    def _serialize_v1(self, macaroon):
        combined = self._packetize(b'location', macaroon.location)
        combined += self._packetize(b'identifier', macaroon.identifier)

        for caveat in macaroon.caveats:
            combined += self._packetize(b'cid', caveat._caveat_id)

            if caveat._verification_key_id and caveat._location:
                combined += self._packetize(
                    b'vid', caveat._verification_key_id)
                combined += self._packetize(b'cl', caveat._location)

        combined += self._packetize(
            b'signature',
            binascii.unhexlify(macaroon.signature_bytes)
        )
        return combined

    def _serialize_v2(self, macaroon):
        from pymacaroons.macaroon import MACAROON_V2

        # https://github.com/rescrv/libmacaroons/blob/master/doc/format.txt
        data = bytearray()
        data.append(MACAROON_V2)
        if macaroon.location is not None:
            self._append_packet(data, self._LOCATION, convert_to_bytes(
                macaroon.location))
            self._append_packet(data, self._IDENTIFIER,
                                macaroon.identifier_bytes)
        self._append_packet(data, self._EOS)
        for c in macaroon.caveats:
            if c.location is not None:
                self._append_packet(data, self._LOCATION,
                                    convert_to_bytes(c.location))
            self._append_packet(data, self._IDENTIFIER, c.caveat_id_bytes)
            if c.verification_key_id is not None:
                self._append_packet(data, self._VID, convert_to_bytes(
                    c.verification_key_id))
            self._append_packet(data, self._EOS)
        self._append_packet(data, self._EOS)
        self._append_packet(data, self._SIGNATURE, binascii.unhexlify(
            macaroon.signature_bytes))
        return bytes(data)

    def deserialize(self, serialized):
        if len(serialized) == 0:
            raise ValueError('empty macaroon')
        serialized = convert_to_string(serialized)
        decoded = raw_b64decode(serialized)
        return self.deserialize_raw(decoded)

    def deserialize_raw(self, serialized):
        from pymacaroons.macaroon import MACAROON_V2
        from pymacaroons.exceptions import MacaroonDeserializationException

        first = six.byte2int(serialized[:1])
        if first == MACAROON_V2:
            return self._deserialize_v2(serialized)
        if _is_ascii_hex(first):
            return self._deserialize_v1(serialized)
        raise MacaroonDeserializationException(
            'cannot determine data format of binary-encoded macaroon')

    def _deserialize_v1(self, decoded):
        from pymacaroons.macaroon import Macaroon, MACAROON_V1
        from pymacaroons.caveat import Caveat
        from pymacaroons.exceptions import MacaroonDeserializationException

        macaroon = Macaroon(version=MACAROON_V1)

        index = 0

        while index < len(decoded):
            packet_length = int(
                struct.unpack(
                    b"4s",
                    decoded[index:index + self.PACKET_PREFIX_LENGTH]
                )[0],
                16
            )
            packet = decoded[
                index + self.PACKET_PREFIX_LENGTH:index + packet_length
            ]

            key, value = self._depacketize(packet)

            if key == b'location':
                macaroon.location = value
            elif key == b'identifier':
                macaroon.identifier = value
            elif key == b'cid':
                macaroon.caveats.append(Caveat(caveat_id=value,
                                               version=MACAROON_V1))
            elif key == b'vid':
                macaroon.caveats[-1].verification_key_id = value
            elif key == b'cl':
                macaroon.caveats[-1].location = value
            elif key == b'signature':
                macaroon.signature = binascii.hexlify(value)
            else:
                raise MacaroonDeserializationException(
                    'Key {key} not valid key for this format. '
                    'Value: {value}'.format(
                        key=key, value=value
                    )
                )

            index = index + packet_length

        return macaroon

    def _deserialize_v2(self, serialized):
        from pymacaroons.macaroon import Macaroon, MACAROON_V2
        from pymacaroons.caveat import Caveat
        from pymacaroons.exceptions import MacaroonDeserializationException

        # skip the initial version byte.
        serialized = serialized[1:]
        serialized, section = self._parse_section_v2(serialized)
        loc = ''
        if len(section) > 0 and section[0].field_type == self._LOCATION:
            loc = section[0].data.decode('utf-8')
            section = section[1:]
        if len(section) != 1 or section[0].field_type != self._IDENTIFIER:
            raise MacaroonDeserializationException('invalid macaroon header')
        macaroon = Macaroon(
            identifier=section[0].data,
            location=loc,
            version=MACAROON_V2,
        )
        while True:
            rest, section = self._parse_section_v2(serialized)
            serialized = rest
            if len(section) == 0:
                break
            cav = Caveat(version=MACAROON_V2)
            if len(section) > 0 and section[0].field_type == self._LOCATION:
                cav.location = section[0].data.decode('utf-8')
                section = section[1:]

            if len(section) == 0 or section[0].field_type != self._IDENTIFIER:
                raise MacaroonDeserializationException(
                    'no identifier in caveat')

            cav.caveat_id = section[0].data
            section = section[1:]
            if len(section) == 0:
                # First party caveat.
                if cav.location is not None:
                    raise MacaroonDeserializationException(
                        'location not allowed in first party caveat')
                macaroon.caveats.append(cav)
                continue

            if len(section) != 1:
                raise MacaroonDeserializationException(
                    'extra fields found in caveat')

            if section[0].field_type != self._VID:
                raise MacaroonDeserializationException(
                    'invalid field found in caveat')
            cav.verification_key_id = section[0].data
            macaroon.caveats.append(cav)
        serialized, packet = self._parse_packet_v2(serialized)
        if packet.field_type != self._SIGNATURE: