import os
import backoff
from requests.exceptions import ConnectionError, SSLError  # type: ignore[import-untyped]
import google.auth
import google.auth.transport.requests
import requests  # type: ignore[import-untyped]
from requests.exceptions import ConnectTimeout, ReadTimeout  # type: ignore[import-untyped]
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.exceptions import RefreshError
import googleapiclient.discovery
from googleapiclient.http import BatchHttpRequest
from googleapiclient.errors import HttpError
from httplib2.error import ServerNotFoundError
import socket
from .dialog import Dialog
from .subdialogs import TableDialog,Applet
from .framework import beep
from .secrets_paths import get_google_credentials_path_with_fallback, get_google_token_path_with_fallback, get_google_token_path
from functools import wraps
from typing import Any, Callable
from collections.abc import Sequence
#def is_connected(host="8.8.8.8", port=53, timeout=1):
def is_connected(host: str = "www.google.com", port: int = 443, timeout: int = 1) -> bool:
    """Check if there is an active internet connection.

    Attempts to open a socket connection to a known web service. Uses Google's
    homepage by default as a reliable connectivity check endpoint.

    Args:
        host: The host to attempt connection to. Defaults to "www.google.com".
        port: The port to connect on. Defaults to 443 (HTTPS).
        timeout: Connection timeout in seconds. Defaults to 1.

    Returns:
        True if connection successful, False on any socket error.

    Note:
        Sets socket default timeout globally for the duration of the connection attempt.
    """
    try:
        # Try to open a socket connection to the specified host and port
        socket.setdefaulttimeout(timeout)
        socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect((host, port))
        return True
    except socket.error as e:
        #print(e)
        return False

def on_connection_timeout(details: dict[str, Any]) -> None:
    """Backoff callback for connection timeout retries.

    Args:
        details: Backoff details dict containing 'tries' count and other metadata.
    """
    print(f"Attempt {details['tries']}: Connection is timing out. Please wait...")

def on_read_timeout(details: dict[str, Any]) -> None:
    """Backoff callback for read timeout retries.

    Args:
        details: Backoff details dict containing 'tries' count and other metadata.
    """
    print(f"Attempt {details['tries']}: Read is timing out. Please wait...")

# Custom function to create a session with timeout handling and retries
def create_custom_session() -> requests.Session:
    """Create a requests Session with custom timeout and retry configuration.

    Configures HTTP adapter with:
    - 10 connection pool connections
    - 10 max pool size
    - 3 automatic retries
    - 5 second connection timeout
    - 10 second read timeout

    Returns:
        Configured requests.Session instance.

    Note:
        Timeout settings apply to all requests made through this session.
    """
    session = requests.Session()
    # Set up HTTP adapter with retry and pool management
    adapter = requests.adapters.HTTPAdapter(
        pool_connections=10,
        pool_maxsize=10,
        max_retries=3,  # Retry up to 3 times
    )
    session.mount('https://', adapter)
    # Set custom timeouts for connection and read
    session.request = lambda *args, **kwargs: requests.Session.request(
        session, *args, timeout=(5, 10), **kwargs  # 5 sec connection, 10 sec read timeout
    )
    return session
# Function to build the Google API service with a custom session
@backoff.on_exception(backoff.expo, (ConnectTimeout, ReadTimeout), max_tries=3, on_backoff=on_connection_timeout)  # type: ignore[arg-type]
def build_google_service() -> Any:
    """Build Google API service with custom session (DEPRECATED).

    Args:
        None.

    Returns:
        Google API service instance.

    Note:
        DEPRECATED - Contains undefined variables (build, credentials).
        Use create_service() instead. Kept for reference only.
    """
    # Create a custom session with timeout and retry configuration
    session = create_custom_session()
    # Create a custom transport request object using the custom session
    auth_req = google.auth.transport.requests.Request(session=session)
    # Build the Google API client service with the custom transport
    service = build('drive', 'v3', credentials=credentials, requestBuilder=auth_req)  # type: ignore[name-defined]
    return service
def online_only(method: Callable[..., Any]) -> Callable[..., Any]:
    """Decorator to enforce online connectivity for dialog methods.

    Checks internet connection and creates Google API service if needed before
    executing decorated method. Speaks error message if offline or service unavailable.

    Args:
        method: The dialog method to wrap with connectivity check.

    Returns:
        Wrapped method that enforces online requirement.

    Note:
        Expects decorated method to be on a Dialog or Element with home_dialog attribute.
        Returns None if offline or service unavailable instead of executing method.
    """
    @wraps(method)
    def wrapper(self: Any,  *args: Any, **kwargs: Any) -> Any:
        home_dialog = self.home_dialog if isinstance(self,Dialog) else self.dialog.home_dialog
        connected= is_connected()
        if connected and not home_dialog.service:  # type: ignore[union-attr]
            home_dialog.create_my_service()  # type: ignore[union-attr]
        if not (connected and home_dialog.service):  # type: ignore[union-attr]
            home_dialog.say_strings('command only available online')
            return None
        return method(self, *args, **kwargs)
    return wrapper

def create_service(api_name: str, api_version: str, creds: Credentials) -> Any:
    """Create a Google API service instance.

    Args:
        api_name: Google API name (e.g., 'gmail', 'calendar', 'people').
        api_version: API version string (e.g., 'v1', 'v3').
        creds: OAuth2 credentials for authentication.

    Returns:
        Google API service instance for making API calls.

    Note:
        Uses custom session for timeout handling but doesn't pass custom
        requestBuilder due to method signature conflicts in googleapiclient.
    """
    # Create a custom session with timeout and retry configuration
    session = create_custom_session()
    # Create a custom transport request object using the custom session
    auth_req = google.auth.transport.requests.Request(session=session)
    # would like to Build the Google API client service with the custom transport by adding      requestBuilder=auth_req to below call but causes barf when service is used claiming duplicate method arg to request builder
    return   googleapiclient.discovery.build(api_name, api_version, credentials=creds)

class HTTPErrorString(str):
    """Error string subclass for Google API HTTP errors.

    Used to distinguish error strings returned by request_with_retry from
    successful responses. Allows callers to use isinstance() check instead
    of type checking.
    """
    pass
def get_creds(scope_names: Sequence[str] = ['contacts.other.readonly','gmail.modify','calendar']) -> Credentials | HTTPErrorString:  #,'contacts']):
    """Get or create Google API OAuth2 credentials.

    Loads credentials from winston_google_token.json if available, refreshes
    expired credentials, or initiates OAuth flow for new credentials.

    Args:
        scope_names: List of Google API scope suffixes (e.g., 'gmail.modify').
            Full scope URLs are constructed by prepending 'https://www.googleapis.com/auth/'.
            Defaults to contacts, gmail, and calendar scopes.

    Returns:
        Valid Credentials object, or HTTPErrorString on connection/auth failure.

    Note:
        All Winston applets share winston_google_credentials.json (client secret)
        and winston_google_token.json (user tokens). During OAuth flow, Google
        will show "Winston" as the app name for all applets.
    """
    if not is_connected():
        return HTTPErrorString('no internet connection')
    SCOPES = [f'https://www.googleapis.com/auth/{name}' for name in scope_names]
    creds = None
    # The file google_token.json stores the user's access and refresh tokens, and is created automatically when the authorization flow completes for the first time.
    token_path = get_google_token_path_with_fallback()
    if os.path.exists(token_path):
        creds = Credentials.from_authorized_user_file(token_path, SCOPES)
    # If there are no (valid) credentials available, let the user log in.
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            try:
                creds.refresh(Request())
            except RefreshError as e:
                #print(f'refresh error, so re-authenticating')
                creds = None
            except Exception as e:
                print('refresh google credentials failed, may be offline')
                return HTTPErrorString(e)
    if not creds:
        creds_path = get_google_credentials_path_with_fallback()
        flow = InstalledAppFlow.from_client_secrets_file(creds_path, SCOPES)
        creds = flow.run_local_server(port=0)
        # Save the credentials for the next run (always to new location)
        with open(get_google_token_path(), 'w') as token:
            token.write(creds.to_json())
    #print(f'get_creds returning {type(creds)} creds object')
    return creds
def is_transient_error(exc: Exception) -> bool:
    """Check if exception is a transient HTTP 5xx error.

    Args:
        exc: Exception to check.

    Returns:
        True if exc is HttpError with 500-599 status code, False otherwise.

    Note:
        Used by execute_with_retry to determine if request should be retried.
    """
    return isinstance(exc, HttpError) and 500 <= exc.resp.status < 600

def request_with_retry(request_maker_func: Callable[..., Any], service: Any, **kwargs: Any) -> Any:
    """Build and execute a Google API request with retry logic.

    Args:
        request_maker_func: Applet-specific function that builds a Google API request.
            Should accept service as first arg and return a request object.
        service: Google API service instance (from create_service).
        **kwargs: Additional arguments passed to request_maker_func.

    Returns:
        API response on success, or HTTPErrorString on failure.

    Note:
        Wraps execute_with_retry to provide common pattern for building then executing requests.
    """
    request = request_maker_func(service, **kwargs)
    return execute_with_retry(request)
@backoff.on_exception(backoff.expo,(ConnectionError, SSLError, HttpError),max_time=15,giveup=lambda e: not is_transient_error(e))
def execute_with_retry(request: Any) -> Any:
    """Execute Google API request with exponential backoff retry.

    Retries transient errors (HTTP 5xx, ConnectionError, SSLError) for up to 15 seconds
    using exponential backoff. Non-transient errors (HTTP 4xx) are not retried.

    Args:
        request: Google API request object (from service.method().build()).

    Returns:
        API response dict on success, or HTTPErrorString on failure.

    Note:
        Plays beep sounds during retries:
        - 350Hz, 500ms: HTTP 5xx retry
        - 350Hz, 1500ms: Connection/SSL retry
    """
    if not is_connected():
        return HTTPErrorString('no internet connection')
    try:
        response = request.execute()
        return response
    except HttpError as e:
        #print(f'In execute_with-retry: {e.__class__.__name__}: {e}')
        error_code = e.resp.status
        if error_code< 500:
            return HTTPErrorString(f"request non-retried failure code {error_code}: {e}")
        else:
            beep(350,500) #indicate retry in progress
            raise
    except (ConnectionError, SSLError) as e:
        beep(350,1500) #indicate retry in progress
        print(f'connection or SSL error type: {e.__class__.__name__}, error value {e}')
        raise
    except ServerNotFoundError as e:
        return HTTPErrorString(f"server not found, may be offline:  {e}")
    except Exception as e:
        return HTTPErrorString(f"Non-retried error: {e.__class__.__name__}: {e}")

def connection_error(http_response: Any) -> bool:
    """Check if response is a connection error and print if so.

    Args:
        http_response: Response from execute_with_retry or request_with_retry.

    Returns:
        True if response is HTTPErrorString (error occurred), False otherwise.

    Note:
        Prints error message as side effect when error detected.
    """
    if isinstance(http_response,HTTPErrorString):
        print(http_response)
        return True
    return False
class GoogleDialog(Applet,TableDialog):  # type: ignore[misc]
    """Base dialog class for Google API-integrated applets.

    Provides common functionality for dialogs that interact with Google APIs:
    - Automatic service creation with credential management
    - Connection status checking
    - Offline mode handling

    Subclasses must implement:
    - get_api_name_and_version(): Return API name and version tuple
    - preliminaries(): Perform pre-initialization setup

    Attributes:
        service: Google API service instance (or None if offline/unavailable).
    """

    def __init__(self, parent: Dialog, name_seed: str, *args: Any, **kwargs: Any) -> None:
        """Initialize GoogleDialog with service creation.

        Args:
            parent: Parent dialog.
            name_seed: Base name for this dialog instance.
            *args: Additional positional arguments for parent classes.
            **kwargs: Additional keyword arguments for parent classes.

        Note:
            Sets self.app early so say_strings() can access TTS before
            super().__init__() completes. Creates service and calls
            preliminaries() before parent initialization.
        """
        self.app = parent.app #early fix up of app attribute so that say_strings can access tts
        #self.breakpoint()
        #print('creating service in google.py')
        self.service: Any = self.create_my_service()
        self.preliminaries()
        super().__init__(parent,name_seed,*args,**kwargs)

    def __str__(self) -> str:
        """Return dialog name with offline indicator if disconnected.

        Returns:
            Dialog name string with ' offline' suffix if no internet connection.
        """
        return super().__str__() + ('' if is_connected() else ' offline')

    def get_api_name_and_version(self) -> tuple[str, str]:
        """Get Google API name and version for this dialog.

        Returns:
            Tuple of (api_name, api_version) strings (e.g., ('gmail', 'v1')).

        Raises:
            NotImplementedError: Must be overridden by subclass.
        """
        raise NotImplementedError

    def preliminaries(self) -> None:
        """Perform pre-initialization setup.

        Called after service creation but before parent __init__().
        Override to load data, set attributes, etc.

        Raises:
            NotImplementedError: Must be overridden by subclass.
        """
        raise NotImplementedError

    def create_my_service(self) -> Any | None:
        """Create Google API service instance with credential handling.

        Loads or creates credentials via get_creds(), then builds service
        using create_service(). Stores credentials in self.app.google_creds
        for reuse across applets.

        Returns:
            Google API service instance, or None if credentials/connection unavailable.

        Note:
            Credentials are shared across all Google API applets (diary, mail, people).
        """
        assert self.app is not None
        if self.app.google_creds is None:
            #self.say_strings('no credentials available')
            creds = get_creds()
            if isinstance(creds,HTTPErrorString):
                print(creds)
                service =  None
            else:
                self.app.google_creds =creds
        if self.app.google_creds  is None:
            service = None
        else:
            api_name,api_version = self.get_api_name_and_version()
            service = create_service(api_name,api_version,self.app.google_creds)
        return service

    def connection_error(self, http_response: Any) -> bool:
        """Check if API response is an error and print if so.

        Args:
            http_response: Response from Google API call.

        Returns:
            True if error occurred, False otherwise.

        Note:
            Delegates to module-level connection_error() function.
        """
        return connection_error(http_response)

    def google_api_refresh(self, filler: Any) -> None:
        """Refresh dialog content from Google API.

        Args:
            filler: Content specification passed to refill().

        Note:
            Checks connection before refreshing. Currently incomplete -
            needs implementation of "try to get online" logic.
        """
        response=None
        if not is_connected():
            self.say_strings('implement try and get online in google_api.google_api_refresh')
            return
        if not self.connection_error(response):
            self.refill(filler)

    def empty_message(self) -> str:
        """Return message explaining why dialog has no content.

        Returns:
            Message string with reason (offline, no credentials, no service, or empty).

        Note:
            Called by Dialog.generate_valid_content() when content generator yields nothing.
        """
        assert self.app is not None
        if not is_connected():
            reason = 'offline'
        elif not self.app.google_creds:
            reason = 'no api credentials available - may be offline'
        elif self.service is None:
            reason = 'no service, may be offline'
        else:
            reason = ''
        return f'No items to display: {reason}'            