import json
from json.decoder import JSONDecodeError

from pydantic import ValidationError

from .base64url_to_bytes import base64url_to_bytes
from .exceptions import InvalidClientDataJSONStructure
from .structs import CollectedClientData, TokenBinding


def parse_client_data_json(val: bytes) -> CollectedClientData:
    """
    Break apart `response.clientDataJSON` buffer into structured data
    """
    try:
        json_dict = json.loads(val)
    except JSONDecodeError:
        raise InvalidClientDataJSONStructure(
            "Unable to decode client_data_json bytes as JSON"
        )

    # Ensure required values are present in client data
    if "type" not in json_dict:
        raise InvalidClientDataJSONStructure(
            'client_data_json missing required property "type"'
        )
    if "challenge" not in json_dict:
        raise InvalidClientDataJSONStructure(
            'client_data_json missing required property "challenge"'
        )
    if "origin" not in json_dict:
        raise InvalidClientDataJSONStructure(
            'client_data_json missing required property "origin"'
        )

    client_data = CollectedClientData(
        type=json_dict["type"],
        challenge=base64url_to_bytes(json_dict["challenge"]),
        origin=json_dict["origin"],
    )

    # Populate optional values if set
    if "crossOrigin" in json_dict:
        cross_origin = bool(json_dict["crossOrigin"])
        client_data.cross_origin = cross_origin

    if "tokenBinding" in json_dict:
        token_binding_dict = json_dict["tokenBinding"]

        # Some U2F devices set a string to `token_binding`, in which case ignore it
        if type(token_binding_dict) is dict:
            if "status" not in token_binding_dict:
                raise InvalidClientDataJSONStructure(
                    'token_binding missing required property "status"'
                )

            status = token_binding_dict["status"]
            try:
                # This will raise ValidationError on an unexpected status
                token_binding = TokenBinding(status=status)

                # Handle optional values
                if "id" in token_binding_dict:
                    id = token_binding_dict["id"]
                    token_binding.id = f"{id}"

                client_data.token_binding = token_binding
            except ValidationError:
                # If we encounter a status we don't expect then ignore token_binding
                # completely
                pass

    return client_data
