#
# Copyright 2020 Venafi, Inc.
#
# 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 base64
import logging as log
import re
import time
from datetime import datetime, timedelta

from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.x509 import SignatureAlgorithmOID as AlgOID
from six.moves.urllib import parse as url_parse
from types import SimpleNamespace

from .common import CertField, CommonConnection, CertificateRequest, CSR_ORIGIN_LOCAL, CSR_ORIGIN_PROVIDED, \
    CSR_ORIGIN_SERVICE, KeyType, CHAIN_OPTION_LAST, CHAIN_OPTION_FIRST, CHAIN_OPTION_IGNORE, Policy, ZoneConfig
from .errors import VenafiError, ServerUnexptedBehavior, ClientBadData, RetrieveCertificateTimeoutError, \
    CertificateRequestError, CertificateRenewError
from .http_status import HTTPStatus
from .pem import parse_pem
from .policy import RPA, SPA
from .policy.pm_tpp import TPPPolicy, is_service_generated_csr, SetAttrResponse, validate_policy_spec, \
    get_int_value, build_identity_entry
from .ssh_utils import SSHCertRequest, SSHCertResponse, build_tpp_retrieve_request, SSHResponse, \
    SSHRetrieveResponse, build_tpp_request, SSHCATemplateRequest, SSHConfig, PATH_SEPARATOR, CA_ROOT_PATH, \
    SSHTPPCADetails
from .tpp_utils import IssuerHint

POLICY_ATTR_CLASS = "X509 Certificate"  # type: str
ROOT_PATH = "\\VED\\Policy\\"  # type: str
POLICY_CLASS = "Policy"  # type: str


class URLS:
    API_TOKEN_URL = "vedauth/"  # type: str  # nosec
    API_BASE_URL = "vedsdk/"  # type: str  # nosec

    AUTHORIZE_TOKEN = API_TOKEN_URL + "authorize/oauth"  # type: str
    REFRESH_TOKEN = API_TOKEN_URL + "authorize/token"  # type: str
    REVOKE_TOKEN = API_TOKEN_URL + "revoke/token"  # type: str

    AUTHORIZE = API_BASE_URL + "authorize/"
    CERTIFICATE_REQUESTS = API_BASE_URL + "certificates/request"
    CERTIFICATE_RETRIEVE = API_BASE_URL + "certificates/retrieve"
    FIND_POLICY = API_BASE_URL + "config/findpolicy"
    CERTIFICATE_REVOKE = API_BASE_URL + "certificates/revoke"
    CERTIFICATE_RENEW = API_BASE_URL + "certificates/renew"
    CERTIFICATE_SEARCH = API_BASE_URL + "certificates/"
    CERTIFICATE_IMPORT = API_BASE_URL + "certificates/import"
    ZONE_CONFIG = API_BASE_URL + "certificates/checkpolicy"
    CONFIG_READ_DN = API_BASE_URL + "Config/ReadDn"

    POLICY_IS_VALID = API_BASE_URL + "config/isvalid"
    POLICY_CREATE = API_BASE_URL + "config/create"
    POLICY_SET_ATTRIBUTE = API_BASE_URL + "config/writepolicy"
    POLICY_CLEAR_ATTRIBUTE = API_BASE_URL + "config/clearpolicyattribute"
    POLICY_BROWSE_IDENTITY = API_BASE_URL + "identity/browse"
    POLICY_VALIDATE_IDENTITY = API_BASE_URL + "identity/validate"

    SSH_BASE_URL = API_BASE_URL + "SSHCertificates/"
    SSH_CERTIFICATE_REQUEST = SSH_BASE_URL + "request"
    SSH_CERTIFICATE_RETRIEVE = SSH_BASE_URL + "retrieve"
    SSH_CA_DETAILS = SSH_BASE_URL + "Template/Retrieve"
    SSH_CA_PUBLIC_KEY = SSH_CA_DETAILS + "/PublicKeyData"

    def __init__(self):
        pass


class AbstractTPPConnection(CommonConnection):
    def __init__(self):
        super().__init__()

    ARG_URL = 'url'
    ARG_PARAMS = 'params'
    ARG_CHECK_TOKEN = 'check_token'  # nosec
    ARG_INCLUDE_TOKEN_HEADER = 'include_token_header'  # nosec
    ARG_DATA = 'data'
    IDENTITY_USER = 1
    IDENTITY_SECURITY_GROUP = 2
    IDENTITY_DISTRIBUTION_GROUP = 8
    ALL_IDENTITIES = IDENTITY_USER + IDENTITY_SECURITY_GROUP + IDENTITY_DISTRIBUTION_GROUP

    def auth(self):
        raise NotImplementedError

    def request_cert(self, request, zone):
        request_data = {
            'PolicyDN': self._normalize_zone(zone),
            'ObjectName': request.friendly_name,
            'DisableAutomaticRenewal': "true",
            'CertificateType': "Auto"
        }
        zone_config = self.read_zone_conf(zone)
        request.update_from_zone_config(zone_config)

        if request.csr_origin == CSR_ORIGIN_LOCAL:
            request.build_csr()

        if request.csr_origin in [CSR_ORIGIN_PROVIDED, CSR_ORIGIN_LOCAL]:
            request_data['PKCS10'] = request.csr
        elif request.csr_origin == CSR_ORIGIN_SERVICE:
            request_data['Subject'] = request.common_name
            request_data['SubjectAltNames'] = self.wrap_alt_names(request)
        else:
            log.error(f"CSR Origin option [{request.csr_origin}] is not valid")
            raise ClientBadData

        if request.origin:
            request_data['Origin'] = request.origin
            ca_origin = {'Name': "Origin", 'Value': request.origin}
            if request_data.get('CASpecificAttributes'):
                request_data['CASpecificAttributes'].append(ca_origin)
            else:
                request_data['CASpecificAttributes'] = [ca_origin]

        if request.validity_hours is not None:
            if request.issuer_hint == IssuerHint.MICROSOFT:
                exp_date_attr = IssuerHint.MICROSOFT.json_value
            elif request.issuer_hint == IssuerHint.DIGICERT:
                exp_date_attr = IssuerHint.DIGICERT.json_value
            elif request.issuer_hint == IssuerHint.ENTRUST:
                exp_date_attr = IssuerHint.ENTRUST.json_value
            else:
                exp_date_attr = IssuerHint.DEFAULT.json_value

            expiration_date = datetime.utcnow() + timedelta(hours=request.validity_hours)
            formatted_expiration_date = expiration_date.strftime("%Y-%m-%d %H:%M:%S")

            expiration_date = {'Name': exp_date_attr, 'Value': formatted_expiration_date}
            if request_data.get('CASpecificAttributes'):
                request_data['CASpecificAttributes'].append(expiration_date)
            else:
                request_data['CASpecificAttributes'] = [expiration_date]

        if request.custom_fields:
            custom_fields_map = {}
            for c_field in request.custom_fields:
                if custom_fields_map.get(c_field.name):
                    custom_fields_map[c_field.name].append(c_field.value)
                else:
                    custom_fields_map[c_field.name] = [c_field.value]

            for key in custom_fields_map:
                custom_field_json = {
                    'Name': key,
                    'Values': custom_fields_map[key]
                }
                if request_data.get('CustomFields'):
                    request_data['CustomFields'].append(custom_field_json)
                else:
                    request_data['CustomFields'] = [custom_field_json]

        args = {
            self.ARG_URL: URLS.CERTIFICATE_REQUESTS,
            self.ARG_DATA: request_data
        }
        status, data = self.post(args)
        if status == HTTPStatus.OK:
            request.id = data['CertificateDN']
            request.cert_guid = data['Guid']
            log.debug(f"Certificate successfully requested with request id {request.id}")
            log.debug(f"Certificate successfully requested with GUID {request.cert_guid}")
            return True

        log.error(f"Request status is not {HTTPStatus.OK}. {status}")
        raise CertificateRequestError

    def retrieve_cert(self, cert_request):
        log.debug(f"Getting certificate status for id {cert_request.id}")

        retrieve_request = dict(CertificateDN=cert_request.id,
                                Format="Base64 (PKCS #8)",
                                IncludeChain=True)

        if cert_request.use_legacy_pem:
            retrieve_request["Format"] = "base64"

        if cert_request.csr_origin == CSR_ORIGIN_SERVICE:
            retrieve_request['IncludePrivateKey'] = cert_request.include_private_key
            if cert_request.key_password:
                # The password is encoded when assigned (for local use, I suppose).
                # decode is needed to send a raw string
                retrieve_request['Password'] = cert_request.key_password.decode()

        if cert_request.chain_option == CHAIN_OPTION_LAST:
            retrieve_request['RootFirstOrder'] = 'false'
            retrieve_request['IncludeChain'] = 'true'
        elif cert_request.chain_option == CHAIN_OPTION_FIRST:
            retrieve_request['RootFirstOrder'] = 'true'
            retrieve_request['IncludeChain'] = 'true'
        elif cert_request.chain_option == CHAIN_OPTION_IGNORE:
            retrieve_request['IncludeChain'] = 'false'
        else:
            log.error(f"chain option {cert_request.chain_option} is not valid")
            raise ClientBadData

        time_start = time.time()
        while True:
            try:
                # TODO: Change _post() with post(args)
                status, data = self._post(URLS.CERTIFICATE_RETRIEVE, data=retrieve_request)
            except VenafiError:
                log.debug(f"Certificate with id {cert_request.id} not found")
                status = 0

            if status == HTTPStatus.OK:
                pem64 = data['CertificateData']
                pem = base64.b64decode(pem64)
                cert_response = parse_pem(pem.decode(), cert_request.chain_option)
                if cert_response.key is None and cert_request.private_key is not None:
                    log.debug("Adding private key to response...")
                    cert_response.key = cert_request.private_key_pem
                return cert_response
            elif (time.time() - time_start) < cert_request.timeout:
                log.debug("Waiting for certificate...")
                time.sleep(2)
            else:
                raise RetrieveCertificateTimeoutError(f"Operation timed out at {cert_request.timeout} seconds while "
                                                      f"retrieving certificate with id {cert_request.id}")

    def renew_cert(self, request, reuse_key=False):
        if not request.id and not request.thumbprint:
            log.debug("Request id or thumbprint must be specified for TPP")
            raise CertificateRenewError
        if not request.id and request.thumbprint:
            request.id = self.search_by_thumbprint(request.thumbprint)

        if reuse_key:
            log.debug(f"Trying to renew certificate {request.id}")
            # TODO: Change _post() with post(args)
            status, data = self._post(URLS.CERTIFICATE_RENEW, data={'CertificateDN': request.id})
            if not data['Success']:
                raise CertificateRenewError
            return

        cert = self.retrieve_cert(request)
        cert = x509.load_pem_x509_certificate(cert.cert.encode(), default_backend())
        for a in cert.subject:
            if a.oid == x509.NameOID.COMMON_NAME:
                request.common_name = a.value
            elif a.oid == x509.NameOID.COUNTRY_NAME:
                request.country = a.value
            elif a.oid == x509.NameOID.LOCALITY_NAME:
                request.locality = a.value
            elif a.oid == x509.NameOID.STATE_OR_PROVINCE_NAME:
                request.province = a.value
            elif a.oid == x509.NameOID.ORGANIZATION_NAME:
                request.organization = a.value
            elif a.oid == x509.NameOID.ORGANIZATIONAL_UNIT_NAME:
                request.organizational_unit = a.value
        for e in cert.extensions:
            dns = []
            emails = []
            ips = []
            upns = []
            uris = []
            if e.oid == x509.OID_SUBJECT_ALTERNATIVE_NAME:
                for x in e.value:
                    if isinstance(x, x509.DNSName):
                        dns.append(x.value)
                    elif isinstance(x, x509.RFC822Name):
                        emails.append(x.value)
                    elif isinstance(x, x509.IPAddress):
                        ips.append(x.value)
                    elif isinstance(x, x509.OtherName):
                        # remove header bytes from ASN1 encoded UPN field before setting it in the request object
                        upns.append(x.value[2::])
                    elif isinstance(x, x509.UniformResourceIdentifier):
                        uris.append(x.value)
                # request.san_dns = list([x.value for x in e.value if isinstance(x, x509.DNSName)])
                # request.email_addresses = list([x.value for x in e.value if isinstance(x, x509.RFC822Name)])
                # request.ip_addresses = list([x.value.exploded for x in e.value if isinstance(x, x509.IPAddress)])
                # remove header bytes from ASN1 encoded UPN field before setting it in the request object
                # upns = []
                # for x in e.value:
                #     if isinstance(x, x509.OtherName):
                #         upns.append(x.value[2::])
                # request.user_principal_names = upns
                # request.uniform_resource_identifiers = \
                #     list([x.value for x in e.value if isinstance(x, x509.UniformResourceIdentifier)])
            request.san_dns = dns
            request.email_addresses = emails
            request.ip_addresses = ips
            request.user_principal_names = upns
            request.uniform_resource_identifiers = uris

        if request.csr_origin == CSR_ORIGIN_LOCAL:
            if cert.signature_algorithm_oid in (AlgOID.ECDSA_WITH_SHA1, AlgOID.ECDSA_WITH_SHA224,
                                                AlgOID.ECDSA_WITH_SHA256, AlgOID.ECDSA_WITH_SHA384,
                                                AlgOID.ECDSA_WITH_SHA512):
                request.key_type = (KeyType.ECDSA, KeyType.ALLOWED_CURVES[0])
            else:
                request.key_type = KeyType(KeyType.RSA, 2048)  # todo: make parsing key size
            request.build_csr()

        request_data = {'CertificateDN': request.id}
        if request.csr_origin in [CSR_ORIGIN_PROVIDED, CSR_ORIGIN_LOCAL]:
            request_data['PKCS10'] = request.csr
        elif request.csr_origin == CSR_ORIGIN_SERVICE:
            request_data['Subject'] = request.common_name
            request_data['SubjectAltNames'] = self.wrap_alt_names(request)

        # TODO: Change _post() with post(args)
        status, data = self._post(URLS.CERTIFICATE_RENEW, data=request_data)
        if status == HTTPStatus.OK:
            if 'CertificateDN' in data:
                request.id = data['CertificateDN']
            log.debug(f"Certificate successfully requested with request id {request.id}")
            return True

        log.error(f"Request status is not {HTTPStatus.OK}. {status}")
        raise CertificateRequestError

    def revoke_cert(self, request):
        if not (request.id or request.thumbprint):
            raise ClientBadData
        d = {
            'Disable': request.disable
        }
        if request.reason:
            d['Reason'] = request.reason
        if request.id:
            d['CertificateDN'] = request.id
        elif request.thumbprint:
            d['Thumbprint'] = request.thumbprint
        else:
            raise ClientBadData
        if request.comments:
            d['Comments'] = request.comments
        # TODO: Change _post() with post(args)
        status, data = self._post(URLS.CERTIFICATE_REVOKE, data=d)
        if status in (HTTPStatus.OK, HTTPStatus.ACCEPTED):
            return data

        raise ServerUnexptedBehavior

    def import_cert(self, request):
        raise NotImplementedError

    def read_zone_conf(self, tag):
        args = {
            self.ARG_URL: URLS.ZONE_CONFIG,
            self.ARG_DATA: {
                'PolicyDN': self._normalize_zone(tag)
            }
        }
        status, data = self.post(args=args)
        if status != HTTPStatus.OK:
            raise ServerUnexptedBehavior(f"Server returns {status} status on reading zone configuration")
        return self._parse_zone_data_to_object(data)

    def get_policy(self, zone):
        # get policy spec from name
        policy_name = self._normalize_zone(zone)
        exists = self._policy_exists(policy_name)
        if not exists:
            log.error(f"The Policy {policy_name} does not exist")
            raise VenafiError

        # TODO: Change _post() with post(args)
        status, data = self._post(URLS.ZONE_CONFIG, {"PolicyDN": policy_name})
        if status != HTTPStatus.OK:
            raise ServerUnexptedBehavior(f"Server returns {status} status on reading policy configuration")

        if not ('Policy' in data):
            raise VenafiError(f"Policy structure not found in response data for [{policy_name}] policy")
        p = data['Policy']
        tpp_policy = TPPPolicy()

        value = 'Value'
        locked = 'Locked'

        # Contact
        if RPA.TPP_CONTACT in p:
            tpp_policy.contact = [p[RPA.TPP_CONTACT][value]]

        # Approver
        if RPA.TPP_APPROVER in p:
            tpp_policy.approver = [p[RPA.TPP_APPROVER][value]]

        # Prohibited wildcard
        if RPA.TPP_WILDCARDS_ALLOWED in p:
            tpp_policy.wildcards_allowed = p[RPA.TPP_WILDCARDS_ALLOWED]

        # Domain suffix whitelist
        if RPA.TPP_DOMAIN_SUFFIX_WHITELIST in p:
            tpp_policy.domain_suffix_whitelist = p[RPA.TPP_DOMAIN_SUFFIX_WHITELIST]

        # Certification Authority
        if RPA.TPP_CERT_AUTHORITY in p:
            tpp_policy.cert_authority = p[RPA.TPP_CERT_AUTHORITY][value]

        # Management Type
        if RPA.TPP_MANAGEMENT_TYPE in p:
            tpp_policy.management_type = p[RPA.TPP_MANAGEMENT_TYPE][value]

        if not ('Subject' in p):
            raise VenafiError(f"Subject structure not found in response data for [{policy_name}] policy")
        subject = p['Subject']

        # Organization
        if RPA.TPP_ORGANIZATION in subject:
            org = subject[RPA.TPP_ORGANIZATION]
            tpp_policy.org = CertField(org[value], org[locked])

        # Organizational Unit
        if RPA.TPP_ORG_UNIT in subject:
            ou = subject[RPA.TPP_ORG_UNIT]
            tpp_policy.org_unit = CertField(ou[value+"s"], ou[locked])

        # City
        if RPA.TPP_CITY in subject:
            city = subject[RPA.TPP_CITY]
            tpp_policy.city = CertField(city[value], city[locked])

        # State
        if RPA.TPP_STATE in subject:
            st = subject[RPA.TPP_STATE]
            tpp_policy.state = CertField(st[value], st[locked])

        # Country
        if RPA.TPP_COUNTRY in subject:
            country = subject[RPA.TPP_COUNTRY]
            tpp_policy.country = CertField(country[value], country[locked])

        if not ('KeyPair' in p):
            raise VenafiError(f"KeyPair structure not found in response data for [{policy_name}] policy")
        kp = p['KeyPair']

        # Key Algorithm
        if RPA.TPP_KEY_ALGORITHM in kp:
            tpp_policy.key_algo = CertField(kp[RPA.TPP_KEY_ALGORITHM][value], kp[RPA.TPP_KEY_ALGORITHM][locked])

        # Key Bit Strength
        if RPA.TPP_KEY_SIZE in kp:
            tpp_policy.key_bit_str = CertField(kp[RPA.TPP_KEY_SIZE][value], kp[RPA.TPP_KEY_SIZE][locked])

        # Elliptic Curve
        if RPA.TPP_ELLIPTIC_CURVE in kp:
            tpp_policy.elliptic_curve = CertField(kp[RPA.TPP_ELLIPTIC_CURVE][value], kp[RPA.TPP_ELLIPTIC_CURVE][locked])

        # Manual CSR
        if RPA.TPP_CSR_GENERATION in p:
            bool_val = is_service_generated_csr(p[RPA.TPP_CSR_GENERATION][value])
            tpp_policy.service_generated = CertField(bool_val, p[RPA.TPP_CSR_GENERATION][locked])

        # ALLOWED SANS
        if RPA.TPP_DNS_ALLOWED in p:
            tpp_policy.dns_allowed = p[RPA.TPP_DNS_ALLOWED]
        if RPA.TPP_EMAIL_ALLOWED in p:
            tpp_policy.email_allowed = p[RPA.TPP_EMAIL_ALLOWED]
        if RPA.TPP_IP_ALLOWED in p:
            tpp_policy.ip_allowed = p[RPA.TPP_IP_ALLOWED]
        if RPA.TPP_UPN_ALLOWED in p:
            tpp_policy.upn_allowed = p[RPA.TPP_UPN_ALLOWED]
        if RPA.TPP_URI_ALLOWED in p:
            tpp_policy.uri_allowed = p[RPA.TPP_URI_ALLOWED]

        # Allow Private Key Reuse
        if RPA.TPP_ALLOWED_PRIVATE_KEY_REUSE in p:
            tpp_policy.allow_private_key_reuse = p[RPA.TPP_ALLOWED_PRIVATE_KEY_REUSE]

        # TPP Want Renewal
        # tpp_want_renewal, _ = self._get_policy_attr(policy_name, TPPPolicyAttr.TPP_WANT_RENEWAL)
        # if tpp_want_renewal:
        #     tpp_policy.want_renewal = tpp_want_renewal[0]

        log.info("Building Policy Specification")
        spec = tpp_policy.to_policy_spec()
        spec.users = self.retrieve_usernames_from_tpp_contacts(policy_name)
        return spec

    def set_policy(self, zone, policy_spec):
        """
        :param str zone:
        :param PolicySpecification policy_spec:
        :rtype: PolicySpecification
        """
        validate_policy_spec(policy_spec)
        tpp_policy = TPPPolicy.build_tpp_policy(policy_spec)
        name = self._normalize_zone(zone)
        tpp_policy.name = name
        identities_list = self.resolve_tpp_contacts(tpp_policy.contact)
        tpp_policy.contact = identities_list

        create_policy = False
        policy_exists = self._policy_exists(name)
        if not policy_exists:
            log.info(f"Policy [{name}] does not exist, validating parent existence")
            parent_name = self._get_policy_parent(name)
            if self._policy_exists(parent_name) or parent_name == ROOT_PATH:
                log.info(f"Parent policy [{parent_name}] exists")
                create_policy = True
            else:
                raise VenafiError(f"Parent Policy [{parent_name}] does not exist")

        # Create the policy if necessary
        if create_policy:
            log.info(f"Creating Policy [{name}]")
            policy_request_data = {
                'Class': POLICY_CLASS,
                'ObjectDN': name
            }
            # TODO: Change _post() with post(args)
            status, resp_data = self._post(URLS.POLICY_CREATE, data=policy_request_data)
            if status != HTTPStatus.OK:
                raise VenafiError(f"Failed to create policy [{name}]. Status {status}")

        # Set attributes to policy
        if tpp_policy.contact:
            self._set_policy_attr(name, SPA.TPP_CONTACT, tpp_policy.contact, True)
        if tpp_policy.approver:
            self._set_policy_attr(name, SPA.TPP_APPROVER, tpp_policy.approver, True)

        # Reset all values for existing policy before setting the new ones.
        # This way, values that do not get updated will be inherited from parent.
        if policy_exists:
            self._reset_policy(name)

        if tpp_policy.domain_suffix_whitelist:
            self._set_policy_attr(name, SPA.TPP_DOMAIN_SUFFIX_WHITELIST, tpp_policy.domain_suffix_whitelist, True)
        if tpp_policy.cert_authority:
            self._set_policy_attr(name, SPA.TPP_CERT_AUTHORITY, [tpp_policy.cert_authority], False)
        if tpp_policy.org:
            self._set_policy_attr(name, SPA.TPP_ORGANIZATION, [tpp_policy.org.value], tpp_policy.org.locked)
        if tpp_policy.org_unit:
            self._set_policy_attr(name, SPA.TPP_ORG_UNIT, [tpp_policy.org_unit.value], tpp_policy.org_unit.locked)
        if tpp_policy.city:
            self._set_policy_attr(name, SPA.TPP_CITY, [tpp_policy.city.value], tpp_policy.city.locked)
        if tpp_policy.state:
            self._set_policy_attr(name, SPA.TPP_STATE, [tpp_policy.state.value], tpp_policy.state.locked)
        if tpp_policy.country:
            self._set_policy_attr(name, SPA.TPP_COUNTRY, [tpp_policy.country.value], tpp_policy.state.locked)
        if tpp_policy.key_algo:
            self._set_policy_attr(name, SPA.TPP_KEY_ALGORITHM, [tpp_policy.key_algo.value], tpp_policy.key_algo.locked)
        if tpp_policy.key_bit_str:
            self._set_policy_attr(name, SPA.TPP_KEY_BIT_STR, [tpp_policy.key_bit_str.value],
                                  tpp_policy.key_bit_str.locked)
        if tpp_policy.elliptic_curve:
            self._set_policy_attr(name, SPA.TPP_ELLIPTIC_CURVE, [tpp_policy.elliptic_curve.value],
                                  tpp_policy.elliptic_curve.locked)
        if tpp_policy.management_type:
            self._set_policy_attr(name, SPA.TPP_MANAGEMENT_TYPE, [tpp_policy.management_type.value],
                                  tpp_policy.management_type.locked)

        if tpp_policy.wildcards_allowed is not None:
            int_val = get_int_value(not tpp_policy.wildcards_allowed)
            self._set_policy_attr(name, SPA.TPP_PROHIBIT_WILDCARD, [int_val], False)
        if tpp_policy.service_generated is not None:
            int_val = get_int_value(not tpp_policy.service_generated)
            self._set_policy_attr(name, SPA.TPP_MANUAL_CSR, [int_val], tpp_policy.service_generated.locked)
        if tpp_policy.allow_private_key_reuse is not None:
            int_val = get_int_value(tpp_policy.allow_private_key_reuse)
            self._set_policy_attr(name, SPA.TPP_ALLOWED_PRIVATE_KEY_REUSE, [int_val], True)
        if tpp_policy.want_renewal is not None:
            int_val = get_int_value(tpp_policy.want_renewal)
            self._set_policy_attr(name, SPA.TPP_WANT_RENEWAL, [int_val], True)

        prohibited_sans = tpp_policy.get_prohibited_sans()
        if len(prohibited_sans) > 0:
            self._set_policy_attr(name, SPA.TPP_PROHIBITED_SAN_TYPES, prohibited_sans, False)

        return

    def resolve_tpp_contacts(self, contacts):
        identities_id_list = list()
        if contacts:
            contacts = list(set(contacts))
            for contact in contacts:
                identity = self.get_tpp_identity(contact)
                identities_id_list.append(identity.prefixed_universal)
        return identities_id_list

    def get_tpp_identity(self, username):
        if not username or username == "":
            raise VenafiError("Identity cannot be empty")
        data = {
            'Filter': username,
            'Limit': 2,
            'IdentityType': self.ALL_IDENTITIES
        }
        try:
            status, response = self._post(URLS.POLICY_BROWSE_IDENTITY, data=data)
        except VenafiError as err:
            log.debug(f"Error while getting contact [{username}]: {err.args[0]}")
        if len(response['Identities']) == 0:
            raise VenafiError(f"The username [{username}] was not found.")
        identity = None
        if len(response['Identities']) > 1:
            for identy_entry in response['Identities']:
                identy_user = build_identity_entry(identy_entry)
                if identy_user.name.casefold() == username:
                    identity = identy_user
                    break
        else:
            identities = response['Identities'][0]
            identity = build_identity_entry(identities)
        if not identity:
            raise VenafiError(f"The username [{username}] was not found.")
        return identity

    def request_ssh_cert(self, request):
        """

        :param SSHCertRequest request:
        :rtype: bool
        """
        json_request = build_tpp_request(request)
        log.info(f"Requesting SSH Certificate with id {request.key_id}")
        # TODO: Change _post() with post(args)
        status, data = self._post(URLS.SSH_CERTIFICATE_REQUEST, json_request)

        if status == HTTPStatus.OK:
            response_object = SSHResponse(data['Response'])
            if response_object.success:
                cert_req_response = SSHCertResponse(data)
                request.pickup_id = cert_req_response.dn
                request.guid = cert_req_response.guid
                return True
            else:
                raise VenafiError(f"An error occurred with status {response_object.error_code}. "
                                  f"Message: {response_object.error_msg}")
        else:
            raise ServerUnexptedBehavior(f"Server returns {status} status on requesting SSH certificate.")

    def retrieve_ssh_cert(self, request):
        """

        :param SSHCertRequest request:
        :rtype: SSHRetrieveResponse
        """
        json_request = build_tpp_retrieve_request(request)
        log.info(f"Retrieving SSH Certificate with id {request.pickup_id}")

        time_start = time.time()
        while True:
            try:
                # TODO: Change _post() with post(args)
                status, data = self._post(URLS.SSH_CERTIFICATE_RETRIEVE, json_request)
            except VenafiError:
                log.debug(f"SSH Certificate with id {request.pickup_id} not found")
                status = 0

            if status == HTTPStatus.OK:
                response_object = SSHResponse(data['Response'])
                if response_object.success:
                    return SSHRetrieveResponse(data)
                else:
                    log.info(f"Failed to retrieve certificate with following details:"
                             f"\nDN: {json_request['DN']}"
                             f"\nGuid: {json_request['Guid']}"
                             f"\nErrorCode: {response_object.error_code}"
                             f"\nErrorMessage: {response_object.error_msg}")

            if (time.time() - time_start) < request.timeout:
                log.debug("Waiting for certificate...")
                time.sleep(2)
            else:
                raise RetrieveCertificateTimeoutError(f"Operation timed out at {request.timeout} seconds while "
                                                      f"retrieving SSH certificate with id {request.pickup_id}")

    def retrieve_ssh_config(self, ca_request):
        """

        :param SSHCATemplateRequest ca_request:
        :rtype: SSHConfig
        """
        key = None
        value = None
        if ca_request.template:
            key = 'DN'
            value = ca_request.template
            if not value.startswith(PATH_SEPARATOR):
                value = f"{PATH_SEPARATOR}{value}"
            if not value.startswith(CA_ROOT_PATH):
                value = f"{CA_ROOT_PATH}{value}"
        elif ca_request.guid:
            key = 'guid'
            value = ca_request.guid
        else:
            raise ClientBadData("CA Guid or CA template must be provided to retrieve SSH config.")

        value = url_parse.quote(value)
        query = f"{key}={value}"
        url = f"{URLS.SSH_CA_PUBLIC_KEY}?{query}"

        args = {
            self.ARG_URL: url,
            self.ARG_CHECK_TOKEN: False,
            self.ARG_INCLUDE_TOKEN_HEADER: False
        }
        status, data = self.get(args=args)
        if status == HTTPStatus.OK:
            ssh_config_response = SSHConfig()
            ssh_config_response.ca_public_key = data
            if self._is_valid_auth():
                details = self._retrieve_ssh_ca_details(ca_request)
                ssh_config_response.ca_principals = details.access_control.default_principals
            return ssh_config_response
        else:
            raise ServerUnexptedBehavior(f"Server returns {status} status on requesting "
                                         f"SSH CA Public Key Data for {key} = {value}")

    def get(self, args):
        """

        :param dict args:
        :rtype: tuple[Any, Any]
        """
        raise NotImplementedError

    def post(self, args):
        """

        :param dict args:
        :rtype: tuple[Any, Any]
        """
        raise NotImplementedError

    # ======================================== API IMPLEMENTATION ENDS ======================================== #
    # ========================================================================================================= #

    def _policy_exists(self, zone):
        """
        :param str zone:
        :rtype bool:
        """
        req_data = {'ObjectDN': zone}
        # TODO: Change _post() with post(args)
        status, data = self._post(URLS.POLICY_IS_VALID, data=req_data)

        if status != HTTPStatus.OK:
            raise ServerUnexptedBehavior(f"Could not complete request. Status {status}. {data}")

        if data['Result'] == 1 and data['Object']['TypeName'] == POLICY_CLASS:
            return True
        elif data['Result'] == 400 and 'Error' in data:
            return False

        log.error(f"Unknown error while executing. Status: {status}.Data: {data}.")
        raise VenafiError

    def _set_policy_attr(self, zone, attr_name, attr_values, locked):
        """
        :param str zone: The policy name
        :param str attr_name: The name of the attribute to be set
        :param any attr_values: The values assigned to the attr_name
        :param bool locked: Whether the attribute should be policy locked
        :rtype: tuple[str, SetAttrResponse]
        """
        data = {
            'Locked': locked,
            'ObjectDN': zone,
            'Class': POLICY_ATTR_CLASS,
            'AttributeName': attr_name,
            'Values': attr_values
        }

        # TODO: Change _post() with post(args)
        status, response = self._post(URLS.POLICY_SET_ATTRIBUTE, data=data)
        if status != HTTPStatus.OK:
            raise ServerUnexptedBehavior(f"Got status {status} from server")

        response = self._parse_attr_response(response)

        if response.error:
            raise VenafiError(f"Error while setting attribute [{attr_name}] in policy [{zone}]")

        return status, response

    def _reset_policy_attr(self, zone, attr_name):
        """
        :param str zone:
        :param str attr_name:
        :rtype: tuple[str, SetAttrResponse]
        """
        data = {
            'ObjectDN': zone,
            'Class':  POLICY_ATTR_CLASS,
            'AttributeName': attr_name,
        }

        # TODO: Change _post() with post(args)
        status, response = self._post(URLS.POLICY_CLEAR_ATTRIBUTE, data=data)
        if status != HTTPStatus.OK:
            raise ServerUnexptedBehavior(f"Got status {status} from server")

        response = self._parse_attr_response(response)

        if response.error:
            raise VenafiError(f"Error while setting attribute [{attr_name}] in policy [{zone}]")

        return status, response

    def _reset_policy(self, zone):
        atrr_list = [SPA.TPP_DOMAIN_SUFFIX_WHITELIST, SPA.TPP_PROHIBIT_WILDCARD, SPA.TPP_CERT_AUTHORITY,
                     SPA.TPP_ORGANIZATION, SPA.TPP_ORG_UNIT, SPA.TPP_CITY, SPA.TPP_STATE, SPA.TPP_COUNTRY,
                     SPA.TPP_KEY_ALGORITHM, SPA.TPP_KEY_BIT_STR, SPA.TPP_ELLIPTIC_CURVE, SPA.TPP_MANUAL_CSR,
                     SPA.TPP_PROHIBITED_SAN_TYPES, SPA.TPP_ALLOWED_PRIVATE_KEY_REUSE, SPA.TPP_WANT_RENEWAL,
                     SPA.TPP_MANAGEMENT_TYPE]
        for attr in atrr_list:
            self._reset_policy_attr(zone, attr)

    @staticmethod
    def _parse_attr_response(response):
        """
        :param dict response:
        :rtype: SetAttrResponse
        """
        if not response:
            raise VenafiError('Response is empty')

        err = response['Error'] if 'Error' in response else None
        result = response['Result'] if 'Result' in response else None

        return SetAttrResponse(result, err)

    @staticmethod
    def _normalize_zone(zone):
        if zone is None:
            log.error("Zone argument is empty")
            raise ClientBadData
        if not re.match(r"^((\\[^\\<]+)|([^\\<]+))+$", zone):
            log.error(f"Bad zone format: {zone}")
            raise ClientBadData

        if zone.startswith("\\VED\\Policy"):
            return zone
        elif zone.startswith("VED\\Policy"):
            return f"\\{zone}"
        elif zone.startswith("\\"):
            return f"\\VED\\Policy{zone}"
        else:
            return f"\\VED\\Policy\\{zone}"

    @staticmethod
    def _get_policy_parent(zone):
        """
        :param str zone:
        """
        if zone is None:
            raise ClientBadData("Zone is empty")
        index = zone.rindex('\\')
        # Return a substring of zone that starts at 0 and ends at index. Zone here is treated as an slice
        # zone[0:index] returns the same result
        return zone[:index]

    def wrap_alt_names(self, request):
        """

        :param CertificateRequest request:
        :rtype: list[dict]
        """
        items = []
        for value in request.user_principal_names:
            items.append(self._create_san_item(0, value))
        for value in request.email_addresses:
            items.append(self._create_san_item(1, value))
        for value in request.san_dns:
            items.append(self._create_san_item(2, value))
        for value in request.uniform_resource_identifiers:
            items.append(self._create_san_item(6, value))
        for value in request.ip_addresses:
            items.append(self._create_san_item(7, value))
        return items

    @staticmethod
    def _create_san_item(san_type, value):
        """

        :param int san_type:
        :param str value:
        :return: dict
        """
        return {
            'Type': san_type,
            'Name': value
        }

    def search_by_thumbprint(self, thumbprint):
        """
        :param str thumbprint:
        :rtype: str
        """
        thumbprint = re.sub(r'[^\dabcdefABCDEF]', "", thumbprint)
        thumbprint = thumbprint.upper()
        # TODO: Change _post() with post(args)
        status, data = self._get(URLS.CERTIFICATE_SEARCH, params={"Thumbprint": thumbprint})
        if status != HTTPStatus.OK:
            raise ServerUnexptedBehavior

        if not data['Certificates']:
            raise ClientBadData("Certificate not found by thumbprint")
        return data['Certificates'][0]['DN']

    @staticmethod
    def _parse_zone_config_to_policy(data):
        # todo: parse over values to regexps (dont forget tests!)
        p = data['Policy']
        if p['KeyPair']['KeyAlgorithm']['Locked']:
            if p['KeyPair']['KeyAlgorithm']['Value'] == 'RSA':
                if p['KeyPair']['KeySize']['Locked']:
                    key_types = [KeyType(KeyType.RSA, p['KeyPair']['KeySize']['Value'])]
                else:
                    key_types = [KeyType(KeyType.RSA, x) for x in KeyType.ALLOWED_SIZES]
            elif p['KeyPair']['KeyAlgorithm']['Value'] == 'ECC':
                if p['KeyPair']['EllipticCurve']['Locked']:
                    key_types = [KeyType(KeyType.ECDSA, p['KeyPair']['EllipticCurve']['Value'])]
                else:
                    key_types = [KeyType(KeyType.ECDSA, x) for x in KeyType.ALLOWED_CURVES]
            else:
                raise ServerUnexptedBehavior
        else:
            key_types = []
            if p['KeyPair'].get('KeySize', {}).get('Locked'):
                key_types += [KeyType(KeyType.RSA, p['KeyPair']['KeySize']['Value'])]
            else:
                key_types += [KeyType(KeyType.RSA, x) for x in KeyType.ALLOWED_SIZES]
            if p['KeyPair'].get('EllipticCurve', {}).get('Locked'):
                key_types += [KeyType(KeyType.ECDSA, p['KeyPair']['EllipticCurve']['Value'])]
            else:
                key_types += [KeyType(KeyType.ECDSA, x) for x in KeyType.ALLOWED_CURVES]
        return Policy(key_types=key_types)

    @staticmethod
    def _parse_zone_data_to_object(data):
        s = data['Policy']['Subject']
        ou = s['OrganizationalUnit'].get('Values')
        policy = AbstractTPPConnection._parse_zone_config_to_policy(data)
        if data['Policy']['KeyPair']['KeyAlgorithm']['Value'] == 'RSA':
            key_type = KeyType(KeyType.RSA, data['Policy']['KeyPair']['KeySize']['Value'])
        elif data['Policy']['KeyPair']['KeyAlgorithm']['Value'] == 'ECC':
            key_type = KeyType(KeyType.ECDSA, data['Policy']['KeyPair']['EllipticCurve']['Value'])
        else:
            key_type = None

        z = ZoneConfig(
            organization=CertField(s['Organization']['Value'], locked=s['Organization']['Locked']),
            organizational_unit=CertField(ou, locked=s['OrganizationalUnit']['Locked']),
            country=CertField(s['Country']['Value'], locked=s['Country']['Locked']),
            province=CertField(s['State']['Value'], locked=s['State']['Locked']),
            locality=CertField(s['City']['Value'], locked=s['City']['Locked']),
            policy=policy,
            key_type=key_type,
        )
        return z

    def _get_certificate_details(self, cert_guid):
        # TODO: Change _get() with get(args)
        status, data = self._get(URLS.CERTIFICATE_SEARCH + cert_guid)
        if status != HTTPStatus.OK:
            raise ServerUnexptedBehavior("")
        return data

    def _is_valid_auth(self):
        raise NotImplementedError

    def _retrieve_ssh_ca_details(self, ca_request):
        """

        :param SSHCATemplateRequest ca_request:
        :rtype: SSHTPPCADetails
        """
        json_request = dict()
        if ca_request.template:
            value = ca_request.template
            if not value.startswith(PATH_SEPARATOR):
                value = f"{PATH_SEPARATOR}{value}"
            if not value.startswith(CA_ROOT_PATH):
                value = f"{CA_ROOT_PATH}{value}"
            json_request['DN'] = value
        elif ca_request.guid:
            json_request['Guid'] = ca_request.guid
        else:
            raise ClientBadData("CA Guid or CA template must be provided to retrieve SSH CA details.")

        args = {
            self.ARG_URL: URLS.SSH_CA_DETAILS,
            self.ARG_DATA: json_request
        }
        status, data = self.post(args=args)
        if status == HTTPStatus.OK:
            response_object = SSHResponse(data['Response'])
            if response_object.success:
                return SSHTPPCADetails(data)
            else:
                raise VenafiError(f"An error occurred with status {response_object.error_code}. "
                                  f"Message: {response_object.error_msg}")
        else:
            raise ServerUnexptedBehavior(f"Server returns {status} status on requesting SSH CA details")

    def _get_policy_attr(self, zone, attr_name):
        """
        :param str zone:
        :param str attr_name:
        :rtype: tuple[str, SetAttrResponse]
        """
        data = {
            'ObjectDN': zone,
            'Class': POLICY_ATTR_CLASS,
            'AttributeName': attr_name,
        }

        # TODO: Change _post() with post(args)
        status, response = self._post(URLS.FIND_POLICY, data=data)
        if status != HTTPStatus.OK:
            raise ServerUnexptedBehavior(f"Got status {status} from server")
        return status, response

    def retrieve_usernames_from_tpp_contacts(self, zone):
        users_list = list()
        contacts = list()
        try:
            status, response = self._get_policy_attr(zone, SPA.TPP_CONTACT)
            contacts = response['Values']
        except VenafiError as err:
            log.debug(f"Error while getting attribute [Contact] value from policy [{zone}]: {err.args[0]}")
        contacts = list(set(contacts))
        for prefixed_universal in contacts:
            try:
                identity = self.validate_identity(prefixed_universal)
                users_list.append(identity.name)
            except VenafiError as err:
                log.debug(f"Error while validating contact [{prefixed_universal}]: {err.args[0]}")
        return users_list

    def browse_identities(self, username):
        data = {
            'Filter': username,
            'Limit': 2,
            'IdentityType': self.ALL_IDENTITIES
        }
        try:
            status, response = self._post(URLS.POLICY_BROWSE_IDENTITY, data=data)
        except VenafiError as err:
            log.debug(f"Error while getting contact [{username}]: {err.args[0]}")

        if response['Identities'] > 1:
            raise VenafiError("Extraneous information returned in the identity response. "
                              "Expected size: 1, found: 2\n")
        identity = build_identity_entry(response)
        return identity

    def validate_identity(self, prefixed_universal):
        data = {
            'ID': {
                'PrefixedUniversal': prefixed_universal
            }
        }
        status, response = self._post(URLS.POLICY_VALIDATE_IDENTITY, data=data)
        identity = build_identity_entry(response['ID'])
        return identity
