# Copyright IBM Corp. 2017 All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import binascii
import logging
import pickle

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.serialization import load_pem_private_key

from hfc.fabric_ca.caservice import Enrollment
from hfc.util.crypto.crypto import ecies

_logger = logging.getLogger(__name__ + ".user")


class User(object):
    """The default implementation of user."""

    def __init__(self, name, org, state_store):
        """Constructor for a user.

        :param name: name
        :param org: org
        :param state_store: persistent state store as a cache
        :return: An instance of user object
        """
        self._name = name
        self._org = org
        self._state_store = state_store
        self._state_store_key = "user." + name + "." + org
        self._roles = []
        self._account = None
        self._affiliation = None
        self._enrollment_secret = None
        self._enrollment = None
        self._msp_id = None
        self._cryptoSuite = None

        user_state = state_store.get_value(self._state_store_key)

        if not user_state:
            self._save_state()
        else:
            self._restore_state()

    @property
    def name(self):
        """Get the user name
        :return: The user name
        """
        return self._name

    @property
    def org(self):
        """Get the org
        :return: The org
        """
        return self._org

    @property
    def roles(self):
        """Get the roles
        :return: The roles
        """
        return self._roles

    @roles.setter
    def roles(self, roles):
        """Set the roles

        :param roles: the roles
        :return:
        """
        self._roles = roles
        self._save_state()

    @property
    def account(self):
        """Get the account
        :return: The account
        """
        return self._account

    @account.setter
    def account(self, account):
        """Set the account

        :param account: the account
        :return:
        """
        self._account = account
        self._save_state()

    @property
    def affiliation(self):
        """Get the affiliation
        :return: The affiliation
        """
        return self._affiliation

    @affiliation.setter
    def affiliation(self, affiliation):
        """Set the affiliation

        :param affiliation: the affiliation
        :return:
        """
        self._affiliation = affiliation
        self._save_state()

    @property
    def enrollment(self):
        """Get the enrollment"""
        return self._enrollment

    @enrollment.setter
    def enrollment(self, enrollment):
        """Set the enrollment

        :param enrollment: the enrollment
        :return:
        """
        self._enrollment = enrollment
        self._save_state()

    @property
    def enrollment_secret(self):
        """Get the enrollment_secret"""
        return self._enrollment_secret

    @enrollment_secret.setter
    def enrollment_secret(self, enrollment_secret):
        """Set the enrollment_secret

        :param enrollment_secret: the enrollment_secret
        :return:
        """
        self._enrollment_secret = enrollment_secret
        self._save_state()

    @property
    def msp_id(self):
        """Get the msp_id"""
        return self._msp_id

    @msp_id.setter
    def msp_id(self, msp_id):
        """Set the msp_id

        :param msp_id: the msp_id
        :return:
        """
        self._msp_id = msp_id
        self._save_state()

    @property
    def cryptoSuite(self):
        """Get the cryptoSuite"""
        return self._cryptoSuite

    @cryptoSuite.setter
    def cryptoSuite(self, cryptoSuite):
        """Set the cryptoSuite

        :param msp_id: the cryptoSuite
        :param cryptoSuite:
        :return:
        """
        self._cryptoSuite = cryptoSuite
        self._save_state()

    def is_registered(self):
        """Check if user registered

        :return: boolean
        """
        return self._enrollment_secret is not None

    def is_enrolled(self):
        """Check if user enrolled

        :return: boolean
        """
        return self._enrollment is not None

    def _save_state(self):
        """Persistent user state."""
        try:
            state = {
                'name': self.name, 'org': self.org, 'roles': self.roles,
                'affiliation': self.affiliation, 'account': self.account,
                'enrollment_secret': self.enrollment_secret,
                'msp_id': self.msp_id
            }

            if self.enrollment:
                enrollment = {
                    'private_key':
                        self.enrollment.private_key.private_bytes(
                            encoding=serialization.Encoding.PEM,
                            format=serialization.PrivateFormat.PKCS8,
                            encryption_algorithm=serialization.NoEncryption()
                        ),
                    'cert': self.enrollment.cert
                }

                state['enrollment'] = enrollment

            self._state_store.set_value(
                self._state_store_key,
                binascii.hexlify(pickle.dumps(state)).decode("utf-8"))
        except Exception as e:
            raise IOError("Cannot serialize the user", e)

    def _restore_state(self):
        """Restore user state."""
        try:
            state = self._state_store.get_value(self._state_store_key)
            state_dict = pickle.loads(
                binascii.unhexlify(state.encode("utf-8")))
            self._name = state_dict['name']
            self.enrollment_secret = state_dict['enrollment_secret']
            enrollment = state_dict['enrollment']
            if enrollment:
                private_key = serialization.load_pem_private_key(
                    enrollment['private_key'],
                    password=None,
                    backend=default_backend()
                )
                cert = enrollment['cert']
                self.enrollment = Enrollment(private_key, cert)
            self.affiliation = state_dict['affiliation']
            self.account = state_dict['account']
            self.roles = state_dict['roles']
            self._org = state_dict['org']
            self.msp_id = state_dict['msp_id']
        except Exception as e:
            raise IOError("Cannot deserialize the user", e)

    def get_attrs(self):
        return ",".join("{}={}"
                        .format(k, getattr(self, k))
                        for k in self.__dict__.keys())

    def __str__(self):
        return "[{}:{}]".format(self.__class__.__name__, self.get_attrs())


def validate(user):
    """Check the user.

    :param user: A user object
    :return: A validated user object
    :raises ValueError: When user property is invalid
    """
    if not user:
        raise ValueError("User cannot be empty.")

    if not user.name:
        raise ValueError("Missing user name.")

    enrollment = user.enrollment
    if not enrollment:
        raise ValueError("Missing user enrollment.")

    if not enrollment.cert:
        raise ValueError("Missing user enrollment cert.")

    if not enrollment.private_key:
        raise ValueError("Missing user enrollment key.")

    if not user.msp_id:
        raise ValueError("Missing msp id.")

    if not user.cryptoSuite:
        raise ValueError("Missing crypto suite.")

    return user


def create_user(name, org, state_store, msp_id, key_path, cert_path,
                crypto_suite=ecies()):
    """Create user

    :param name: user's name
    :param org: org name
    :param state_store: user state store
    :param msp_id: msp id for the user
    :param crypto_suite: the cryptoSuite used to store crypto and key store
         settings (Default value = ecies())
    :param key_path: identity private key path
    :param cert_path: identity public cert path
    :return: a user instance
    """

    _logger.debug("Create user with {}:{}:{}:{}:{}:{}".format(
        name, org, state_store, msp_id, key_path, cert_path
    ))
    with open(key_path, 'rb') as key:
        key_pem = key.read()

    with open(cert_path, 'rb') as cert:
        cert_pem = cert.read()

    private_key = load_pem_private_key(key_pem, None, default_backend())
    enrollment = Enrollment(private_key, cert_pem)

    user = User(name, org, state_store)
    user.enrollment = enrollment
    user.msp_id = msp_id
    user.cryptoSuite = crypto_suite

    return validate(user)
