# Copyright 2017 VMware, Inc.  All rights reserved. -- VMware Confidential
#
'''The file contains json utilities allowing custom object to be serialized and
deserialized as json ojbects. Usually the json files do not carry over meta-data
for wrapper objects. This module enhances the default json behavior as allowing
us to serialize meta-data for wrapper objects and then to deserialized the
json file to same object.'''

import json
import logging
import sys
import six
import os
import codecs

from .filelock import SecureOpen

logger = logging.getLogger(__name__)

#List of default keys for passwords
DEFAULT_PWD_KEYS = ["password", "infrastructurePassword",
                    "sourceSsoPassword", "vcSvcPassword",
                    "sourceManagementPassword", "targetManagementPassword",
                    "vcPassword"]
CENSORED_PWD='CENSORED'

def pruneSensitiveData(o, pwdKeys=None):
    '''Censor values of the dict keys specified by keys parameter.

    @param o: Dictionary to be prune
    @param keys: List of keys that contains data to be censored.

    @return: Dictionary with censored values
    '''
    pwdKeys = pwdKeys or DEFAULT_PWD_KEYS

    if isinstance(o, dict):
        result = {}
        for k, v in six.iteritems(o):
            if any([pwdKey in k for pwdKey in pwdKeys]):
                result[k] = CENSORED_PWD
            else:
                result[k] = pruneSensitiveData(v, pwdKeys)
        return result
    elif isinstance(o, list):
        return [pruneSensitiveData(element, pwdKeys) for element in o]
    else:
        return o

def toDict(obj, keepUtf8=True):
    '''Transforms a serializable object to a dictionary

    @param obj: Serializable object which will be tranformed
    @type obj: Serializable

    @param keepUtf8: Since json internally convert str to unicode, this
      parameter specifies if we should convert unicode back to utf-8, or
      leave the content as it is. Default will convert strings to utf-8
    @type keepUtf8: boolean

    @return: Dictionary representation of Serializable object
    @rtype: dict
    '''
    if obj is None:
        return {}

    if not isinstance(obj, Serializable):
        raise ValueError('Given object inherits type %s, not Serializable' % type(obj))

    cnt = json.dumps(obj, default=ObjectJsonConverter(False).encode)
    result = json.loads(cnt, object_hook=ObjectJsonConverter.decode)
    if keepUtf8:
        result = _convertToUtf8(result)

    return result

def _convertToUtf8(o):
    '''Converts unicode strings from given input to utf-8.

    @param o: Object to be decoded
    @return: Decoded object
    '''
    if isinstance(o, dict):
        result = {}
        for k, v in six.iteritems(o):
            result[_convertToUtf8(k)] = _convertToUtf8(v)
        return result
    elif isinstance(o, list):
        return [_convertToUtf8(element) for element in o]
    elif six.PY2 and isinstance(o, six.text_type):
        return o.encode('utf-8')
    else:
        return o

class Serializable(object):
    '''Interface allowing object to be serializaed/deserialized to/from json objects.
    '''
    def __serialize__(self):
        '''Encode the object to an object which could be serialized as json object.
        Default implementation serializes all instance variables, but exclude
        class variables from serialization process. If some of the instance
        variables cannot be properly serialized, you would need to add custom
        logic for their serialization.
        '''
        return vars(self)

    @classmethod
    def __deserialize__(cls, jObject):
        '''Decode the object from json object. Default implementation will call
        the object constructor where the key will be equal to instance variables
        names and values equal to the value before the object to be serialized.

        @param jObject: Json encoded object
        @type jObject: Same type returned in __serialize__

        @return: The deserialized object
        '''
        try:
            result = cls(**jObject)
            return result
        except:
            logger.error('Cannot call class constructor %s, with arguments %s',
                         cls, str(jObject))
            raise

class ObjectJsonConverter(object):
    '''The module should be plugged in the default json serialization/deserialization
    logic, allowing to serialize meta-data for custom Serializable objects.
    That metadata/signature would be used later by decoder/objectHook function
    to restore the original custom objects. Only Serializable objects could
    be properly serialized/deserialized

    Usage:
      originalMsg = _('upgrade.helloMessage, %s', 'Ivo')
      serializedMsg = json.dumps(originalMsg, default=ObjectJsonConverter(True).encode, indent=4)
      deserializedMsg = json.loads(serializedMsg, object_hook=ObjectJsonConverter.decode)
      print deserializedMsg, type(deserializedMsg), deserializedMsg.translatedMessage, deserializedMsg.args
    '''
    MODULE_SIGNATURE = '__obj_module__'
    CLASS_SIGNATURE = '__obj_type__'
    ENCODED_JSON_OBJECT = '__ENCODED_JSON_OBJECT__'

    def __init__(self, writeObjSignature=True, *_args, **_kwargs): #pylint: disable=W0231
        '''Creates ObjectSerializer.

        @param writeObjSignature: The signature has to be set if we are planning
          to deserialize the object later. If the json is planned to be sent to
          3rd party software, then writeObjSignature should be set to False,
          in order to keep json pretty printed.
        '''
        self.writeSignature = writeObjSignature

    def encode(self, o): #pylint: disable=E0202
        '''Encodes the object. If the object is Serializable, calls __serialize__
        function to give custom object ability to be properly transformed from
        object serializable to json. If the object is not Serializable then
        function will call JSONEncoder default function, which is going to raise
        TypeError informing that custom object cannot be serialized to json.

        @param o: Object which have to be serialized
        @type o: Serializable

        @return: Object which could be properly serialized to json.
        '''
        if isinstance(o, Serializable):
            if self.writeSignature:
                result = {
                    self.MODULE_SIGNATURE : o.__module__,
                    self.CLASS_SIGNATURE : o.__class__.__name__,
                    self.ENCODED_JSON_OBJECT : o.__serialize__()
                }
            else:
                result = o.__serialize__()
        else:
            # Leave JSONEncoder to report error
            result = json.JSONEncoder().default(o)

        return result

    @classmethod
    def decode(cls, encodedObj):
        '''Decodes serialized object to its original version. The function calls
        static __deserialize__ class function which have to restore the original
        serialized object.

        @param cls: ObjectJsonConverter class
        @type cls: type

        @param encodedObj: Serializable json object
        @type encodedObj: The type returned by #default function

        @return: Deserialized custom object
        @rtype: Serializable
        '''
        if cls.MODULE_SIGNATURE in encodedObj and cls.CLASS_SIGNATURE in encodedObj:
            modName = encodedObj[cls.MODULE_SIGNATURE]
            __import__(modName)
            objModule = sys.modules[modName]
            objCls = getattr(objModule, encodedObj[cls.CLASS_SIGNATURE])

            objFactory = getattr(objCls, '__deserialize__')
            objProperties = _convertToUtf8(encodedObj[cls.ENCODED_JSON_OBJECT])

            obj = objFactory(objProperties)
            return obj
        return encodedObj


class JsonSerializer(object):
    '''Serializer/Deserializer of python objects to/from json format.
    '''
    def serialize(self, o):
        '''Serializes object to byte array. If o is not json serializable, then
        it should implement json_utils.Serializable

        @param o: Serializable object
        @type o: object

        @return: Array of bytes as string representation
        @rtype: str
        '''
        cnt = json.dumps(o, default=ObjectJsonConverter(True).encode, indent=4)
        return cnt

    def deserialize(self, cnt, keepUtf8=True):
        '''Deserializes object from byte array.

        @param cnt: Array of bytes
        @type cnt: str

        @param keepUtf8: Since json internally convert str to unicode, this
          parameter specifies if we should convert unicode back to utf-8, or
          leave the content as it is. Default will convert strings to utf-8
        @type keepUtf8: boolean

        @return: Deserialized object
        @rtype: object
        '''
        o = None
        if cnt:
            o = json.loads(cnt, object_hook=ObjectJsonConverter.decode)

            if keepUtf8:
                o = _convertToUtf8(o)

        return o

class JsonFileSerializer(object):
    '''Responsible to serialize/de-serialize python objects to/from json file.
    '''
    def __init__(self, filePath, json_serializer=None):
        ''' Create an instance of JsonFileSerializer class.

        @param filePath: A path to the file read/write the python objects
        @type filePath: str

        @param json_serializer: A serializer responsible to serialize python
            objects from/to json
        @type json_serializer: JsonSerializer
        '''
        self.filePath = filePath
        self.serializer = json_serializer or JsonSerializer()

    def serialize(self, o):
        '''Serializes object to json file. If o is not json serializable, then
        it should implement json_utils.Serializable

        @param o: Serializable object
        @type o: object

        @return: Content of the json file
        @rtype: str
        '''
        with SecureOpen(self.filePath, open, True, 'w') as fp:
            fileCnt = self.serializer.serialize(o)
            fp.write(fileCnt)

        return fileCnt

    def deserialize(self, keepUtf8=True):
        '''Deserializes object from json file.

        @param keepUtf8: Since json internally convert str to unicode, this
          parameter specifies if we should convert unicode back to utf-8, or
          leave the content as it is. Default will convert strings to utf-8
        @type keepUtf8: boolean

        @return: Deserialized object or None if file does not exist.
        @rtype: object
        '''
        result = None
        if os.path.exists(self.filePath):
            with open(self.filePath) as fp:
                fileCnt = fp.read()
                result = self.serializer.deserialize(fileCnt, keepUtf8)

        return result

def prettyJsonDump(outputFile, jsonSerializable):
    '''Persists the json serializable into a file as json format.

    @param outputFile: The output file where the content will be saved to
    @type outputFile: str

    @param jsonSerializable: Json serializable object
    @type jsonSerializable: json_utils.Serializable
    '''
    with SecureOpen(outputFile, codecs.open, False, "w", encoding='utf-8') as fp:
        json.dump(jsonSerializable, fp, indent=4,
                  default=ObjectJsonConverter(False).encode)
