"""
Tools for Google Secret Manager Access - Version 2026-02-21

pip3 install --upgrade google-cloud-secret-manager

Credential resolution (same logic as toolsbq, duplicated to avoid heavy dependency):
  1) Explicit keyfile_json / path_keyfile
  2) GOOGLE_APPLICATION_CREDENTIALS env var
  3) Local RAM-ADC fast path (macOS ramdisk / Linux /dev/shm)
  4) Application Default Credentials (Cloud Run metadata, gcloud auth, etc.)

Usage:
  # Standalone (resolves credentials internally)
  secret = get_secret("my-secret")

  # With explicit SA
  secret = get_secret("my-secret", path_keyfile="~/.config/gcloud/sa-keys/key.json")

  # Reuse credentials from toolsbq (avoids re-reading SA file)
  from toolsbq import bq_get_client
  bq = bq_get_client(path_keyfile="~/key.json")
  secret = get_secret("my-secret", credentials=bq._credentials)

  # Pre-build SM client for repeated calls (avoids gRPC channel recreation)
  client = sm_get_client(path_keyfile="~/key.json")
  s1 = get_secret("secret-a", client=client)
  s2 = get_secret("secret-b", client=client)
"""

from __future__ import annotations

import hashlib
import json
import logging
import os
import platform
import threading
from pathlib import Path
from typing import Any, Dict, Tuple

import google.auth
from google.auth.exceptions import DefaultCredentialsError
from google.cloud import secretmanager
from google.oauth2 import service_account

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Cache: (project_id, secret_name, version_id, cred_fingerprint) -> value
# ---------------------------------------------------------------------------
_SECRET_CACHE: Dict[Tuple[str, str, str, str], str] = {}
_SECRET_CACHE_LOCK = threading.Lock()


# ---------------------------------------------------------------------------
# Credential helpers (duplicated from toolsbq — lightweight, no BQ dependency)
# ---------------------------------------------------------------------------


def _normalize_path(path: str) -> str:
    """Expand '~' and environment variables like '$HOME' and return a usable path."""
    p = Path(os.path.expandvars(path)).expanduser()
    return str(p)


def _is_running_on_gcp() -> bool:
    return any(
        os.environ.get(k)
        for k in (
            "K_SERVICE",  # Cloud Run
            "CLOUD_RUN_JOB",  # Cloud Run Jobs / Cloud Functions
        )
    )


def _ram_adc_key_path() -> Path:
    if platform.system() == "Darwin":
        return Path("/Volumes/GCLOUD_SA_RAM/sa.json")
    return Path("/dev/shm/sa.json")


def _ensure_ram_adc_env_if_present() -> None:
    """
    Local-only "temporary ADC replacement":
      - If GOOGLE_APPLICATION_CREDENTIALS already points to an existing file: do nothing
      - If running on GCP: skip RAM checks (use real ADC)
      - Otherwise: if RAM key exists, set GOOGLE_APPLICATION_CREDENTIALS
    """
    gac = os.environ.get("GOOGLE_APPLICATION_CREDENTIALS")
    if gac and Path(gac).expanduser().is_file():
        return
    if _is_running_on_gcp():
        return
    ram_key = _ram_adc_key_path()
    if ram_key.is_file():
        os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = str(ram_key)


# ---------------------------------------------------------------------------
# Credential resolution (mirrors bq_get_client logic)
# ---------------------------------------------------------------------------


def _resolve_credentials(
    *,
    path_keyfile: str | None = None,
    keyfile_json: dict | None = None,
) -> tuple[Any | None, str]:
    """
    Resolve Google credentials + return a fingerprint string for cache keying.

    Order:
      1) Explicit keyfile_json / path_keyfile
      2) RAM-ADC / env / ADC fallback

    Returns:
        (credentials_or_None, fingerprint_string)
        credentials=None means "use ADC" (client will resolve automatically).
    """
    # 1a) Explicit JSON dict
    if keyfile_json:
        creds = service_account.Credentials.from_service_account_info(keyfile_json)
        payload = json.dumps(
            keyfile_json, sort_keys=True, separators=(",", ":")
        ).encode()
        fp = "info:" + hashlib.sha256(payload).hexdigest()[:16]
        return creds, fp

    # 1b) Explicit file path
    if path_keyfile:
        norm = _normalize_path(path_keyfile)
        if not Path(norm).is_file():
            raise FileNotFoundError(f"Service account file not found: {norm}")
        creds = service_account.Credentials.from_service_account_file(norm)
        return creds, f"file:{norm}"

    # 2) Env / RAM-ADC / true ADC
    _ensure_ram_adc_env_if_present()
    return None, "adc"


# ---------------------------------------------------------------------------
# Project ID detection
# ---------------------------------------------------------------------------


def _detect_project_id(
    credentials: Any | None = None,
    *,
    keyfile_json: dict | None = None,
    path_keyfile: str | None = None,
) -> str:
    """Best-effort project ID detection from multiple sources."""

    # 1) Env vars
    for var in ("GOOGLE_CLOUD_PROJECT", "GCLOUD_PROJECT", "GCP_PROJECT"):
        value = os.getenv(var)
        if value:
            return value

    # 2) From explicit SA info
    if keyfile_json:
        pid = keyfile_json.get("project_id")
        if isinstance(pid, str) and pid:
            return pid

    # 3) From credentials object (covers both file and info)
    if credentials is not None:
        pid = getattr(credentials, "project_id", None)
        if isinstance(pid, str) and pid:
            return pid

    # 4) From GOOGLE_APPLICATION_CREDENTIALS file
    _ensure_ram_adc_env_if_present()
    gac = os.environ.get("GOOGLE_APPLICATION_CREDENTIALS")
    if gac and Path(gac).expanduser().is_file():
        creds = service_account.Credentials.from_service_account_file(
            str(Path(gac).expanduser())
        )
        pid = getattr(creds, "project_id", None)
        if isinstance(pid, str) and pid:
            return pid

    # 5) True ADC fallback
    try:
        _, project_id = google.auth.default()
        if isinstance(project_id, str) and project_id:
            return project_id
    except DefaultCredentialsError:
        pass

    raise RuntimeError(
        "Could not determine GCP project ID. "
        "Set GOOGLE_CLOUD_PROJECT / GCLOUD_PROJECT / GCP_PROJECT, "
        "pass project_id explicitly, provide keyfile_json/path_keyfile, "
        f"start the RAM-ADC session (creates {_ram_adc_key_path()}), "
        "or configure Application Default Credentials."
    )


# ---------------------------------------------------------------------------
# Public: sm_get_client  (mirrors bq_get_client)
# ---------------------------------------------------------------------------


def sm_get_client(
    *,
    path_keyfile: str | None = None,
    keyfile_json: dict | None = None,
    credentials: Any | None = None,
) -> secretmanager.SecretManagerServiceClient:
    """
    Build a Secret Manager client.

    Credential priority:
      1) Pre-resolved ``credentials`` object (e.g. from a BQ client via ``bq._credentials``)
      2) Explicit ``keyfile_json`` / ``path_keyfile``
      3) RAM-ADC / env / ADC fallback

    Returns:
        A SecretManagerServiceClient ready to use.
    """
    # If caller already has credentials (e.g. from toolsbq), just wrap them
    if credentials is not None:
        return secretmanager.SecretManagerServiceClient(credentials=credentials)

    creds, _ = _resolve_credentials(
        path_keyfile=path_keyfile, keyfile_json=keyfile_json
    )

    if creds is not None:
        return secretmanager.SecretManagerServiceClient(credentials=creds)

    # ADC fallback — let the client resolve
    try:
        return secretmanager.SecretManagerServiceClient()
    except DefaultCredentialsError as e:
        hint = (
            "\n\nNo Google credentials found.\n"
            "Local options:\n"
            f" - Start your RAM-ADC session (creates {_ram_adc_key_path()})\n"
            " - Or run: gcloud auth application-default login\n"
            " - Or pass path_keyfile/keyfile_json/credentials to sm_get_client\n"
        )
        raise DefaultCredentialsError(str(e) + hint) from e


# ---------------------------------------------------------------------------
# Public: get_secret
# ---------------------------------------------------------------------------


def get_secret(
    secret_name: str,
    *,
    project_id: str | None = None,
    version_id: str = "latest",
    # --- credential options (pick one or none) ---
    client: secretmanager.SecretManagerServiceClient | None = None,
    credentials: Any | None = None,
    path_keyfile: str | None = None,
    keyfile_json: dict | None = None,
) -> str:
    """
    Fetch a secret from Google Secret Manager.

    Credential priority:
      1) ``client``               — pre-built SM client (fastest for repeated calls)
      2) ``credentials``          — pre-resolved google.oauth2 Credentials object
                                    (e.g. ``my_bq_client._credentials`` from toolsbq)
      3) ``keyfile_json``         — SA key as dict
      4) ``path_keyfile``         — path to SA JSON file
      5) RAM-ADC / env / ADC fallback

    Caching:
      Results are cached in-process by (project, secret, version, credential fingerprint).
      Pass ``client`` to also avoid repeated gRPC channel setup.
    """
    if not secret_name:
        raise ValueError("secret_name is required")

    # --- Resolve credentials + fingerprint for cache key ---
    _client_provided = client is not None

    if _client_provided:
        cred_fingerprint = "client"
        resolved_creds = None
    elif credentials is not None:
        resolved_creds = credentials
        cred_fingerprint = f"creds:{id(credentials)}"
    else:
        resolved_creds, cred_fingerprint = _resolve_credentials(
            path_keyfile=path_keyfile, keyfile_json=keyfile_json
        )

    # --- Resolve project_id ---
    if project_id is None:
        project_id = _detect_project_id(
            resolved_creds,
            keyfile_json=keyfile_json,
            path_keyfile=path_keyfile,
        )

    # --- Check cache ---
    cache_key = (project_id, secret_name, version_id, cred_fingerprint)
    with _SECRET_CACHE_LOCK:
        cached = _SECRET_CACHE.get(cache_key)
        if cached is not None:
            logger.debug("Secret '%s' served from cache", secret_name)
            return cached

    # --- Build client if not provided ---
    if _client_provided:
        sm_client = client
    elif resolved_creds is not None:
        sm_client = secretmanager.SecretManagerServiceClient(credentials=resolved_creds)
    else:
        sm_client = secretmanager.SecretManagerServiceClient()

    # --- Fetch ---
    full_name = f"projects/{project_id}/secrets/{secret_name}/versions/{version_id}"
    logger.debug("Fetching secret: %s", full_name)
    response = sm_client.access_secret_version(request={"name": full_name})
    secret_value = response.payload.data.decode("utf-8")

    # --- Store in cache ---
    with _SECRET_CACHE_LOCK:
        _SECRET_CACHE[cache_key] = secret_value

    return secret_value
