"""
Google API authentication and client helpers for g-gremlin.
"""

from __future__ import annotations

import json
import os
import time
from pathlib import Path
from typing import Any, Optional

from google.auth.exceptions import RefreshError
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import Resource, build
from googleapiclient.errors import HttpError
from googleapiclient.http import MediaFileUpload

from g_gremlin.errors import (
    AuthenticationError,
    ci_credentials_missing_error,
    credentials_not_found_error,
    token_expired_error,
)
from g_gremlin.paths import gremlin_path, resolve_gremlin_home

# Scopes include write access for push plus read access for metadata.
SCOPES: list[str] = [
    "https://www.googleapis.com/auth/spreadsheets",
    "https://www.googleapis.com/auth/drive.readonly",
    "https://www.googleapis.com/auth/script.projects",
    "https://www.googleapis.com/auth/script.scriptapp",
    "https://www.googleapis.com/auth/script.deployments",  # List/manage deployments
    "https://www.googleapis.com/auth/script.external_request",  # Execute scripts via API
    "https://www.googleapis.com/auth/documents",  # Google Docs API access
    "https://www.googleapis.com/auth/presentations",  # Google Slides API access
    "https://www.googleapis.com/auth/userinfo.email",  # Identify which account is authed
    "openid",  # Required by Google when userinfo.email is requested
]

# ---------------------------------------------------------------------------
# Multi-account profile support
# ---------------------------------------------------------------------------
_active_profile: str | None = None
GOOGLE_PROFILES_PATH = gremlin_path("google_profiles.json")
VIZ_SCOPES: list[str] = SCOPES + ["https://www.googleapis.com/auth/drive.file"]

CREDENTIALS_ENV_VAR = "G_GREMLIN_CREDENTIALS"


def set_google_profile(profile: str | None) -> None:
    """Set the active Google profile for this process."""
    global _active_profile
    _active_profile = profile


def get_google_profile() -> str:
    """Resolve the active Google profile name.

    Resolution order: explicit set > env var > ProfileStore current > "default".
    """
    if _active_profile:
        return _active_profile
    env = os.environ.get("G_GREMLIN_GOOGLE_PROFILE", "").strip()
    if env:
        return env
    store = _load_google_profile_store()
    return store.current_profile_name()


def _load_google_profile_store():
    """Load the Google profile store (lazy import to avoid circular deps)."""
    from g_gremlin.profile_manager import ProfileStore
    return ProfileStore.load(GOOGLE_PROFILES_PATH, default_profile="default")


def _token_path_for_profile(profile: str) -> Path:
    """Return the token file path for a named profile."""
    if profile == "default":
        return gremlin_path("token.json")
    return gremlin_path(f"token.{profile}.json")


def _fetch_google_email(creds: Credentials) -> str | None:
    """Fetch the email address of the authenticated Google account."""
    try:
        service = build("oauth2", "v2", credentials=creds, cache_discovery=False)
        info = service.userinfo().get().execute()
        return info.get("email")
    except Exception:
        return None


def maybe_adopt_legacy_token() -> None:
    """If token.json exists but no google_profiles.json, register it as 'default'."""
    legacy = gremlin_path("token.json")
    if not legacy.exists():
        return
    if GOOGLE_PROFILES_PATH.exists():
        return
    store = _load_google_profile_store()
    # Try to get email from the existing token
    email = None
    try:
        creds = Credentials.from_authorized_user_file(str(legacy), SCOPES)
        if creds.expired and creds.refresh_token:
            creds.refresh(Request())
            legacy.write_text(creds.to_json())
        email = _fetch_google_email(creds)
    except Exception:
        pass
    store.set_profile("default", {"email": email or "(unknown)"})
    store.save()


def _dynamic_path_override(name: str) -> Path | None:
    value = globals().get(name)
    if isinstance(value, Path):
        return value
    if isinstance(value, str) and value.strip():
        return Path(value)
    return None


def _config_dir() -> Path:
    return _dynamic_path_override("CONFIG_DIR") or resolve_gremlin_home()


def _token_path() -> Path:
    override = _dynamic_path_override("TOKEN_PATH")
    if override:
        return override
    return _token_path_for_profile(get_google_profile())


def _default_credentials_path() -> Path:
    return _dynamic_path_override("DEFAULT_CREDENTIALS_PATH") or gremlin_path("credentials.json")


def __getattr__(name: str) -> Any:
    if name == "CONFIG_DIR":
        return _config_dir()
    if name == "TOKEN_PATH":
        return _token_path()
    if name == "DEFAULT_CREDENTIALS_PATH":
        return _default_credentials_path()
    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")


def _is_ci_environment() -> bool:
    """Detect if running in CI/CD environment."""
    return any(os.environ.get(var) for var in ("GITHUB_ACTIONS", "CI", "GITLAB_CI"))


def _ensure_config_dir() -> None:
    """
    Make sure the user config directory exists.
    """
    _config_dir().mkdir(parents=True, exist_ok=True)


def _get_credentials_path() -> Path:
    """
    Resolve the client secret file path from an env var or default location.
    """
    env_path = os.environ.get(CREDENTIALS_ENV_VAR)
    if env_path:
        path = Path(env_path).expanduser()
        if path.exists():
            return path
        raise credentials_not_found_error(attempted_path=path)

    default_credentials_path = _default_credentials_path()
    if default_credentials_path.exists():
        return default_credentials_path

    raise credentials_not_found_error()


def _load_credentials_from_env(scopes: Optional[list[str]] = None) -> Optional[Credentials]:
    """Load credentials from env vars for CI (refresh token approach)."""
    effective_scopes = scopes or SCOPES
    client_id = os.environ.get("G_GREMLIN_GOOGLE_CLIENT_ID")
    client_secret = os.environ.get("G_GREMLIN_GOOGLE_CLIENT_SECRET")
    refresh_token = os.environ.get("G_GREMLIN_GOOGLE_REFRESH_TOKEN")

    if not all([client_id, client_secret, refresh_token]):
        return None

    creds = Credentials(
        token=None,
        refresh_token=refresh_token,
        token_uri="https://oauth2.googleapis.com/token",
        client_id=client_id,
        client_secret=client_secret,
        scopes=effective_scopes,
    )
    try:
        creds.refresh(Request())
    except RefreshError as exc:
        raise AuthenticationError(
            "Failed to refresh Google OAuth credentials from environment variables.",
            "Verify G_GREMLIN_GOOGLE_CLIENT_ID, G_GREMLIN_GOOGLE_CLIENT_SECRET, "
            "and G_GREMLIN_GOOGLE_REFRESH_TOKEN are valid.",
        ) from exc
    return creds


def _token_has_scopes(scopes: list[str]) -> bool:
    token_path = _token_path()
    if not token_path.exists():
        return False
    try:
        payload = json.loads(token_path.read_text(encoding="utf-8"))
    except Exception:
        return False
    token_scopes = payload.get("scopes") or []
    if not token_scopes:
        return False
    return set(scopes).issubset(set(token_scopes))


def _load_credentials(scopes: list[str]) -> Credentials:
    _ensure_config_dir()
    token_path = _token_path()
    try:
        env_creds = _load_credentials_from_env(scopes)
    except TypeError:
        # Backward-compatibility for tests/monkeypatches that stub this helper
        # with a zero-arg callable.
        env_creds = _load_credentials_from_env()
    if env_creds:
        return env_creds
    if _is_ci_environment():
        raise ci_credentials_missing_error()

    creds: Optional[Credentials] = None
    token_scopes_ok = _token_has_scopes(scopes)

    if token_path.exists() and token_scopes_ok:
        try:
            creds = Credentials.from_authorized_user_file(str(token_path), scopes)
        except Exception as exc:
            raise RuntimeError(
                f"Failed to read existing token at {token_path}: {exc}. "
                "Delete the file to re-authenticate."
            ) from exc

    if creds and creds.expired and creds.refresh_token:
        try:
            creds.refresh(Request())
            token_path.write_text(creds.to_json())
            return creds
        except RefreshError as exc:
            raise token_expired_error(token_path) from exc

    if not creds or not creds.valid or not token_scopes_ok:
        try:
            credentials_path = _get_credentials_path()
        except FileNotFoundError:
            raise
        try:
            flow = InstalledAppFlow.from_client_secrets_file(str(credentials_path), scopes)
            creds = flow.run_local_server(port=0)
            token_path.write_text(creds.to_json())
        except Exception as exc:
            raise RuntimeError(
                "OAuth flow failed. Verify your client credentials file and try again. "
                f"Details: {exc}"
            ) from exc

    if creds is None:
        raise RuntimeError("Failed to load Google credentials.")

    return creds


def load_credentials() -> Credentials:
    """
    Load user credentials, prompting for OAuth if needed.
    """
    return _load_credentials(SCOPES)


def load_credentials_with_scopes(scopes: list[str]) -> Credentials:
    """
    Load user credentials with explicit OAuth scopes.
    """
    return _load_credentials(scopes)


def get_sheets_service() -> Resource:
    """
    Build and return the Google Sheets API service client.
    """
    credentials = load_credentials()
    return build("sheets", "v4", credentials=credentials, cache_discovery=False)


def get_drive_service() -> Resource:
    """
    Build and return the Google Drive API service client.
    """
    credentials = load_credentials()
    return build("drive", "v3", credentials=credentials, cache_discovery=False)


def get_drive_service_with_scopes(scopes: list[str]) -> Resource:
    """
    Build and return a Google Drive API client with explicit scopes.
    """
    credentials = load_credentials_with_scopes(scopes)
    return build("drive", "v3", credentials=credentials, cache_discovery=False)


def get_script_service() -> Resource:
    """
    Build and return the Google Apps Script API service client.
    """
    credentials = load_credentials()
    return build("script", "v1", credentials=credentials, cache_discovery=False)


def get_slides_service() -> Resource:
    """
    Build and return the Google Slides API service client.
    """
    credentials = load_credentials()
    return build("slides", "v1", credentials=credentials, cache_discovery=False)


def upload_file_to_drive(
    file_path: Path,
    *,
    mime_type: str,
    name: Optional[str] = None,
    parent_id: Optional[str] = None,
    scopes: Optional[list[str]] = None,
) -> dict:
    """
    Upload a file to Google Drive and return the file metadata.
    """
    drive = get_drive_service_with_scopes(scopes or VIZ_SCOPES)
    body: dict[str, Any] = {"name": name or file_path.name}
    if parent_id:
        body["parents"] = [parent_id]
    media = MediaFileUpload(str(file_path), mimetype=mime_type, resumable=False)
    return (
        drive.files()
        .create(body=body, media_body=media, fields="id,name,webViewLink,webContentLink")
        .execute()
    )


def check_viz_scope() -> bool:
    """
    Check whether the stored OAuth token includes the Drive upload scope.
    """
    return _token_has_scopes(VIZ_SCOPES)


def build_sheets_service_from_token(access_token: str) -> Resource:
    """
    Build a Sheets client from an existing OAuth access token (delegated).
    """
    import logging
    log = logging.getLogger(__name__)

    token = (access_token or "").strip()
    if not token:
        raise ValueError("Access token is required to build Sheets client.")

    # Debug: log token info (not the full token for security)
    token_preview = token[:30] + "..." if len(token) > 30 else token
    log.info(f"[SHEETS] Building client with token: {token_preview} (length={len(token)})")

    # Include scopes - required for the credentials to be valid for Sheets API
    credentials = Credentials(
        token=token,
        scopes=["https://www.googleapis.com/auth/spreadsheets"],
    )
    return build("sheets", "v4", credentials=credentials, cache_discovery=False)


def get_sheet_metadata(spreadsheet_id: str) -> dict:
    """
    Fetch metadata for a sheet from Drive, including ETag and revision info.
    """
    service = get_drive_service()
    backoff = [0, 1, 2]  # simple retries for transient errors
    last_err = None
    for sleep_s in backoff:
        if sleep_s:
            time.sleep(sleep_s)
        try:
            request = service.files().get(
                fileId=spreadsheet_id,
                fields="id,headRevisionId,modifiedTime",
            )
            response = request.execute()
            etag = getattr(request, "resp", {}).get("etag") if hasattr(request, "resp") else None
            if etag:
                response["etag"] = etag
            return response
        except HttpError as err:
            last_err = err
            # Retry on common transient codes
            if err.resp is not None and err.resp.status in (429, 500, 503):
                continue
            raise
    if last_err:
        raise last_err
