import binascii
import json

from pymacaroons2 import utils


class JsonSerializer(object):
    """Serializer used to produce JSON macaroon format v1."""

    def serialize(self, m):
        """Serialize the macaroon in JSON format indicated by the version
        field.

        @param m the macaroon to serialize.
        @return JSON macaroon.
        """
        from pymacaroons2 import macaroon

        if m.version == macaroon.MACAROON_V1:
            return self._serialize_v1(m)
        return self._serialize_v2(m)

    def _serialize_v1(self, macaroon):
        """Serialize the macaroon in JSON format v1.

        @param macaroon the macaroon to serialize.
        @return JSON macaroon.
        """
        serialized = {
            "identifier": utils.convert_to_string(macaroon.identifier),
            "signature": macaroon.signature,
        }
        if macaroon.location:
            serialized["location"] = macaroon.location
        if macaroon.caveats:
            serialized["caveats"] = [
                _caveat_v1_to_dict(caveat) for caveat in macaroon.caveats
            ]
        return json.dumps(serialized)

    def _serialize_v2(self, macaroon):
        """Serialize the macaroon in JSON format v2.

        @param macaroon the macaroon to serialize.
        @return JSON macaroon in v2 format.
        """
        serialized = {}
        _add_json_binary_field(macaroon.identifier_bytes, serialized, "i")
        _add_json_binary_field(
            binascii.unhexlify(macaroon.signature_bytes), serialized, "s"
        )

        if macaroon.location:
            serialized["l"] = macaroon.location
        if macaroon.caveats:
            serialized["c"] = [
                _caveat_v2_to_dict(caveat) for caveat in macaroon.caveats
            ]
        return json.dumps(serialized)

    def deserialize(self, serialized):
        """Deserialize a JSON macaroon depending on the format.

        @param serialized the macaroon in JSON format.
        @return the macaroon object.
        """
        deserialized = json.loads(serialized)
        if deserialized.get("identifier") is None:
            return self._deserialize_v2(deserialized)
        else:
            return self._deserialize_v1(deserialized)

    def _deserialize_v1(self, serialized):
        """Deserialize a JSON macaroon in v1 format.

        @param serialized the macaroon in v1 JSON format.
        @return the macaroon object.
        """
        from pymacaroons2.caveat import Caveat
        from pymacaroons2.macaroon import MACAROON_V1, Macaroon

        caveats = []
        for c in serialized.get("caveats", []):
            caveat = Caveat(
                caveat_id=c["cid"],
                verification_key_id=(
                    utils.raw_b64decode(c["vid"]) if c.get("vid") else None
                ),
                location=(c["cl"] if c.get("cl") else None),
                version=MACAROON_V1,
            )
            caveats.append(caveat)

        return Macaroon(
            location=serialized.get("location"),
            identifier=serialized["identifier"],
            caveats=caveats,
            signature=serialized["signature"],
            version=MACAROON_V1,
        )

    def _deserialize_v2(self, serialized):
        """Deserialize a JSON macaroon v2.

        @param serialized the macaroon in JSON format v2.
        @return the macaroon object.
        """
        from pymacaroons2.caveat import Caveat
        from pymacaroons2.macaroon import MACAROON_V2, Macaroon

        caveats = []
        for c in serialized.get("c", []):
            caveat = Caveat(
                caveat_id=_read_json_binary_field(c, "i"),
                verification_key_id=_read_json_binary_field(c, "v"),
                location=_read_json_binary_field(c, "l"),
                version=MACAROON_V2,
            )
            caveats.append(caveat)
        return Macaroon(
            location=_read_json_binary_field(serialized, "l"),
            identifier=_read_json_binary_field(serialized, "i"),
            caveats=caveats,
            signature=binascii.hexlify(_read_json_binary_field(serialized, "s")),
            version=MACAROON_V2,
        )


def _caveat_v1_to_dict(c):
    """Return a caveat as a dictionary for export as the JSON
    macaroon v1 format.
    """
    serialized = {}
    if len(c.caveat_id) > 0:
        serialized["cid"] = c.caveat_id
    if c.verification_key_id:
        serialized["vid"] = utils.raw_urlsafe_b64encode(c.verification_key_id).decode(
            "utf-8"
        )
    if c.location:
        serialized["cl"] = c.location
    return serialized


def _caveat_v2_to_dict(c):
    """Return a caveat as a dictionary for export as the JSON
    macaroon v2 format.
    """
    serialized = {}
    if len(c.caveat_id_bytes) > 0:
        _add_json_binary_field(c.caveat_id_bytes, serialized, "i")
    if c.verification_key_id:
        _add_json_binary_field(c.verification_key_id, serialized, "v")
    if c.location:
        serialized["l"] = c.location
    return serialized


def _add_json_binary_field(b, serialized, field):
    """Set the given field to the given val (a bytearray) in the serialized
    dictionary.

    If the value isn't valid utf-8, we base64 encode it and use field+"64"
    as the field name.
    """
    try:
        val = b.decode("utf-8")
        serialized[field] = val
    except UnicodeDecodeError:
        val = utils.raw_urlsafe_b64encode(b).decode("utf-8")
        serialized[field + "64"] = val


def _read_json_binary_field(deserialized, field):
    """Read the value of a JSON field that may be string or base64-encoded."""
    val = deserialized.get(field)
    if val is not None:
        return utils.convert_to_bytes(val)
    val = deserialized.get(field + "64")
    if val is None:
        return None
    return utils.raw_urlsafe_b64decode(val)
