import logging
import time
from abc import ABC
from typing import List

import requests

from acceldata_sdk.models.connection import ConnectionType, AnalyticsPipeline, Connection, ConnectionCheck, ConnectionCheckStatus
from acceldata_sdk.models.profile import Profile, ProfileRequest
from acceldata_sdk.models.rule import Rule, RuleCancelResponse, RuleExecution, ExecutionResult
from acceldata_sdk.models.tags import TAG, AssetTag, AssetLabel
from acceldata_sdk.errors import APIError, TorchSdkException
from acceldata_sdk.models.assetType import AssetType
from acceldata_sdk.models.datasource import DataSource,SourceType, CrawlerStatus
from acceldata_sdk.models.job import Job
from acceldata_sdk.models.pipeline import CreatePipeline, Pipeline, PipelineRun, PipelineInfo, PipelineDetails
from acceldata_sdk.models.span import Span, SpanContextEvent, CreateSpanEvent
from acceldata_sdk.client import TorchClientInterFace
from acceldata_sdk.models.asset import Asset, Metadata
from requests_toolbelt import MultipartEncoder
from acceldata_sdk.models.dqrule import DataQualityExecutionResult, DataQualityRule, DataQualityRuleResource
from acceldata_sdk.models.reconcillationrule import ReconcillationExecutionResult, ReconciliationRuleResource
from acceldata_sdk.models.ruleExecutionResult import RuleResult, PolicyFilter, ExecutionPeriod, RuleType, RuleExecutionSummary
from acceldata_sdk.constants import AssetSourceType, SdkSupportedVersions, TorchBuildVersion
from acceldata_sdk.common import Executor
from acceldata_sdk.constants import PolicyType, FailureStrategy, RuleExecutionStatus, PipelineBulkupdateRequest
from semantic_version import Version, SimpleSpec


from typing import List
from dataclasses import asdict, dataclass
import json
from datetime import datetime, timedelta

_HEADERS = {'User-Agent': 'Acceldata-sdk', 'accessKey': None, 'secretKey': None, 'Content-Type': 'application/json'}
catalog_api_path = "/catalog-server/api"
pipeline_api_path = "/torch-pipeline/api"
admin_api_path = "/admin/api"

import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)


def check_int(input_str):
    try:
        int(input_str)
        return True
    except ValueError:
        return False


class TorchHttpClient(TorchClientInterFace, ABC):
    access_key = None
    secret_key = None
    logger = logging.getLogger('torch')
    logger.setLevel(logging.INFO)

    # Create torch client by passing secret and access keys of catalog server for a given url
    def __init__(self, url, timeout_ms=10000, access_key: str = None, secret_key: str = None):
        """
            Description : Torch client is used to send data to catalog server.

            :param url: (String) url of the catalog server
                        ex:  https://torch.acceldata.local:5443
            :param timeout_ms: (Integer) timeout of the requests sending to catalog
            :param access_key: (String) Access key of API key. You can generate API key from torch ui's setting
            :param secret_key: (String) Secret key of API key.

            : TorchClient = TorchClient(url='https://torch.acceldata.local:5443', access_key='OY2VVIN2N6LJ', secret_key='da6bDBimQfXSMsyyhlPVJJfk7Zc2gs')
        """
        self.timeout_ms = timeout_ms
        self._timeout = 10000
        self._catalog_api_base = f"{url}{catalog_api_path}"
        self._pipeline_api_base = f"{url}{pipeline_api_path}"
        loc = url.rfind('/torch')
        if loc > 0:
            self._admin_api_base = f"{url[:loc]}{admin_api_path}"
        else:
            self._admin_api_base = f"{url}{admin_api_path}"

        if access_key is None and secret_key is None:
            raise Exception('Access key and secret key - required')
        self.access_key = access_key
        self.secret_key = secret_key
        _HEADERS['accessKey'] = access_key
        _HEADERS['secretKey'] = secret_key

    # # convert pipeline object to dict type
    def _convert_pipeline_to_dict(self, pipeline: CreatePipeline):
        """
        Description:
            Convert pipeline object to dict type.
        :param pipeline: createPipeline class instance
        :return: pipeline in dict type
        """
        pipeline_dict = pipeline.__dict__
        if pipeline.meta is not None:
            meta = asdict(pipeline.meta)
            pipeline_dict['meta'] = meta
        return pipeline_dict

    def get_supported_sdk_versions(self) -> SdkSupportedVersions:
        response = self._get(
            f'{self._catalog_api_base}/sdk/compatible-versions'
        )
        return SdkSupportedVersions(**response['versions'])

    def get_torch_version(self) -> TorchBuildVersion:
        response = self._get(
            f'{self._admin_api_base}/build-details'
        )
        return TorchBuildVersion(**response)

    # function to create pipeline
    def create_pipeline(self, pipeline: CreatePipeline) -> Pipeline:
        """
        Description:
            Used to create pipeline
        :param pipeline: createPipeline class instance that you want to create
        :return: pipeline class instance
        """
        payload = self._convert_pipeline_to_dict(pipeline)
        pipeline_payload = {'pipeline': payload}
        response = self._put(
            f'{self._pipeline_api_base}/pipelines',
            payload=pipeline_payload
        )
        response['pipeline']['client'] = self
        self.logger.info('Pipeline Created')
        return Pipeline(**response['pipeline'])


    def bulkupdate_pipeline(self, bulkUpdateRequest: PipelineBulkupdateRequest):
        """
        Description:
            Used to bulk update pipeline
        :param pipeline: createPipeline class instance that you want to create
        :return: pipeline class instance
        """
        payload = asdict(bulkUpdateRequest)
        response = self._post(
            f'{self._pipeline_api_base}/pipelines/bulkupdate',
            payload=payload, as_json=False
        )
        # response['pipeline']['client'] = self
        self.logger.info(f'Pipelines Created')
        return response

    # function to delete pipeline
    def delete_pipeline(self, id):
        """
        Description:
            Used to delete pipeline
        :param id: id of pipeline
        """

        response = self._delete(
            f'{self._pipeline_api_base}/pipelines/{id}'
        )
        self.logger.info(f'Pipeline {id} deleted')
        return response

    # get pipeline by uid or id
    def get_pipeline(self, pipeline_identity) -> Pipeline:
        """
            Description:
                To get an existing pipeline from torch catalog
        :param pipeline_identity: uid or id of the pipeline
        :return:(Pipeline) pipeline class instance
        """
        url = f'{self._pipeline_api_base}/pipelines/{pipeline_identity}'
        response = self._get(url)
        response['pipeline']['client'] = self
        return Pipeline(**response['pipeline'])

    # get all pipelines
    def get_pipelines(self) -> List[PipelineInfo]:
        """
            Description:
                To get all pipelines from torch catalog
        :return:(List[PipelineInfo]) PipelineInfo class instance
        """
        url = f'{self._pipeline_api_base}/pipelines'
        response = self._get(url)
        pipelines_infos = list()
        pipelines = response['pipelines']
        for obj in pipelines:
            obj['pipeline']['client'] = self
            pipelines_infos.append(PipelineInfo(pipeline=obj['pipeline'], runs=obj['runs']))

        return pipelines_infos

    # to create job for any given pipeline
    def create_job(self, job: {}, pipelineId: int) -> Job:
        """
        Description:
            Used to create job in a pipeline
        :param pipelineId: pipeline id of the respective pipeline
        :param job: createJob class instance that you want to add in pipeline
        :return: Job class instance of created job
        """
        response = self._put(
            f'{self._pipeline_api_base}/pipelines/{pipelineId}/jobs',
            payload=job
        )
        self.logger.info('Pipeline Job Created')
        return Job(**response['node'])

    def delete_datasource(self, datasource_id: int):
        """
        Description:
            used to delete datasource
        :param datasource_id: id of the datasource to be updated
        :return: response of the operation
        """
        response = self._delete(
            f'{self._catalog_api_base}/assemblies/{datasource_id}'
        )
        return response

    def start_crawler(self, datasource_name) -> CrawlerStatus:
        response = self._post(
            f'{self._catalog_api_base}/crawler/{datasource_name}',
            payload={}
        )
        return CrawlerStatus(**response['data'])

    def get_crawler_status(self, datasource_name) -> CrawlerStatus:
        response = self._get(
            f'{self._catalog_api_base}/crawler/{datasource_name}',
        )
        return CrawlerStatus(**response['data'])

    # create run for a pipeline
    def create_pipeline_run(self, pipeline_run: {}) -> PipelineRun:
        """
        Description:
            used to create a pipeline run
        :param pipeline_run:
        :return: pipelineRun class instance
        """
        pipelineId = pipeline_run['run']['pipelineId']
        response = self._post(
            f'{self._pipeline_api_base}/pipelines/{pipelineId}/runs',
            payload=pipeline_run
        )
        response['run']['client'] = self
        self.logger.info('Pipeline Run Created')
        return PipelineRun(**response['run'])

    # update run for a pipeline
    def update_pipeline_run(self, pipeline_run_id: int, pipeline_run: {}) -> PipelineRun:
        """
        Description:
            used to update an existing pipeline run
        :param pipeline_run_id: pipeline run id that you want to update
        :param pipeline_run: pipelineRun class instance that you want to update
        :return: updated pipelineRun class instance
        """
        response = self._put(
            f'{self._pipeline_api_base}/pipelines/runs/{pipeline_run_id}',
            payload=pipeline_run
        )
        response['run']['client'] = self
        self.logger.info('Pipeline Run Updated')
        return PipelineRun(**response['run'])

    # get latest pipeline run
    def get_latest_pipeline_run(self, pipeline_id: int) -> PipelineRun:
        """
            Description:
                To get latest pipeline run instance of any pipeline
        :param pipeline_id: id of the pipeline
        :return: PipelineRun instance
        """
        url = f'{self._pipeline_api_base}/pipelines/{pipeline_id}/latestRun'
        response = self._get(url)
        response['run']['client'] = self
        return PipelineRun(**response['run'])

    # get pipeline run with particular continuation id or run id
    def get_pipeline_run(self, pipeline_run_id: int = None, continuation_id: str = None, pipeline_id: str = None) -> PipelineRun:
        """
            Description:
                To get pipeline run with particular id or continuation id of any pipeline
        :param pipeline_run_id: run id of the pipeline run
        :param continuation_id: continuation id of the pipeline run
        :param pipeline_id: id of the pipeline. This is a mandatory parameter when run is being queried using continuation_id
        :return: PipelineRun instance
        """
        payload = dict()
        return_run = dict()
        if pipeline_run_id is not None:
            url = f'{self._pipeline_api_base}/pipelines/runs/{pipeline_run_id}'
            response = self._get(url)
            return_run = response['run']
        elif continuation_id is not None and pipeline_id is not None:
            supported_versions = self.get_supported_sdk_versions()
            torch_version: str = self.get_torch_version()
            if torch_version.buildVersion is not None and torch_version.buildVersion != "":
                actual_version = torch_version.buildVersion[:5]
            else:
                actual_version = supported_versions.maxVersion
            ver_comparator = SimpleSpec(f'<={actual_version}')
            if Version('2.5.0') in ver_comparator:
                url = f'{self._pipeline_api_base}/pipelines/{pipeline_id}/runs'
                payload = {"continuationId": continuation_id}
                response = self._get(url, params=payload)
                return_run = response["runs"][0]
            else:
                # For torch versions lower than 2.5.0 use old call for continuation_id
                if continuation_id.isdigit() is False:
                    url = f'{self._pipeline_api_base}/pipelines/runs/{continuation_id}'
                    response = self._get(url)
                    return_run = response['run']
                else:
                    raise TorchSdkException('Please provide continuation_id which is not a valid number.')
        else:
            raise TorchSdkException('Please provide either continuation_id and pipeline_id or pipeline_run_id')

        return_run['client'] = self
        return PipelineRun(**return_run)

    # get pipeline runs for a pipeline
    def get_pipeline_runs(self, pipeline_id: int) -> List[PipelineRun]:
        """
            Description:
                To get pipeline run  id of any pipeline
        :param pipeline_id: id of the pipeline
        :return: List[PipelineRun] instance
        """
        url = f'{self._pipeline_api_base}/pipelines/{pipeline_id}/runs'
        response = self._get(url)
        runs = list(response['runs'])
        runs_list = []
        for res in runs:
            res['client'] = self
            runs_list.append(PipelineRun(**res))
        return runs_list

    # create span for pipeline run
    def create_span(self, pipeline_run_id: int, span: dict) -> Span:
        """
        Description:
            used to create span for any pipeline run
        :param pipeline_run_id:
        :param span: Span class instance
        :return: Span
        """
        response = self._post(
            f'{self._pipeline_api_base}/pipelines/runs/{pipeline_run_id}/spans',
            payload=span
        )
        self.logger.info('Span Created')
        return Span(**response['span'])

    def get_child_spans(self, span_id: int):
        response = self._get(
            f'{self._pipeline_api_base}/pipelines/spans/{span_id}/childSpans'
        )
        self.logger.info('Fetched child spans.')
        child_spans = list(response['childSpans'])
        child_spans_res = []
        for res in child_spans:
            child_spans_res.append(Span(**res))
        return child_spans_res

    def get_span(self, pipeline_run_id: int, uid: str) -> Span:
        """
            Description:
                Get span of the pipeline run by uid
        :param pipeline_run_id: pipeline run id
        :param uid: uid of the span
        :return: SpanContext instance of the input span uid
        """
        url = f'{self._pipeline_api_base}/pipelines/runs/{pipeline_run_id}/spans/{uid}'
        response = self._get(url)
        response['span']['client'] = self
        return Span(**response['span'])

    def get_root_span(self, pipeline_run_id: int) -> Span:
        """
            Description:
                Get root span of the pipeline run
        :param pipeline_run_id: pipeline run id
        :return: SpanContext instance of the root span
        """
        url = f'{self._pipeline_api_base}/pipelines/runs/{pipeline_run_id}/spans'
        params = {"onlyRootSpan": "true"}
        response = self._get(url, params=params)
        print(f'response: {response}')
        return_value = dict()
        return_value['span'] = dict()
        return_value['span']['client'] = self
        if response['spans']:
            return_value['span'] = response['spans'][0]
            return_value['span']['client'] = self
            return Span(**return_value['span'])
        else:
            return None

    def get_spans(self, pipeline_run_id):
        response = self._get(
            f'{self._pipeline_api_base}/pipelines/runs/{pipeline_run_id}/spans'
        )
        self.logger.info('Fetched spans.')
        spans = list(response['spans'])
        spans_res = []
        for res in spans:
            spans_res.append(Span(**res))
        return spans_res

    # convert span object to dict type
    def convert_span_event_to_dict(self, span_event: CreateSpanEvent):
        """
            Description:
                Convert CreateSpanEvent class instance to dict type
            :param spanEvent: CreateSpanEvent class instance
            :return: dict form of CreateSpanEvent class instance
        """
        payload = span_event.__dict__
        # payload['eventUid'] = span_event.eventType
        event_payload = {'event': payload}
        return event_payload

    # create span event for any span
    def create_span_event(self, span_event: CreateSpanEvent) -> SpanContextEvent:
        """
        Description:
            used to create span event
        :param span_event: CreateSpanEvent class instance that you want to create
        :return: SpanContextEvent class instance
        """
        payload = self.convert_span_event_to_dict(span_event)
        if span_event.spanId is None:
            Exception('To update a pipeline run, id is required.')
        response = self._post(
            f'{self._pipeline_api_base}/pipelines/spans/{span_event.spanId}/events',
            payload=payload
        )
        self.logger.info('Span Event Created')
        return SpanContextEvent(**response['event'])

    def get_run_details(self, pipeline_id: int, version_id: int):
        """
            Description:
                Get details of the pipeline by versionid
        :param pipeline_id: pipeline  id
        :param version_id: version_id of the pipeline
        :return: SpanContext instance of the input span uid
        """
        url = f'{self._pipeline_api_base}/pipelines/{pipeline_id}/details?versionId={version_id}'
        response = self._get(url)
        details = response['details']
        details['pipeline']['client'] = self
        return PipelineDetails(**details)

    def get_all_asset_types(self):
        """
        Description:
            get all asset types supported in torch xatalog
        :return: list of asset types
        """
        response = self._get(
            f'{self._catalog_api_base}/asset-types'
        )
        asset_types = list(response['data'])
        asset_types_res = []
        for res in asset_types:
            asset_types_res.append(AssetType(**res))
        return asset_types_res

    def get_all_source_types(self):
        """
        Description:
            get all source types supported in torch catalog
        :return: list of all source type
        """
        response = self._get(
            f'{self._catalog_api_base}/source-types'
        )
        source_types = list(response['data'])
        source_types_res = []
        for res in source_types:
            source_types_res.append(SourceType(**res))
        return source_types_res

    def get_connection_types(self):
        """
        Description:
            get all source types supported in torch catalog
        :return: list of all source type
        """
        response = self._get(
            f'{self._catalog_api_base}/connection-types'
        )
        connection_types = list(response['data'])
        conn_type_response = []
        for res in connection_types:
            conn_type_response.append(ConnectionType(**res))
        return conn_type_response

    def get_tags(self):
        response = self._get(
            f'{self._catalog_api_base}/assets/tags'
        )
        tags = list(response['tags'])
        tags_list = []
        for res in tags:
            tags_list.append(TAG(**res))
        return tags_list

    def get_datasource(self, assembly_identifier, properties: bool = False) -> DataSource:
        """
        Description:
            Find datasource by its name or id in torch catalog
        :param assembly_identifier: name or id of the datasource given in torch
        :param properties: optional parameter, bool, to get datasource properties as well
        :return: (DataSource) datasource
        """
        url = f'{self._catalog_api_base}/assemblies/{assembly_identifier}?properties={properties}'

        response = self._get(url)
        datasource = response['data']
        if len(datasource) > 0:
            datasource['client'] = self
            return DataSource(**datasource)
        raise Exception('Datasource not found.')

    def get_datasources(self, type: AssetSourceType = None) -> List[DataSource]:
        """
        Description:
            Find datasources by its type in torch catalog
        :param type: type of the datasource given in torch, optional
        :return: list(DataSource) datasource
        """
        url = f'{self._catalog_api_base}/assemblies'
        if type is not None:
            params = dict()
            params['sourceType'] = type.name
            response = self._get(url, params=params)
        else:
            response = self._get(url)
        datasources = list(response['data'])
        ds_list = []
        for res in datasources:
            res['client'] = self
            ds_list.append(DataSource(**res))
        return ds_list
        raise Exception('Datasource not found.')

    def get_all_datasources(self):
        """
        Description:
            list all datasources in torch catalog
        :return: (DataSource) list of datasource
        """
        url = f'{self._catalog_api_base}/assemblies'
        response = self._get(url)
        datasources = list(response['data'])
        ds_list = []
        for res in datasources:
            ds_list.append(DataSource(**res))
        return ds_list

    def get_asset(self, identifier):
        """"
            Description:
                Find an asset of the datasource
            :param identifier: uid or ID of the asset
        """
        if check_int(identifier):
            return self.get_asset_by_id(id=identifier)
        else:
            return self.get_asset_by_uid(uid=identifier)

    def get_asset_by_id(self, id: int) -> Asset:
        """
        Description:
            used to find an asset
        :param id: id of an asset
        :return: asset data (Asset)
        """
        if id is None:
            raise Exception('Asset id is required')
        url = f'{self._catalog_api_base}/assets?ids={id}'
        asset = self._get_asset(url)
        asset_res = list(asset['data'])[0]
        asset_res['client'] = self
        return Asset(**asset_res)

    def get_asset_by_uid(self, uid: str):
        """
        Description:
            used to find an asset
        :param uid: uid of an asset
        :return: asset data (Asset)
        """
        if uid is None:
            raise Exception('Asset uid is required')
        url = f'{self._catalog_api_base}/assets?uid={uid}'
        asset = self._get_asset(url)
        asset['data']['client'] = self
        return Asset(**asset['data'])

    def _get_asset(self, url: str):
        response = self._get(
            url=url
        )
        return response

    def get_asset_metadata(self, id: int):
        url = f'{self._catalog_api_base}/assets/{id}/metadata'
        response = self._get(
            url=url
        )
        metadata = Metadata(**response['data'])
        return metadata

    def add_custom_metadata(self, id: int, custom_metadata: dict):
        url = f'{self._catalog_api_base}/assets/{id}/metadata/custom'
        response = self._post(
            url=url,
            payload=custom_metadata
        )
        metadata = Metadata(**response['data'])
        return metadata

    def profile_asset(self, id: int, payload: dict) -> Profile:
        url = f'{self._catalog_api_base}/assets/{id}/profile'
        response = self._post(
            url=url,
            payload=payload
        )
        response['data']['client'] = self
        profile_response = Profile(**response['data'])
        return profile_response

    def sample_data(self, id: int):
        url = f'{self._catalog_api_base}/assets/{id}/sample'
        response = self._get(
            url=url
        )
        return response

    def get_profile_request_details(self, asset_id: int, req_id: int):
        url = f'{self._catalog_api_base}/assets/{asset_id}/profile/{req_id}'
        response = self._get(
            url=url
        )
        return response

    def get_latest_profile_status(self, id: int):
        url = f'{self._catalog_api_base}/assets/{id}/profile'
        response = self._get(
            url=url
        )
        response['data']['client'] = self
        profile_response = ProfileRequest(**response['data'])
        return profile_response

    def cancel_profile(self, profile_req_id: int):
        url = f'{self._catalog_api_base}/assets/profile/{profile_req_id}/cancel'
        response = self._put(
            url=url,
            payload={}
        )
        return response['result']

    def get_asset_tags(self, id: int):
        if id is None:
            raise Exception('Asset id is required')
        url = f'{self._catalog_api_base}/assets/{id}/tags'
        response = self._get(
            url=url
        )
        tags = list(response['assetTags'])
        tags_list = []
        for res in tags:
            tags_list.append(AssetTag(**res))
        return tags_list

    def add_asset_tag(self, id: int, payload: dict):
        if id is None:
            raise Exception('Asset id is required')
        url = f'{self._catalog_api_base}/assets/{id}/tag'
        response = self._post(
            url=url, payload=payload
        )
        return AssetTag(**response)

    def add_asset_labels(self, id: int, payload: dict):
        if id is None:
            raise Exception('Asset id is required')
        url = f'{self._catalog_api_base}/assets/{id}/labels'
        response = self._put(
            url=url, payload=payload
        )
        labels = list(response)
        labels_list = []
        for lbl in labels:
            labels_list.append(AssetLabel(**lbl))
        return labels_list

    def get_asset_labels(self, id: int):
        if id is None:
            raise Exception('Asset id is required')
        url = f'{self._catalog_api_base}/assets/{id}/labels'
        response = self._get(
            url=url
        )
        labels = list(response)
        labels_list = []
        for lbl in labels:
            labels_list.append(AssetLabel(**lbl))
        return labels_list

    def get_analysis_pipeline(self, id: int) -> AnalyticsPipeline:
        response = self._get(
            url=f'{self._catalog_api_base}/analytics-pipelines/{id}'
        )
        return AnalyticsPipeline(**response['pipeline'])

    def check_connection(self, create_connection):
        create_conn = create_connection.__dict__
        config_prop = []
        for cp in create_connection.properties:
            config_prop.append(asdict(cp))
        create_conn['properties'] = config_prop
        payload = {
            'connection': create_conn
        }
        url = f'{self._catalog_api_base}/connections/check'
        response = self._post_form_data(
            url=url,
            payload=payload
        )
        check_res = ConnectionCheck(**response['result'])
        if check_res.status == ConnectionCheckStatus.SUCCESS.value:
            return check_res
        if check_res.status == ConnectionCheckStatus.FAILURE.value:
            raise TorchSdkException(f'Error: {check_res.message}')
        return check_res

    def create_connection(self, create_connection) -> Connection:
        create_conn = create_connection.__dict__
        config_prop = []
        # for cp in create_connection.properties:
        #     config_prop.append(asdict(cp))
        # create_conn['properties'] = config_prop
        payload = {
            'connection': create_conn
        }
        url = f'{self._catalog_api_base}/connections'
        response = self._post_form_data(
            url=url,
            payload=payload
        )
        return Connection(**response['connection'])

    def enable_rule(self, rule_id) -> Rule:
        payload = {
            "enable": True
        }
        response = self._put(
            url=f'{self._catalog_api_base}/rules/{rule_id}/enable',
            payload=payload
        )
        return Rule(**response['rule'])

    def disable_rule(self, rule_id) -> Rule:
        payload = {
            "enable": False
        }
        response = self._put(
            url=f'{self._catalog_api_base}/rules/{rule_id}/enable',
            payload=payload
        )
        return Rule(**response['rule'])

    def cancel_rule_execution(self, execution_id) -> RuleCancelResponse:
        response = self._put(
            url=f'{self._catalog_api_base}/rules/execution/{execution_id}/cancel',
            payload={}
        )
        return RuleCancelResponse(**response['result'])

    def execute_rule(self, rule_type: PolicyType, rule_id, sync=True, incremental=False,
                     failure_strategy: FailureStrategy = FailureStrategy.DoNotFail, pipeline_run_id=None):
        """
        Description:
            To execute rule synchronously and asynchronously
        :param rule_type: (PolicyType) Type of rule to be executed
        :param rule_id: (String) id of the rule to be executed
        :param sync: (bool) optional Set it to False if asynchronous execution has to be done
        :param incremental: (bool) optional Set it to True if full execution has to be done
        :param failure_strategy: (enum) optional Set it to decide if it should fail at error,
            fail at warning or never fail
        """
        rule_executor = Executor(rule_type, self, sync=sync)
        rule_executor.execute(rule_id, incremental, failure_strategy=failure_strategy, pipeline_run_id=pipeline_run_id)
        return rule_executor

    def get_rule_status(self, rule_type, execution_id) -> RuleExecutionStatus:
        """
        Description:
            To get status of rule execution
        :param rule_type: (String) Type of rule to be executed
        :param execution_id: (String) ID of execution of the rule previously executed.
        """
        rule_executor = Executor(rule_type, self)
        return rule_executor.get_execution_status(execution_id)

    def get_rule_execution_result(self, rule_type, execution_id, failure_strategy: FailureStrategy = FailureStrategy.DoNotFail):
        """
        Description:
            To get result of rule execution
        :param rule_type: (String) Type of rule to be executed
        :param execution_id: (String) ID of execution of the rule previously executed.
        :param failure_strategy: (enum) optional Set it to decide if it should fail at error,
            fail at warning or never fail
        """
        rule_executor = Executor(rule_type, self)
        return rule_executor.get_execution_result(execution_id, failure_strategy=failure_strategy)

    def execute_dq_rule(self, rule_id, incremental=False, pipeline_run_id=None) -> RuleExecution:
        payload_temp = dict()
        payload_temp['executionType'] = "FULL"
        if incremental:
            payload_temp['executionType'] = "INCREMENTAL"
        if pipeline_run_id is not None:
            payload_temp['pipelineRunId'] = pipeline_run_id
        response = self._post(
            url=f'{self._catalog_api_base}/rules/data-quality/{rule_id}/executions',
            payload=payload_temp
            )
        return RuleExecution(**response['execution'])

    def execute_reconciliation_rule(self, rule_id, incremental=False, pipeline_run_id=None) -> RuleExecution:
        payload_temp = dict()
        payload_temp['executionType'] = "FULL"
        if incremental:
            payload_temp['executionType'] = "INCREMENTAL"
        if pipeline_run_id is not None:
            payload_temp['pipelineRunId'] = pipeline_run_id
        response = self._post(
            url=f'{self._catalog_api_base}/rules/reconciliation/{rule_id}/executions',
            payload=payload_temp
        )
        return RuleExecution(**response['execution'])

    def get_dq_rule(self, rule_id) -> DataQualityRuleResource:
        response = self._get(
            url=f'{self._catalog_api_base}/rules/data-quality/{rule_id}'
            )
        response['client'] = self
        return DataQualityRuleResource(**response)

    def get_reconciliation_rule(self, rule_id) -> ReconciliationRuleResource:
        response = self._get(
            url=f'{self._catalog_api_base}/rules/reconciliation/{rule_id}'
        )
        response['client'] = self
        return ReconciliationRuleResource(**response)

    def get_dq_rule_execution_details(self, execution_id) -> ExecutionResult:
        response = self._get(
            url=f'{self._catalog_api_base}/rules/data-quality/executions/{execution_id}'
        )
        return ExecutionResult(**response)

    def get_reconciliation_rule_execution_details(self, execution_id) -> ExecutionResult:
        response = self._get(
            url=f'{self._catalog_api_base}/rules/reconciliation/executions/{execution_id}'
        )
        return ExecutionResult(**response)

    def get_reconciliation_rule_result(self, execution_id) -> RuleResult:
        response = self._get(
            url=f'{self._catalog_api_base}/rules/reconciliation/executions/{execution_id}/result'
        )
        return ReconcillationExecutionResult(**response)

    def get_dq_rule_result(self, execution_id) -> RuleResult:
        response = self._get(
            url=f'{self._catalog_api_base}/rules/data-quality/executions/{execution_id}/result'
        )
        return DataQualityExecutionResult(**response)

    def policy_executions(self, identifier, rule_type: RuleType, page=0, size=25,
                          sortBy='finishedAt:DESC') -> List[RuleExecutionSummary]:
        params = dict()
        params['page'] = page
        params['size'] = size
        params['sortBy'] = sortBy
        rule_id = identifier
        if not check_int(identifier):
            if rule_type.value == RuleType.DATA_QUALITY.value:
                rule_res = self.get_dq_rule(identifier)
            elif rule_type.value == RuleType.RECONCILIATION.value:
                rule_res = self.get_reconciliation_rule(identifier)
            rule_id = rule_res.rule.id

        response = self._get(
            url=f'{self._catalog_api_base}/rules/{rule_type.value.lower()}/{rule_id}/executions', params=params
        )
        executions = list(response['executions'])
        rule_execution_list = []
        for res in executions:
            execution = res['execution']
            rule_execution_list.append(RuleExecutionSummary(**execution))
        return rule_execution_list

    def get_all_rules(self, filter: PolicyFilter, page=0, size=25, withLatestExecution=True,
                               sortBy='updatedAt:DESC') -> RuleResult:
        params = dict()
        params['page'] = page
        params['size'] = size
        params['withLatestExecution'] = withLatestExecution
        params['sortBy'] = sortBy
        if filter.policyType is not None:
            params['ruleType'] = filter.policyType.name
            if filter.policyType.name == RuleType.RECONCILIATION.name:
                params['ruleType'] = params['ruleType'] + ',EQUALITY'
        if filter.lastExecutionResult is not None:
            params['resultStatus'] = filter.lastExecutionResult.name
        if filter.enable is not None:
            params['enabled'] = filter.enable
        if filter.active is not None and filter.active is True:
            params['ruleStatus'] = 'ACTIVE'
        else:
            params['ruleStatus'] = 'ALL'
        if filter.assets is not None:
            params['assetIds'] = ",".join([str(i.id) for i in filter.assets])
        if filter.data_sources is not None:
            params['assemblyIds'] = ",".join([str(i.id) for i in filter.data_sources])
        if filter.period is not None:
            finished_before_time = datetime.now()
            if filter.period == ExecutionPeriod.Last15minutes:
                started_after_time = datetime.now() - timedelta(minutes = 15)
            elif filter.period == ExecutionPeriod.Last30minutes:
                started_after_time = datetime.now() - timedelta(minutes = 30)
            elif filter.period == ExecutionPeriod.Last1hour:
                started_after_time = datetime.now() - timedelta(hours = 1)
            elif filter.period == ExecutionPeriod.Last3hours:
                started_after_time = datetime.now() - timedelta(hours = 3)
            elif filter.period == ExecutionPeriod.Last6hours:
                started_after_time = datetime.now() - timedelta(hours = 6)
            elif filter.period == ExecutionPeriod.Last12hours:
                started_after_time = datetime.now() - timedelta(hours = 12)
            elif filter.period == ExecutionPeriod.Last24hours:
                started_after_time = datetime.now() - timedelta(hours = 24)
            elif filter.period == ExecutionPeriod.Today:
                started_after_time = datetime.now() - timedelta(hours = 24)
            elif filter.period == ExecutionPeriod.Yesterday:
                started_after_time = datetime.now() - timedelta(days = 2)
                finished_before_time = datetime.now() - timedelta(days = 1)
            elif filter.period == ExecutionPeriod.Last7days:
                started_after_time = datetime.now() - timedelta(days = 7)
            elif filter.period == ExecutionPeriod.Thismonth:
                started_after_time = datetime.now() - timedelta(days = 30)
            elif filter.period == ExecutionPeriod.Last1month:
                started_after_time = datetime.now() - timedelta(days = 30)
            elif filter.period == ExecutionPeriod.Last3month:
                started_after_time = datetime.now() - timedelta(days = 90)

            params['finishedBefore'] = int(round(finished_before_time.timestamp() * 1000))
            params['startedAfter'] = int(round(started_after_time.timestamp() * 1000))

        response = self._get(
            url=f'{self._catalog_api_base}/rules',
            params=params
        )
        rules = list(response['rules'])
        rule_list = []
        for res in rules:
            rule = res['rule']
            rule_list.append(DataQualityRule(**rule))
        return rule_list

    @staticmethod
    def now_ms():
        return int(round(time.time() * 1000))

    def _post(self, url, payload=None, as_json=True):
        now_ms = self.now_ms()
        if _HEADERS['accessKey'] is None:
            _HEADERS['accessKey'] = self.access_key
        if _HEADERS['secretKey'] is None:
            _HEADERS['secretKey'] = self.secret_key
        response = requests.post(
            url=url, headers=_HEADERS, json=payload, timeout=self._timeout, verify=False
        )
        self.logger.info(
            f" POST {url} "
            f"payload={json.dumps(payload)} "
            f"duration_ms={self.now_ms() - now_ms}"
        )

        return self._response(response, as_json)

    def _put(self, url, payload=None, as_json=True):
        now_ms = self.now_ms()
        if _HEADERS['accessKey'] is None:
            _HEADERS['accessKey'] = self.access_key
        if _HEADERS['secretKey'] is None:
            _HEADERS['secretKey'] = self.secret_key
        response = requests.put(
            url=url, headers=_HEADERS, json=payload, timeout=self._timeout, verify=False
        )
        self.logger.info(
            f" PUT {url} "
            f"payload={json.dumps(payload)} "
            f"duration_ms={self.now_ms() - now_ms}"
        )

        return self._response(response, as_json)

    def _get(self, url, params=None, as_json=True):
        now_ms = self.now_ms()
        if _HEADERS['accessKey'] is None:
            _HEADERS['accessKey'] = self.access_key
        if _HEADERS['secretKey'] is None:
            _HEADERS['secretKey'] = self.secret_key
        response = requests.get(
            url=url, params=params, headers=_HEADERS, timeout=self._timeout, verify=False
        )
        self.logger.info(
            f" GET {url} "
            f"duration_ms={self.now_ms() - now_ms}"
        )

        return self._response(response, as_json)

    def _post_form_data(self, url, payload=None, as_json=True):
        now_ms = self.now_ms()
        if _HEADERS['accessKey'] is None:
            _HEADERS['accessKey'] = self.access_key
        if _HEADERS['secretKey'] is None:
            _HEADERS['secretKey'] = self.secret_key

        params = {
            'request': json.dumps(payload)
        }
        data = MultipartEncoder(fields=params)
        _HEADERS['Content-Type'] = data.content_type
        response = requests.post(
            url=url, headers=_HEADERS, data=data, timeout=self._timeout, verify=False
        )
        self.logger.info(
            f" POST {url} "
            f"payload={json.dumps(payload)} "
            f"duration_ms={self.now_ms() - now_ms}"
        )

        return self._response(response, as_json)

    def _put_form_data(self, url, payload=None, as_json=True):
        now_ms = self.now_ms()
        if _HEADERS['accessKey'] is None:
            _HEADERS['accessKey'] = self.access_key
        if _HEADERS['secretKey'] is None:
            _HEADERS['secretKey'] = self.secret_key

        params = {
            'request': json.dumps(payload)
        }
        data = MultipartEncoder(fields=params)
        _HEADERS['Content-Type'] = data.content_type
        response = requests.put(
            url=url, headers=_HEADERS, data=data, timeout=self._timeout, verify=False
        )
        self.logger.info(
            f" POSPUTT {url} "
            f"payload={json.dumps(payload)} "
            f"duration_ms={self.now_ms() - now_ms}"
        )

        return self._response(response, as_json)

    def _delete(self, url, params=None, as_json=True):
        now_ms = self.now_ms()
        if _HEADERS['accessKey'] is None:
            _HEADERS['accessKey'] = self.access_key
        if _HEADERS['secretKey'] is None:
            _HEADERS['secretKey'] = self.secret_key
        response = requests.delete(
            url=url, params=params, headers=_HEADERS, timeout=self._timeout, verify=False
        )
        self.logger.info(
            f" DELETE {url} "
            f"duration_ms={self.now_ms() - now_ms}"
        )

        return response

    def _response(self, response, as_json):
        try:
            response.raise_for_status()
        except requests.exceptions.HTTPError as e:
            self.logger.error(f' {response.text}')
            self._raise_api_error(e, response.text)

        return response.json() if as_json else response.text

    def _raise_api_error(self, e, text):
        raise APIError(text) from e
