from __future__ import annotations

import hashlib
import json
import os
import threading
from pathlib import Path
from typing import Any, Dict, Mapping, Optional, Tuple

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

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


def _normalize_sa_path(service_account_file: Optional[str]) -> Optional[str]:
    if not service_account_file:
        return None
    p = Path(os.path.expandvars(service_account_file)).expanduser()
    return str(p)


def _fingerprint_sa_info(info: Mapping[str, Any]) -> str:
    """
    Create a stable, non-reversible fingerprint for cache keying.
    Includes the full dict content hashed (not stored raw).
    """
    payload = json.dumps(info, sort_keys=True, separators=(",", ":")).encode("utf-8")
    return "info:" + hashlib.sha256(payload).hexdigest()[:16]


def _fingerprint_sa_file(path: str) -> str:
    return "file:" + path


def _load_sa_credentials_from_info(service_account_info: Mapping[str, Any]):
    creds = service_account.Credentials.from_service_account_info(
        dict(service_account_info)
    )
    return creds


def _load_sa_credentials_from_file(service_account_file: str):
    sa_path = _normalize_sa_path(service_account_file)
    if not sa_path:
        raise ValueError("service_account_file was provided but is empty")
    if not Path(sa_path).is_file():
        raise FileNotFoundError(f"Service account file not found: {sa_path}")
    creds = service_account.Credentials.from_service_account_file(sa_path)
    return creds, sa_path


def _detect_project_id(
    *,
    service_account_info: Optional[Mapping[str, Any]] = None,
    service_account_file: Optional[str] = None,
) -> str:
    # 1) env vars
    for var in ("GOOGLE_CLOUD_PROJECT", "GCLOUD_PROJECT", "GCP_PROJECT"):
        value = os.getenv(var)
        if value:
            return value

    # 2) service_account_info project_id
    if service_account_info:
        pid = service_account_info.get("project_id")
        if isinstance(pid, str) and pid:
            return pid

    # 3) service_account_file embedded project_id
    if service_account_file:
        creds, _ = _load_sa_credentials_from_file(service_account_file)
        sa_project_id = getattr(creds, "project_id", None)
        if isinstance(sa_project_id, str) and sa_project_id:
            return sa_project_id

    # 4) ADC
    _, project_id = google.auth.default()
    if isinstance(project_id, str) and project_id:
        return project_id

    raise RuntimeError(
        "Could not determine GCP project ID. "
        "Set GOOGLE_CLOUD_PROJECT / GCLOUD_PROJECT / GCP_PROJECT, "
        "pass project_id explicitly, provide service_account_info/service_account_file, "
        "or configure Application Default Credentials."
    )


def get_secret(
    secret_name: str,
    project_id: Optional[str] = None,
    version_id: str = "latest",
    service_account_file: Optional[str] = None,
    service_account_info: Optional[Mapping[str, Any]] = None,
) -> str:
    """
    Fetch a secret from Google Secret Manager.

    If both service_account_info and service_account_file are provided,
    service_account_info takes precedence.
    """
    if not secret_name:
        raise ValueError("secret_name is required")

    credentials = None
    cred_fingerprint = ""

    if service_account_info:
        credentials = _load_sa_credentials_from_info(service_account_info)
        cred_fingerprint = _fingerprint_sa_info(service_account_info)
    elif service_account_file:
        creds, sa_path = _load_sa_credentials_from_file(service_account_file)
        credentials = creds
        cred_fingerprint = _fingerprint_sa_file(sa_path)
    else:
        cred_fingerprint = "adc"

    if project_id is None:
        project_id = _detect_project_id(
            service_account_info=service_account_info,
            service_account_file=service_account_file,
        )

    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:
            return cached

    client = secretmanager.SecretManagerServiceClient(credentials=credentials)
    full_name = f"projects/{project_id}/secrets/{secret_name}/versions/{version_id}"
    response = client.access_secret_version(request={"name": full_name})
    secret_value = response.payload.data.decode("utf-8")

    with _SECRET_CACHE_LOCK:
        _SECRET_CACHE[cache_key] = secret_value

    return secret_value
