from requests.exceptions import ConnectTimeout, ReadTimeout
from tenacity import retry, stop_after_attempt
from tenacity.wait import wait_exponential

from src.config.objects import LastFetchDatetime, TargetObject
from src.error.exceptions import SalesforceAPIException
from src.salesforce.salesforce_api import SalesforceApiClient
from src.salesforce.soql_builder import SOQLBuilder
from src.system_var.constants import FETCH_JP_NAME
from src.system_var.environments import (
    CRM_AUTH_TIMEOUT, CRM_FETCH_RECORD_MAX_RETRY_ATTEMPT,
    CRM_FETCH_RECORD_RETRY_INTERVAL, CRM_FETCH_RECORD_RETRY_MAX_INTERVAL,
    CRM_FETCH_RECORD_RETRY_MIN_INTERVAL, CRM_FETCH_RECORD_TIMEOUT,
    CRM_GET_RECORD_COUNT_MAX_RETRY_ATTEMPT,
    CRM_GET_RECORD_COUNT_RETRY_INTERVAL,
    CRM_GET_RECORD_COUNT_RETRY_MAX_INTERVAL,
    CRM_GET_RECORD_COUNT_RETRY_MIN_INTERVAL, CRM_GET_RECORD_COUNT_TIMEOUT)
from src.util.counter_object import CounterObject
from src.util.logger import logger_instance as logger


def fetch_crm_data_process(target_object: TargetObject, last_fetch_datetime: LastFetchDatetime):
    """CRMデータ取得処理

    Args:
        target_object (TargetObject): 取得対象オブジェクト情報インスタンス
        last_fetch_datetime (LastFetchDatetime): データ取得期間設定インスタンス

    Raises:
        SalesforceAPIException: SalesforceのAPI実行失敗が発生した場合

    Returns:
        crm_data_response: Salesforceオブジェクトデータ
    """

    # ① CRMデータ取得処理開始ログを出力する
    logger.info(
        f'I-FETCH-01 [{target_object.object_name}] のCRMからのデータ取得処理を開始します')

    target_object_name = target_object.object_name

    # リトライ回数判定用のカウンタオブジェクトを生成
    # @retryデコレータを利用した関数のリトライ処理で、基本データ型だとリトライ回数をカウントすることができないため、オブジェクト化する
    count_counter = CounterObject(1)
    data_counter = CounterObject(1)

    try:
        # ② 取得対象オブジェクトの取得期間内のレコード件数を取得する
        logger.info(f'I-FETCH-02 [{target_object_name}] の件数取得を開始します')

        soql_builder = SOQLBuilder(target_object, last_fetch_datetime)
        count_soql = soql_builder.create_count_soql()

        record_count = fetch_record_count_retry(count_soql, target_object_name, count_counter)

        logger.info(f'I-FETCH-03 [{target_object_name}] の件数：[{record_count}]')

    except Exception as e:
        raise SalesforceAPIException(
            'E-FETCH-01', FETCH_JP_NAME, f'[{target_object_name}] の件数取得に失敗しました エラー内容：[{e}]')

    try:
        # ③ 取得対象オブジェクトのレコードを取得する
        logger.info(f'I-FETCH-04 [{target_object_name}] のレコード取得を開始します')

        fetch_soql = soql_builder.create_fetch_soql()

        crm_data_response = fetch_sf_data_retry(fetch_soql, target_object_name, data_counter)

        logger.info(f'I-FETCH-05 [{target_object_name}] のレコード取得が成功しました')

    except Exception as e:
        raise SalesforceAPIException(
            'E-FETCH-02', FETCH_JP_NAME, f'[{target_object_name}] のレコード取得に失敗しました エラー内容：[{e}]')

    # ④ CRMデータ取得処理終了ログを出力する
    logger.info(f'I-FETCH-06 [{target_object_name}] のCRMからのデータ取得処理を終了します')

    # ⑤ 次の処理へ移行する
    return crm_data_response


@retry(
    wait=wait_exponential(multiplier=CRM_GET_RECORD_COUNT_RETRY_INTERVAL,
                          min=CRM_GET_RECORD_COUNT_RETRY_MIN_INTERVAL, max=CRM_GET_RECORD_COUNT_RETRY_MAX_INTERVAL),
    stop=stop_after_attempt(CRM_GET_RECORD_COUNT_MAX_RETRY_ATTEMPT))
def fetch_record_count_retry(soql: str, target_object_name: str, count_counter: CounterObject):
    try:
        salesforce_api_client = SalesforceApiClient()
        return salesforce_api_client.fetch_sf_count(soql)

    except ConnectTimeout as e:
        # 「リトライします」のメッセージ出力後、リトライせず例外終了になってしまうことを防ぐため、カウンタによる回数の判定を行う
        if count_counter.describe() < CRM_GET_RECORD_COUNT_MAX_RETRY_ATTEMPT:
            count_counter.increment(1)
            logger.warning(f'W-FETCH-01 CRMの接続処理がタイムアウトしため、リトライします：[{CRM_AUTH_TIMEOUT}] エラー内容：[{e}]')
        raise e

    except ReadTimeout as e:

        # 「リトライします」のメッセージ出力後、リトライせず例外終了になってしまうことを防ぐため、カウンタによる回数の判定を行う
        if count_counter.describe() < CRM_GET_RECORD_COUNT_MAX_RETRY_ATTEMPT:
            count_counter.increment(1)
            logger.warning(
                f'W-FETCH-02 [{target_object_name}] の件数取得処理がタイムアウトしたため、リトライします：[{CRM_GET_RECORD_COUNT_TIMEOUT}] エラー内容：[{e}]')
        raise e

    except Exception as e:
        # 「リトライします」のメッセージ出力後、リトライせず例外終了になってしまうことを防ぐため、カウンタによる回数の判定を行う
        if count_counter.describe() < CRM_GET_RECORD_COUNT_MAX_RETRY_ATTEMPT:
            count_counter.increment(1)
            logger.warning(
                f'W-FETCH-03 [{target_object_name}] の件数取得に失敗したため、リトライします  エラー内容：[{e}]')
        raise e


@retry(
    wait=wait_exponential(multiplier=CRM_FETCH_RECORD_RETRY_INTERVAL,
                          min=CRM_FETCH_RECORD_RETRY_MIN_INTERVAL, max=CRM_FETCH_RECORD_RETRY_MAX_INTERVAL),
    stop=stop_after_attempt(CRM_FETCH_RECORD_MAX_RETRY_ATTEMPT))
def fetch_sf_data_retry(soql: str, target_object_name: str, data_counter: CounterObject):
    try:
        salesforce_api_client = SalesforceApiClient()
        return salesforce_api_client.fetch_sf_data(soql)

    except ConnectTimeout as e:
        # 「リトライします」のメッセージ出力後、リトライせず例外終了になってしまうことを防ぐため、カウンタによる回数の判定を行う
        if data_counter.describe() < CRM_FETCH_RECORD_MAX_RETRY_ATTEMPT:
            data_counter.increment(1)
            logger.warning(f'W-FETCH-04 CRMの接続処理がタイムアウトしため、リトライします：[{CRM_AUTH_TIMEOUT}] エラー内容：[{e}]')
        raise e

    except ReadTimeout as e:
        # 「リトライします」のメッセージ出力後、リトライせず例外終了になってしまうことを防ぐため、カウンタによる回数の判定を行う
        if data_counter.describe() < CRM_FETCH_RECORD_MAX_RETRY_ATTEMPT:
            data_counter.increment(1)
            logger.warning(
                f'W-FETCH-05 [{target_object_name}] のレコード取得処理がタイムアウトしたため、リトライします：[{CRM_FETCH_RECORD_TIMEOUT}] エラー内容：[{e}]')
        raise e

    except Exception as e:
        # 「リトライします」のメッセージ出力後、リトライせず例外終了になってしまうことを防ぐため、カウンタによる回数の判定を行う
        if data_counter.describe() < CRM_FETCH_RECORD_MAX_RETRY_ATTEMPT:
            data_counter.increment(1)
            logger.warning(
                f'W-FETCH-06 [{target_object_name}] のレコード取得に失敗したため、リトライします  エラー内容：[{e}]')
        raise e
