class EcsCertificate(object):
    '''
    Entrust Certificate Services certificate class.
    '''
    def __init__(self, module):
        self.path = module.params['path']
        self.full_chain_path = module.params['full_chain_path']
        self.force = module.params['force']
        self.backup = module.params['backup']
        self.request_type = module.params['request_type']
        self.csr = module.params['csr']

        # All return values
        self.changed = False
        self.filename = None
        self.tracking_id = None
        self.cert_status = None
        self.serial_number = None
        self.cert_days = None
        self.cert_details = None
        self.backup_file = None
        self.backup_full_chain_file = None

        self.cert = None
        self.ecs_client = None
        if self.path and os.path.exists(self.path):
            try:
                self.cert = load_certificate(self.path, backend='cryptography')
            except Exception as dummy:
                self.cert = None
        # Instantiate the ECS client and then try a no-op connection to verify credentials are valid
        try:
            self.ecs_client = ECSClient(
                entrust_api_user=module.params['entrust_api_user'],
                entrust_api_key=module.params['entrust_api_key'],
                entrust_api_cert=module.params['entrust_api_client_cert_path'],
                entrust_api_cert_key=module.
                params['entrust_api_client_cert_key_path'],
                entrust_api_specification_path=module.
                params['entrust_api_specification_path'])
        except SessionConfigurationException as e:
            module.fail_json(
                msg='Failed to initialize Entrust Provider: {0}'.format(
                    to_native(e)))
        try:
            self.ecs_client.GetAppVersion()
        except RestOperationException as e:
            module.fail_json(
                msg=
                'Please verify credential information. Received exception when testing ECS connection: {0}'
                .format(to_native(e.message)))

    # Conversion of the fields that go into the 'tracking' parameter of the request object
    def convert_tracking_params(self, module):
        body = {}
        tracking = {}
        if module.params['requester_name']:
            tracking['requesterName'] = module.params['requester_name']
        if module.params['requester_email']:
            tracking['requesterEmail'] = module.params['requester_email']
        if module.params['requester_phone']:
            tracking['requesterPhone'] = module.params['requester_phone']
        if module.params['tracking_info']:
            tracking['trackingInfo'] = module.params['tracking_info']
        if module.params['custom_fields']:
            # Omit custom fields from submitted dict if not present, instead of submitting them with value of 'null'
            # The ECS API does technically accept null without error, but it complicates debugging user escalations and is unnecessary bandwidth.
            custom_fields = {}
            for k, v in module.params['custom_fields'].items():
                if v is not None:
                    custom_fields[k] = v
            tracking['customFields'] = custom_fields
        if module.params['additional_emails']:
            tracking['additionalEmails'] = module.params['additional_emails']
        body['tracking'] = tracking
        return body

    def convert_cert_subject_params(self, module):
        body = {}
        if module.params['subject_alt_name']:
            body['subjectAltName'] = module.params['subject_alt_name']
        if module.params['org']:
            body['org'] = module.params['org']
        if module.params['ou']:
            body['ou'] = module.params['ou']
        return body

    def convert_general_params(self, module):
        body = {}
        if module.params['eku']:
            body['eku'] = module.params['eku']
        if self.request_type == 'new':
            body['certType'] = module.params['cert_type']
        body['clientId'] = module.params['client_id']
        body.update(
            convert_module_param_to_json_bool(module, 'ctLog', 'ct_log'))
        body.update(
            convert_module_param_to_json_bool(
                module, 'endUserKeyStorageAgreement',
                'end_user_key_storage_agreement'))
        return body

    def convert_expiry_params(self, module):
        body = {}
        if module.params['cert_lifetime']:
            body['certLifetime'] = module.params['cert_lifetime']
        elif module.params['cert_expiry']:
            body['certExpiryDate'] = module.params['cert_expiry']
        # If neither cerTLifetime or certExpiryDate was specified and the request type is new, default to 365 days
        elif self.request_type != 'reissue':
            gmt_now = datetime.datetime.fromtimestamp(
                time.mktime(time.gmtime()))
            expiry = gmt_now + datetime.timedelta(days=365)
            body['certExpiryDate'] = expiry.strftime("%Y-%m-%dT%H:%M:%S.00Z")
        return body

    def set_tracking_id_by_serial_number(self, module):
        try:
            # Use serial_number to identify if certificate is an Entrust Certificate
            # with an associated tracking ID
            serial_number = "{0:X}".format(self.cert.serial_number)
            cert_results = self.ecs_client.GetCertificates(
                serialNumber=serial_number).get('certificates', {})
            if len(cert_results) == 1:
                self.tracking_id = cert_results[0].get('trackingId')
        except RestOperationException as dummy:
            # If we fail to find a cert by serial number, that's fine, we just don't set self.tracking_id
            return

    def set_cert_details(self, module):
        try:
            self.cert_details = self.ecs_client.GetCertificate(
                trackingId=self.tracking_id)
            self.cert_status = self.cert_details.get('status')
            self.serial_number = self.cert_details.get('serialNumber')
            self.cert_days = calculate_cert_days(
                self.cert_details.get('expiresAfter'))
        except RestOperationException as e:
            module.fail_json(
                'Failed to get details of certificate with tracking_id="{0}", Error: '
                .format(self.tracking_id), to_native(e.message))

    def check(self, module):
        if self.cert:
            # We will only treat a certificate as valid if it is found as a managed entrust cert.
            # We will only set updated tracking ID based on certificate in "path" if it is managed by entrust.
            self.set_tracking_id_by_serial_number(module)

            if module.params[
                    'tracking_id'] and self.tracking_id and module.params[
                        'tracking_id'] != self.tracking_id:
                module.warn(
                    'tracking_id parameter of "{0}" provided, but will be ignored. Valid certificate was present in path "{1}" with '
                    'tracking_id of "{2}".'.format(
                        module.params['tracking_id'], self.path,
                        self.tracking_id))

        # If we did not end up setting tracking_id based on existing cert, get from module params
        if not self.tracking_id:
            self.tracking_id = module.params['tracking_id']

        if not self.tracking_id:
            return False

        self.set_cert_details(module)

        if self.cert_status == 'EXPIRED' or self.cert_status == 'SUSPENDED' or self.cert_status == 'REVOKED':
            return False
        if self.cert_days < module.params['remaining_days']:
            return False

        return True

    def request_cert(self, module):
        if not self.check(module) or self.force:
            body = {}

            # Read the CSR contents
            if self.csr and os.path.exists(self.csr):
                with open(self.csr, 'r') as csr_file:
                    body['csr'] = csr_file.read()

            # Check if the path is already a cert
            # tracking_id may be set as a parameter or by get_cert_details if an entrust cert is in 'path'. If tracking ID is null
            # We will be performing a reissue operation.
            if self.request_type != 'new' and not self.tracking_id:
                module.warn(
                    'No existing Entrust certificate found in path={0} and no tracking_id was provided, setting request_type to "new" for this task'
                    'run. Future playbook runs that point to the pathination file in {1} will use request_type={2}'
                    .format(self.path, self.path, self.request_type))
                self.request_type = 'new'
            elif self.request_type == 'new' and self.tracking_id:
                module.warn(
                    'Existing certificate being acted upon, but request_type is "new", so will be a new certificate issuance rather than a'
                    'reissue or renew')
            # Use cases where request type is new and no existing certificate, or where request type is reissue/renew and a valid
            # existing certificate is found, do not need warnings.

            body.update(self.convert_tracking_params(module))
            body.update(self.convert_cert_subject_params(module))
            body.update(self.convert_general_params(module))
            body.update(self.convert_expiry_params(module))

            if not module.check_mode:
                try:
                    if self.request_type == 'validate_only':
                        body['validateOnly'] = 'true'
                        result = self.ecs_client.NewCertRequest(Body=body)
                    if self.request_type == 'new':
                        result = self.ecs_client.NewCertRequest(Body=body)
                    elif self.request_type == 'renew':
                        result = self.ecs_client.RenewCertRequest(
                            trackingId=self.tracking_id, Body=body)
                    elif self.request_type == 'reissue':
                        result = self.ecs_client.ReissueCertRequest(
                            trackingId=self.tracking_id, Body=body)
                    self.tracking_id = result.get('trackingId')
                    self.set_cert_details(module)
                except RestOperationException as e:
                    module.fail_json(
                        msg=
                        'Failed to request new certificate from Entrust (ECS) {0}'
                        .format(e.message))

                if self.request_type != 'validate_only':
                    if self.backup:
                        self.backup_file = module.backup_local(self.path)
                    write_file(
                        module,
                        to_bytes(self.cert_details.get('endEntityCert')))
                    if self.full_chain_path and self.cert_details.get(
                            'chainCerts'):
                        if self.backup:
                            self.backup_full_chain_file = module.backup_local(
                                self.full_chain_path)
                        chain_string = '\n'.join(
                            self.cert_details.get('chainCerts')) + '\n'
                        write_file(module,
                                   to_bytes(chain_string),
                                   path=self.full_chain_path)
                    self.changed = True
        # If there is no certificate present in path but a tracking ID was specified, save it to disk
        elif not os.path.exists(self.path) and self.tracking_id:
            if not module.check_mode:
                write_file(module,
                           to_bytes(self.cert_details.get('endEntityCert')))
                if self.full_chain_path and self.cert_details.get(
                        'chainCerts'):
                    chain_string = '\n'.join(
                        self.cert_details.get('chainCerts')) + '\n'
                    write_file(module,
                               to_bytes(chain_string),
                               path=self.full_chain_path)
            self.changed = True

    def dump(self):
        result = {
            'changed': self.changed,
            'filename': self.path,
            'tracking_id': self.tracking_id,
            'cert_status': self.cert_status,
            'serial_number': self.serial_number,
            'cert_days': self.cert_days,
            'cert_details': self.cert_details,
        }
        if self.backup_file:
            result['backup_file'] = self.backup_file
            result['backup_full_chain_file'] = self.backup_full_chain_file
        return result
class EntrustCertificateBackend(CertificateBackend):
    def __init__(self, module, backend):
        super(EntrustCertificateBackend, self).__init__(module, backend)
        self.trackingId = None
        self.notAfter = get_relative_time_option(
            module.params['entrust_not_after'],
            'entrust_not_after',
            backend=self.backend)

        if self.csr_content is None and self.csr_path is None:
            raise CertificateError(
                'csr_path or csr_content is required for entrust provider')
        if self.csr_content is None and not os.path.exists(self.csr_path):
            raise CertificateError(
                'The certificate signing request file {0} does not exist'.
                format(self.csr_path))

        self._ensure_csr_loaded()

        # ECS API defaults to using the validated organization tied to the account.
        # We want to always force behavior of trying to use the organization provided in the CSR.
        # To that end we need to parse out the organization from the CSR.
        self.csr_org = None
        if self.backend == 'pyopenssl':
            csr_subject = self.csr.get_subject()
            csr_subject_components = csr_subject.get_components()
            for k, v in csr_subject_components:
                if k.upper() == 'O':
                    # Entrust does not support multiple validated organizations in a single certificate
                    if self.csr_org is not None:
                        self.module.fail_json(msg=(
                            "Entrust provider does not currently support multiple validated organizations. Multiple organizations "
                            "found in Subject DN: '{0}'. ".format(csr_subject)
                        ))
                    else:
                        self.csr_org = v
        elif self.backend == 'cryptography':
            csr_subject_orgs = self.csr.subject.get_attributes_for_oid(
                NameOID.ORGANIZATION_NAME)
            if len(csr_subject_orgs) == 1:
                self.csr_org = csr_subject_orgs[0].value
            elif len(csr_subject_orgs) > 1:
                self.module.fail_json(msg=(
                    "Entrust provider does not currently support multiple validated organizations. Multiple organizations found in "
                    "Subject DN: '{0}'. ".format(self.csr.subject)))
        # If no organization in the CSR, explicitly tell ECS that it should be blank in issued cert, not defaulted to
        # organization tied to the account.
        if self.csr_org is None:
            self.csr_org = ''

        try:
            self.ecs_client = ECSClient(
                entrust_api_user=self.module.params['entrust_api_user'],
                entrust_api_key=self.module.params['entrust_api_key'],
                entrust_api_cert=self.module.
                params['entrust_api_client_cert_path'],
                entrust_api_cert_key=self.module.
                params['entrust_api_client_cert_key_path'],
                entrust_api_specification_path=self.module.
                params['entrust_api_specification_path'])
        except SessionConfigurationException as e:
            module.fail_json(
                msg='Failed to initialize Entrust Provider: {0}'.format(
                    to_native(e.message)))

    def generate_certificate(self):
        """(Re-)Generate certificate."""
        body = {}

        # Read the CSR that was generated for us
        if self.csr_content is not None:
            # csr_content contains bytes
            body['csr'] = to_native(self.csr_content)
        else:
            with open(self.csr_path, 'r') as csr_file:
                body['csr'] = csr_file.read()

        body['certType'] = self.module.params['entrust_cert_type']

        # Handle expiration (30 days if not specified)
        expiry = self.notAfter
        if not expiry:
            gmt_now = datetime.datetime.fromtimestamp(
                time.mktime(time.gmtime()))
            expiry = gmt_now + datetime.timedelta(days=365)

        expiry_iso3339 = expiry.strftime("%Y-%m-%dT%H:%M:%S.00Z")
        body['certExpiryDate'] = expiry_iso3339
        body['org'] = self.csr_org
        body['tracking'] = {
            'requesterName': self.module.params['entrust_requester_name'],
            'requesterEmail': self.module.params['entrust_requester_email'],
            'requesterPhone': self.module.params['entrust_requester_phone'],
        }

        try:
            result = self.ecs_client.NewCertRequest(Body=body)
            self.trackingId = result.get('trackingId')
        except RestOperationException as e:
            self.module.fail_json(
                msg=
                'Failed to request new certificate from Entrust Certificate Services (ECS): {0}'
                .format(to_native(e.message)))

        self.cert_bytes = to_bytes(result.get('endEntityCert'))
        self.cert = load_certificate(path=None,
                                     content=self.cert_bytes,
                                     backend=self.backend)

    def get_certificate_data(self):
        """Return bytes for self.cert."""
        return self.cert_bytes

    def needs_regeneration(self):
        parent_check = super(EntrustCertificateBackend,
                             self).needs_regeneration()

        try:
            cert_details = self._get_cert_details()
        except RestOperationException as e:
            self.module.fail_json(
                msg=
                'Failed to get status of existing certificate from Entrust Certificate Services (ECS): {0}.'
                .format(to_native(e.message)))

        # Always issue a new certificate if the certificate is expired, suspended or revoked
        status = cert_details.get('status', False)
        if status == 'EXPIRED' or status == 'SUSPENDED' or status == 'REVOKED':
            return True

        # If the requested cert type was specified and it is for a different certificate type than the initial certificate, a new one is needed
        if self.module.params['entrust_cert_type'] and cert_details.get(
                'certType') and self.module.params[
                    'entrust_cert_type'] != cert_details.get('certType'):
            return True

        return parent_check

    def _get_cert_details(self):
        cert_details = {}
        try:
            self._ensure_existing_certificate_loaded()
        except Exception as dummy:
            return
        if self.existing_certificate:
            serial_number = None
            expiry = None
            if self.backend == 'pyopenssl':
                serial_number = "{0:X}".format(
                    self.existing_certificate.get_serial_number())
                time_string = to_native(
                    self.existing_certificate.get_notAfter())
                expiry = datetime.datetime.strptime(time_string,
                                                    "%Y%m%d%H%M%SZ")
            elif self.backend == 'cryptography':
                serial_number = "{0:X}".format(
                    cryptography_serial_number_of_cert(
                        self.existing_certificate))
                expiry = self.existing_certificate.not_valid_after

            # get some information about the expiry of this certificate
            expiry_iso3339 = expiry.strftime("%Y-%m-%dT%H:%M:%S.00Z")
            cert_details['expiresAfter'] = expiry_iso3339

            # If a trackingId is not already defined (from the result of a generate)
            # use the serial number to identify the tracking Id
            if self.trackingId is None and serial_number is not None:
                cert_results = self.ecs_client.GetCertificates(
                    serialNumber=serial_number).get('certificates', {})

                # Finding 0 or more than 1 result is a very unlikely use case, it simply means we cannot perform additional checks
                # on the 'state' as returned by Entrust Certificate Services (ECS). The general certificate validity is
                # still checked as it is in the rest of the module.
                if len(cert_results) == 1:
                    self.trackingId = cert_results[0].get('trackingId')

        if self.trackingId is not None:
            cert_details.update(
                self.ecs_client.GetCertificate(trackingId=self.trackingId))

        return cert_details