from dataclasses import dataclass
from datetime import datetime
from typing import TypedDict, NotRequired, Optional

import google.auth
from google.api_core.client_options import ClientOptions, ClientOptions as GCPClientOptions
from google.auth.credentials import Credentials, CredentialsWithQuotaProject, TokenState
from google.oauth2 import service_account


class OptionsInterface:
    credentials: CredentialsWithQuotaProject
    """
    raw secret not cached in credentials object. You need cache it by yourself.
    """
    projectId: str
    client_options: ClientOptions

    @property
    def token(self) -> str:
        """
        :return The bearer token that can be used in HTTP headers to make authenticated requests.
        """
        if self.credentials.token_state != TokenState.FRESH:
            from google.auth.transport.requests import Request
            self.credentials.refresh(Request())
        return self.credentials.token

    @property
    def expiry(self) -> datetime:
        return self.credentials.expiry


default_scopes = ['googleapis.com/auth/cloud-platform']
"""
[Oauth 2.0 Scopes](https://developers.google.com/identity/protocols/oauth2/scopes)
"""


class ADC(OptionsInterface):
    credentials: Credentials


def default(scopes=None) -> ADC:
    c = ADC()
    c.credentials, c.projectId = google.auth.default(
        scopes=scopes,  # used to get Bearer Token
        default_scopes=default_scopes,
    )
    return c


class ServiceAccount(OptionsInterface):
    credentials: service_account.Credentials

    @dataclass
    class Info(TypedDict):
        """
        The service account info in Google format.
        """
        client_email: str
        private_key: str
        token_uri: NotRequired[str]
        project_id: NotRequired[str]
        client_id: Optional[str]  # for Apache Spark pubsub
        private_key_id: Optional[str]  # for Apache Spark pubsub

    @staticmethod
    def assign(info: Info, project_id=None) -> Info:
        if project_id:
            info['project_id'] = project_id
        if not info.get('project_id'):
            info['project_id'] = info.get('client_email').split('@')[1].split('.')[0]
        info['token_uri'] = "https://oauth2.googleapis.com/token"
        return info


def from_service_account(info: ServiceAccount.Info = None, *,
                         client_email=None, private_key=None, project_id=None, scopes=None
                         ) -> ServiceAccount:
    if scopes is None:
        scopes = default_scopes
    scopes = list(map(lambda scope: 'https://www.' + scope, scopes))
    if not info:
        info = ServiceAccount.Info(
            client_email=client_email,
            private_key=private_key,
            client_id=None,
            private_key_id=None,
        )
    info = ServiceAccount.assign(info, project_id)

    c = ServiceAccount()

    c.credentials = service_account.Credentials.from_service_account_info(
        info, scopes=scopes
    )
    c.projectId = info['project_id']
    return c


def from_api_key(api_key: str, client_options: GCPClientOptions = None) -> GCPClientOptions:
    if client_options is None:
        client_options = GCPClientOptions()
    client_options.api_key = api_key
    return client_options
