# standard imports
import json
import logging
import os
from typing import Dict, Union

# third-part imports
import requests
from cic_types.models.person import generate_metadata_pointer, Person

# local imports
from cic_ussd.metadata import make_request
from cic_ussd.metadata.signer import Signer
from cic_ussd.redis import cache_data
from cic_ussd.error import MetadataStoreError


logg = logging.getLogger().getChild(__name__)


class Metadata:
    """
    :cvar base_url: The base url or the metadata server.
    :type base_url: str
    """

    base_url = None


def metadata_http_error_handler(result: requests.Response):
    """ This function handles and appropriately raises errors from http requests interacting with the metadata server.
    :param result: The response object from a http request.
    :type result: requests.Response
    """
    status_code = result.status_code

    if 100 <= status_code < 200:
        raise MetadataStoreError(f'Informational errors: {status_code}, reason: {result.reason}')

    elif 300 <= status_code < 400:
        raise MetadataStoreError(f'Redirect Issues: {status_code}, reason: {result.reason}')

    elif 400 <= status_code < 500:
        raise MetadataStoreError(f'Client Error: {status_code}, reason: {result.reason}')

    elif 500 <= status_code < 600:
        raise MetadataStoreError(f'Server Error: {status_code}, reason: {result.reason}')


class MetadataRequestsHandler(Metadata):

    def __init__(self, cic_type: str, identifier: bytes, engine: str = 'pgp'):
        """
        :param cic_type: The salt value with which to hash a specific metadata identifier.
        :type cic_type: str
        :param engine: Encryption used for sending data to the metadata server.
        :type engine: str
        :param identifier: A unique element of data in bytes necessary for creating a metadata pointer.
        :type identifier: bytes
        """
        self.cic_type = cic_type
        self.engine = engine
        self.headers = {
            'X-CIC-AUTOMERGE': 'server',
            'Content-Type': 'application/json'
        }
        self.identifier = identifier
        self.metadata_pointer = generate_metadata_pointer(
            identifier=self.identifier,
            cic_type=self.cic_type
        )
        if self.base_url:
            self.url = os.path.join(self.base_url, self.metadata_pointer)

    def create(self, data: Union[Dict, str]):
        """ This function is responsible for posting data to the metadata server with a corresponding metadata pointer
        for storage.
        :param data: The data to be stored in the metadata server.
        :type data: dict|str
        """
        data = json.dumps(data).encode('utf-8')
        result = make_request(method='POST', url=self.url, data=data, headers=self.headers)
        metadata_http_error_handler(result=result)
        metadata = result.content
        self.edit(data=metadata)

    def edit(self, data: bytes):
        """ This function is responsible for editing data in the metadata server corresponding to a unique pointer.
        :param data: The data to be edited in the metadata server.
        :type data: bytes
        """
        cic_meta_signer = Signer()
        signature = cic_meta_signer.sign_digest(data=data)
        algorithm = cic_meta_signer.get_operational_key().get('algo')
        decoded_data = data.decode('utf-8')
        formatted_data = {
            'm': data.decode('utf-8'),
            's': {
                'engine': self.engine,
                'algo': algorithm,
                'data': signature,
                'digest': json.loads(data).get('digest'),
            }
        }
        formatted_data = json.dumps(formatted_data).encode('utf-8')
        result = make_request(method='PUT', url=self.url, data=formatted_data, headers=self.headers)
        logg.info(f'signed metadata submission status: {result.status_code}.')
        metadata_http_error_handler(result=result)
        try:
            decoded_identifier = self.identifier.decode("utf-8")
        except UnicodeDecodeError:
            decoded_identifier = self.identifier.hex()
        logg.info(f'identifier: {decoded_identifier}. metadata pointer: {self.metadata_pointer} set to: {decoded_data}.')

    def query(self):
        """This function is responsible for querying the metadata server for data corresponding to a unique pointer."""
        result = make_request(method='GET', url=self.url)
        metadata_http_error_handler(result=result)
        response_data = result.content
        data = json.loads(response_data.decode('utf-8'))
        if result.status_code == 200 and self.cic_type == ':cic.person':
            person = Person()
            deserialized_person = person.deserialize(person_data=data)
            data = json.dumps(deserialized_person.serialize())
            cache_data(self.metadata_pointer, data=data)
            logg.debug(f'caching: {data} with key: {self.metadata_pointer}')