from _sha256 import sha256
from copy import deepcopy
from json import JSONDecodeError
from typing import Optional

from indy_common.authorize.auth_actions import AuthActionEdit, AuthActionAdd
from indy_common.authorize.auth_request_validator import WriteRequestValidator
from indy_common.serialization import attrib_raw_data_serializer
from indy_common.state import domain

from indy_common.constants import ATTRIB
from indy_common.state.state_constants import MARKER_ATTR
from indy_node.server.request_handlers.utils import validate_attrib_keys
from plenum.common.constants import DOMAIN_LEDGER_ID, RAW, ENC, HASH, TARGET_NYM
from plenum.common.exceptions import InvalidClientRequest

from plenum.common.request import Request
from plenum.common.txn_util import get_type, get_request_data, get_payload_data, get_seq_no, get_txn_time
from plenum.server.database_manager import DatabaseManager
from plenum.server.request_handlers.handler_interfaces.write_request_handler import WriteRequestHandler
from plenum.server.request_handlers.utils import encode_state_value
from stp_core.common.log import getlogger

logger = getlogger()

ALL_ATR_KEYS = [RAW, ENC, HASH]


class AttributeHandler(WriteRequestHandler):

    def __init__(self, database_manager: DatabaseManager,
                 write_req_validator: WriteRequestValidator):
        super().__init__(database_manager, ATTRIB, DOMAIN_LEDGER_ID)
        self.write_req_validator = write_req_validator

    def static_validation(self, request: Request):
        self._validate_request_type(request)
        identifier, req_id, operation = get_request_data(request)

        if not validate_attrib_keys(operation):
            raise InvalidClientRequest(identifier, req_id,
                                       '{} should have one and only one of '
                                       '{}, {}, {}'
                                       .format(ATTRIB, RAW, ENC, HASH))

        if RAW in operation:
            try:
                get_key = attrib_raw_data_serializer.deserialize(operation[RAW])
                if len(get_key) == 0:
                    raise InvalidClientRequest(identifier, request.reqId,
                                               '"raw" attribute field must contain non-empty JSON object {}'.
                                               format(TARGET_NYM))
            except JSONDecodeError:
                raise InvalidClientRequest(identifier, request.reqId,
                                           'Attribute field must be valid JSON object when adding it as a raw field {}'.
                                           format(TARGET_NYM))

    def additional_dynamic_validation(self, request: Request, req_pp_time: Optional[int]):
        self._validate_request_type(request)

        identifier, req_id, operation = get_request_data(request)

        if not (not operation.get(TARGET_NYM) or self.__has_nym(operation[TARGET_NYM], is_committed=False)):
            raise InvalidClientRequest(identifier, request.reqId,
                                       '{} should be added before adding '
                                       'attribute for it'.
                                       format(TARGET_NYM))

        is_owner = self.database_manager.idr_cache.getOwnerFor(operation[TARGET_NYM],
                                                               isCommitted=False) == identifier
        field = None
        value = None
        for key in (RAW, ENC, HASH):
            if key in operation:
                field = key
                value = operation[key]
                break

        if field == RAW:
            get_key = attrib_raw_data_serializer.deserialize(value)
            get_key = list(get_key.keys())[0]
        else:
            get_key = value

        old_value, seq_no, _ = self._get_attr(operation[TARGET_NYM], get_key, field)

        if seq_no is not None:
            self.write_req_validator.validate(request,
                                              [AuthActionEdit(txn_type=ATTRIB,
                                                              field=field,
                                                              old_value=old_value,
                                                              new_value=value,
                                                              is_owner=is_owner)])
        else:
            self.write_req_validator.validate(request,
                                              [AuthActionAdd(txn_type=ATTRIB,
                                                             field=field,
                                                             value=value,
                                                             is_owner=is_owner)])

    def gen_txn_id(self, txn):
        self._validate_txn_type(txn)
        path = self.prepare_attr_for_state(txn, path_only=True)
        return path.decode()

    def update_state(self, txn, prev_result, request, is_committed=False) -> None:
        """
        The state trie stores the hash of the whole attribute data at:
            the did+attribute name if the data is plaintext (RAW)
            the did+hash(attribute) if the data is encrypted (ENC)
        If the attribute is HASH, then nothing is stored in attribute store,
        the trie stores a blank value for the key did+hash
        """
        self._validate_txn_type(txn)
        attr_type, path, value, hashed_value, value_bytes = self.prepare_attr_for_state(txn)
        self.state.set(path, value_bytes)
        if attr_type != HASH:
            self.database_manager.attribute_store.set(hashed_value, value)
        return txn

    def _get_attr(self,
                  did: str,
                  key: str,
                  attr_type,
                  is_commited=False) -> (str, int, int, list):
        assert did is not None
        assert key is not None
        path = AttributeHandler.make_state_path_for_attr(did, key, attr_type == HASH)
        try:
            hashed_val, lastSeqNo, lastUpdateTime = self.get_from_state(path, is_commited)
        except KeyError:
            return None, None, None, None
        if not hashed_val or hashed_val == '':
            # Its a HASH attribute
            return hashed_val, lastSeqNo, lastUpdateTime
        else:
            value = self.database_manager.attribute_store.get(hashed_val)
        return value, lastSeqNo, lastUpdateTime

    def __has_nym(self, nym, is_committed: bool = True):
        return self.database_manager.idr_cache.hasNym(nym, isCommitted=is_committed)

    @staticmethod
    def prepare_attr_for_state(txn, path_only=False):
        """
        Make key(path)-value pair for state from ATTRIB or GET_ATTR
        :return: state path, state value, value for attribute store
        """
        assert get_type(txn) == ATTRIB
        txn_data = get_payload_data(txn)
        nym = txn_data[TARGET_NYM]
        attr_type, attr_key, value = AttributeHandler.parse_attr_txn(txn_data)
        path = AttributeHandler.make_state_path_for_attr(nym, attr_key, attr_type == HASH)
        if path_only:
            return path
        hashed_value = domain.hash_of(value) if value else ''
        seq_no = get_seq_no(txn)
        txn_time = get_txn_time(txn)
        value_bytes = encode_state_value(hashed_value, seq_no, txn_time)
        return attr_type, path, value, hashed_value, value_bytes

    @staticmethod
    def parse_attr_txn(txn_data):
        attr_type, attr = AttributeHandler._extract_attr_typed_value(txn_data)

        if attr_type == RAW:
            data = attrib_raw_data_serializer.deserialize(attr)
            # To exclude user-side formatting issues
            re_raw = attrib_raw_data_serializer.serialize(data,
                                                          toBytes=False)
            key, _ = data.popitem()
            return attr_type, key, re_raw
        if attr_type == ENC:
            return attr_type, attr, attr
        if attr_type == HASH:
            return attr_type, attr, None

    @staticmethod
    def _extract_attr_typed_value(txn_data):
        """
        ATTR and GET_ATTR can have one of 'raw', 'enc' and 'hash' fields.
        This method checks which of them presents and return it's name
        and value in it.
        """
        existing_keys = [key for key in ALL_ATR_KEYS if key in txn_data]
        if len(existing_keys) == 0:
            raise ValueError("ATTR should have one of the following fields: {}"
                             .format(ALL_ATR_KEYS))
        if len(existing_keys) > 1:
            raise ValueError("ATTR should have only one of the following fields: {}"
                             .format(ALL_ATR_KEYS))
        existing_key = existing_keys[0]
        return existing_key, txn_data[existing_key]

    @staticmethod
    def make_state_path_for_attr(did, attr_name, attr_is_hash=False) -> bytes:
        nameHash = sha256(attr_name.encode()).hexdigest() if not attr_is_hash else attr_name
        return "{DID}:{MARKER}:{ATTR_NAME}" \
            .format(DID=did,
                    MARKER=MARKER_ATTR,
                    ATTR_NAME=nameHash).encode()

    def transform_txn_for_ledger(self, txn):
        """
        Creating copy of result so that `RAW`, `ENC` or `HASH` can be
        replaced by their hashes. We do not insert actual attribute data
        in the ledger but only the hash of it.
        """
        txn = deepcopy(txn)
        txn_data = get_payload_data(txn)
        attr_type, _, value = self.parse_attr_txn(txn_data)
        if attr_type in [RAW, ENC]:
            txn_data[attr_type] = domain.hash_of(value) if value else ''

        return txn
