from __future__ import annotations

import json
import logging
from datetime import datetime, timedelta, timezone

import aiofiles
from aiohttp import ClientError

from .api_async import ApiAsync
from .authenticator_common import (
    Authentication,
    AuthenticationState,
    AuthenticatorCommon,
    ValidationResult,
    from_authentication_json,
    to_authentication_json,
)
from .exceptions import AugustApiAIOHTTPError

_LOGGER = logging.getLogger(__name__)


class AuthenticatorAsync(AuthenticatorCommon):
    """Class to manage authentication with the August API."""

    _api: ApiAsync

    async def _read_access_token_file(
        self,
        access_token_cache_file: str,
        file: aiofiles.threadpool.binary.AsyncBufferedIOBase,
    ) -> None:
        contents = await file.read()
        self._authentication = from_authentication_json(json.loads(contents))

        # If token is to expire within 7 days then print a warning.
        if self._authentication.is_expired():
            _LOGGER.error("Token has expired.")
            self._authentication = Authentication(
                AuthenticationState.REQUIRES_AUTHENTICATION,
                install_id=self._install_id,
            )
        # If token is not expired but less then 7 days before it
        # will.
        elif (
            self._authentication.parsed_expiration_time() - datetime.now(timezone.utc)
        ) < timedelta(days=7):
            exp_time = self._authentication.access_token_expires
            _LOGGER.warning(
                "API Token is going to expire at %s "
                "hours. Deleting file %s will result "
                "in a new token being requested next"
                " time",
                exp_time,
                access_token_cache_file,
            )

    async def async_setup_authentication(self) -> None:
        if access_token_cache_file := self._access_token_cache_file:
            try:
                async with aiofiles.open(access_token_cache_file) as file:
                    await self._read_access_token_file(access_token_cache_file, file)
                    return
            except FileNotFoundError:
                _LOGGER.debug("Cache file not found: %s", access_token_cache_file)
            except json.decoder.JSONDecodeError as error:
                _LOGGER.error(
                    "Unable to read cache file (%s): %s",
                    access_token_cache_file,
                    error,
                )

        self._authentication = Authentication(
            AuthenticationState.REQUIRES_AUTHENTICATION, install_id=self._install_id
        )

    async def async_authenticate(self) -> Authentication:
        if self._api.brand_config.require_oauth:
            raise RuntimeError(f"OAuth is required for brand {self._api.brand}")

        if self._authentication.state is AuthenticationState.AUTHENTICATED:
            return self._authentication

        identifier = self._login_method + ":" + self._username
        install_id = self._authentication.install_id
        response = await self._api.async_get_session(
            install_id, identifier, self._password
        )

        json_dict = await response.json()
        authentication = self._authentication_from_session_response(
            install_id, response.headers, json_dict
        )

        if authentication.state is AuthenticationState.AUTHENTICATED:
            await self._async_cache_authentication(authentication)

        return authentication

    async def async_validate_verification_code(
        self, verification_code: str
    ) -> ValidationResult:
        if not verification_code:
            return ValidationResult.INVALID_VERIFICATION_CODE

        try:
            await self._api.async_validate_verification_code(
                self._authentication.access_token,
                self._login_method,
                self._username,
                verification_code,
            )
        except (AugustApiAIOHTTPError, ClientError):
            return ValidationResult.INVALID_VERIFICATION_CODE

        return ValidationResult.VALIDATED

    async def async_send_verification_code(self) -> bool:
        await self._api.async_send_verification_code(
            self._authentication.access_token, self._login_method, self._username
        )

        return True

    async def async_refresh_access_token(self, force=False) -> Authentication | None:
        if not self.should_refresh() and not force:
            return self._authentication

        if self._authentication.state is not AuthenticationState.AUTHENTICATED:
            _LOGGER.warning("Tried to refresh access token when not authenticated")
            return self._authentication

        refreshed_token = await self._api.async_refresh_access_token(
            self._authentication.access_token
        )

        authentication = self._process_refreshed_access_token(refreshed_token)
        await self._async_cache_authentication(authentication)
        return authentication

    async def _async_cache_authentication(self, authentication: Authentication) -> None:
        if self._access_token_cache_file is not None:
            async with aiofiles.open(self._access_token_cache_file, "w") as file:
                await file.write(to_authentication_json(authentication))
