import json
import logging
import time
from datetime import (
    UTC,
    datetime,
    timedelta,
)

import boto3
from botocore.exceptions import ClientError

from edpconfigprovider import ConfigProvider
from edpconfigprovider.exceptions import NoConfigurationException

logger = logging.getLogger(__name__)

_DEFAULT_SESSION_RETRY_ATTEMPTS = 5
_DEFAULT_SESSION_RETRY_BACKOFF_SECONDS = 0.2


class AwsAppConfigProvider(ConfigProvider):
    def __init__(
        self,
        application_name: str,
        profile_name: str,
        environment_name: str,
        session_retry_attempts: int = _DEFAULT_SESSION_RETRY_ATTEMPTS,
        session_retry_backoff_seconds: float = _DEFAULT_SESSION_RETRY_BACKOFF_SECONDS,
    ):
        self.application_name = application_name
        self.profile_name = profile_name
        self.environment_name = environment_name
        self._session_retry_attempts = max(1, session_retry_attempts)
        self._session_retry_backoff_seconds = session_retry_backoff_seconds

        self._client = boto3.client("appconfigdata")
        self._configuration_token = None
        self._next_poll_time = None
        self._last_config = None

    def get_configuration(self) -> dict:
        if self._next_poll_time is not None and datetime.now(tz=UTC) < self._next_poll_time:
            return self._last_config

        try:
            self._ensure_session()
            response = self._client.get_latest_configuration(ConfigurationToken=self._configuration_token)
            self._configuration_token = response["NextPollConfigurationToken"]
            self._next_poll_time = datetime.now(tz=UTC) + timedelta(seconds=response["NextPollIntervalInSeconds"])

            new_config = response["Configuration"].read()
            if new_config != b"":
                self._last_config = json.loads(new_config)
            elif self._last_config is None:
                config_name = self._get_config_name()
                logger.error("No configuration found for %s", config_name)
                raise NoConfigurationException(configuration_name=config_name)

            return self._last_config
        except ClientError:
            config_name = self._get_config_name()
            logger.exception("Error getting configuration from AWS AppConfig: %s", config_name)

            self._configuration_token = None
            raise NoConfigurationException(configuration_name=config_name)

    def _ensure_session(self):
        if self._configuration_token is not None:
            return

        # StartConfigurationSession resolves identifier names to IDs server-side and
        # the lookup is eventually consistent — it intermittently returns
        # ResourceNotFoundException for resources that exist. Retry transparently
        # before surfacing the error.
        for attempt in range(self._session_retry_attempts):
            try:
                session = self._client.start_configuration_session(
                    ApplicationIdentifier=self.application_name,
                    ConfigurationProfileIdentifier=self.profile_name,
                    EnvironmentIdentifier=self.environment_name,
                    RequiredMinimumPollIntervalInSeconds=15,
                )
                self._configuration_token = session["InitialConfigurationToken"]
                return
            except ClientError as exc:
                if exc.response.get("Error", {}).get("Code") != "ResourceNotFoundException":
                    raise
                if attempt == self._session_retry_attempts - 1:
                    raise
                backoff = self._session_retry_backoff_seconds * (2 ** attempt)
                logger.warning(
                    "AppConfig StartConfigurationSession returned ResourceNotFoundException for %s "
                    "(attempt %d/%d); retrying in %.2fs",
                    self._get_config_name(),
                    attempt + 1,
                    self._session_retry_attempts,
                    backoff,
                )
                time.sleep(backoff)

    def _get_config_name(self):
        return f"{self.application_name}/{self.environment_name}/{self.profile_name}"

    def cache_key(self) -> str:
        return f"{self.application_name}:{self.environment_name}:{self.profile_name}"
