"""gcloud 인증 영구화 모듈.

Application Default Credentials(ADC)를 우선 시도하고,
실패 시 gcloud CLI fallback으로 토큰을 획득합니다.
토큰 캐싱 및 만료 5분 전 자동 갱신을 지원합니다.

추가 지원:
- API 키 인증: get_api_key()로 .env / .env.keys에서 GEMINI_API_KEY 로드
- SA 경로 설정: .env.keys의 GEMINI_SA_PATH 환경변수 로드
- SA 직접 토큰 획득: get_service_account_token(scope)
"""

from __future__ import annotations

import logging
import os
import re
import subprocess
import time
from typing import Any

import google.auth
import google.auth.transport.requests

logger = logging.getLogger("gcloud_auth")

# 토큰 갱신 기준: 만료 5분(300초) 전
_RENEWAL_THRESHOLD_SECS = 300

# 기본 경로
_DEFAULT_ENV_KEYS_PATH = "/home/jay/workspace/.env.keys"
_DEFAULT_ENV_PATH = "/home/jay/workspace/.env"

# 메모리 내 토큰 캐시
_token_cache: dict[str, Any] = {
    "token": None,
    "expiry": None,  # float (time.time() 기준 Unix timestamp)
}


def load_env_keys(env_path: str = _DEFAULT_ENV_KEYS_PATH) -> None:
    """지정된 .env.keys 파일에서 환경변수를 로드합니다.

    GOOGLE_APPLICATION_CREDENTIALS 등 Google 인증 관련 변수를 파싱하여
    현재 프로세스 환경에 설정합니다. 파일이 없으면 조용히 무시합니다.

    Args:
        env_path: .env.keys 파일 경로. 기본값은 프로젝트 표준 경로.
    """
    path = env_path
    if not os.path.exists(path):
        logger.debug("env.keys 파일이 없습니다: %s (무시)", path)
        return

    try:
        with open(path, encoding="utf-8") as f:
            content = f.read()
    except OSError as e:
        logger.warning("env.keys 파일 읽기 실패: %s", e)
        return

    # export KEY="value" 또는 export KEY=value 패턴 파싱
    pattern = re.compile(
        r'^\s*export\s+([A-Z_][A-Z0-9_]*)\s*=\s*"?([^"\n]*)"?\s*$',
        re.MULTILINE,
    )
    _ENV_KEYS_ALLOWLIST = {"GOOGLE_APPLICATION_CREDENTIALS", "GEMINI_SA_PATH"}
    for match in pattern.finditer(content):
        key = match.group(1)
        value = match.group(2).strip()
        if key in _ENV_KEYS_ALLOWLIST:
            os.environ[key] = value
            logger.debug("환경변수 설정: %s=%s", key, value)


def get_api_key(key_name: str = "GEMINI_API_KEY") -> str | None:
    """.env 및 .env.keys 파일에서 API 키를 로드하여 반환합니다.

    탐색 순서:
    1. 현재 환경변수 (os.environ)
    2. .env 파일 (GEMINI_API_KEY 등)
    3. .env.keys 파일

    Args:
        key_name: 로드할 환경변수 이름. 기본값은 "GEMINI_API_KEY".

    Returns:
        API 키 문자열, 없으면 None.
    """
    # 1. 이미 환경변수에 있으면 그대로 반환
    value = os.environ.get(key_name)
    if value:
        logger.debug("환경변수에서 %s 로드", key_name)
        return value

    # 파일에서 파싱하는 내부 헬퍼
    def _parse_file(path: str) -> str | None:
        if not os.path.exists(path):
            return None
        try:
            with open(path, encoding="utf-8") as f:
                content = f.read()
        except OSError:
            return None
        pattern = re.compile(
            r"^\s*(?:export\s+)?" + re.escape(key_name) + r'\s*=\s*"?([^"\n]+)"?\s*$',
            re.MULTILINE,
        )
        match = pattern.search(content)
        if match:
            return match.group(1).strip()
        return None

    # 2. .env 파일
    val = _parse_file(_DEFAULT_ENV_PATH)
    if val:
        logger.debug(".env 파일에서 %s 로드", key_name)
        return val

    # 3. .env.keys 파일
    val = _parse_file(_DEFAULT_ENV_KEYS_PATH)
    if val:
        logger.debug(".env.keys 파일에서 %s 로드", key_name)
        return val

    logger.debug("%s 를 찾을 수 없습니다.", key_name)
    return None


def get_service_account_token(
    scope: str = "https://www.googleapis.com/auth/generative-language",
) -> str:
    """서비스 계정(SA) 키 파일로 액세스 토큰을 직접 생성하여 반환합니다.

    SA 경로 탐색 순서:
    1. GEMINI_SA_PATH 환경변수 (또는 .env.keys에서 로드)
    2. GOOGLE_APPLICATION_CREDENTIALS 환경변수

    Args:
        scope: 요청할 OAuth2 scope URL.
               기본값: "https://www.googleapis.com/auth/generative-language"

    Returns:
        유효한 Bearer 토큰 문자열.

    Raises:
        RuntimeError: SA 경로를 찾을 수 없거나 토큰 생성에 실패한 경우.
    """
    import google.oauth2.service_account

    # .env.keys 로드 (GEMINI_SA_PATH / GOOGLE_APPLICATION_CREDENTIALS 설정)
    load_env_keys(_DEFAULT_ENV_KEYS_PATH)

    sa_path = os.environ.get("GEMINI_SA_PATH") or os.environ.get("GOOGLE_APPLICATION_CREDENTIALS")

    if not sa_path:
        raise RuntimeError(
            "서비스 계정 경로를 찾을 수 없습니다. " "GEMINI_SA_PATH 또는 GOOGLE_APPLICATION_CREDENTIALS를 설정하세요."
        )

    if not os.path.exists(sa_path):
        raise RuntimeError(f"서비스 계정 파일이 존재하지 않습니다: {sa_path}")

    logger.info("SA 키 파일로 토큰 생성 중: %s (scope: %s)", sa_path, scope)
    try:
        credentials = google.oauth2.service_account.Credentials.from_service_account_file(
            sa_path,
            scopes=[scope],
        )
        request = google.auth.transport.requests.Request()  # type: ignore[attr-defined]
        credentials.refresh(request)
    except Exception as e:
        raise RuntimeError(f"서비스 계정 토큰 생성 실패 ({sa_path}): {e}") from e

    token: str = credentials.token
    if not token:
        raise RuntimeError("서비스 계정 토큰이 비어 있습니다.")

    logger.info("SA 토큰 생성 완료 (길이: %d chars)", len(token))
    return token


def _is_cache_valid() -> bool:
    """캐시된 토큰이 유효하고 5분 이상 남았는지 확인합니다."""
    token = _token_cache["token"]
    expiry = _token_cache["expiry"]

    if token is None or expiry is None:
        return False

    remaining = expiry - time.time()
    return remaining > _RENEWAL_THRESHOLD_SECS


def _try_adc() -> str:
    """Application Default Credentials로 토큰을 획득합니다.

    Returns:
        유효한 액세스 토큰 문자열.

    Raises:
        Exception: ADC 초기화 또는 refresh 실패 시.
    """
    credentials: Any
    credentials, project = google.auth.default()

    # 만료되었거나 유효하지 않으면 refresh
    if not credentials.valid:
        request = google.auth.transport.requests.Request()
        credentials.refresh(request)
        logger.info("ADC 토큰 갱신 완료 (프로젝트: %s)", project)
    else:
        logger.info("ADC 토큰 획득 완료 (프로젝트: %s)", project)

    token: str = credentials.token

    # expiry 계산: credentials.expiry가 있으면 사용, 없으면 1시간 설정
    if hasattr(credentials, "expiry") and credentials.expiry is not None:
        expiry_dt = credentials.expiry
        if expiry_dt.tzinfo is None:
            # naive datetime → UTC로 간주
            expiry_ts = expiry_dt.timestamp()
        else:
            expiry_ts = expiry_dt.timestamp()
    else:
        expiry_ts = time.time() + 3600  # 기본 1시간

    _token_cache["token"] = token
    _token_cache["expiry"] = expiry_ts

    return token


def _try_gcloud_cli() -> str:
    """gcloud CLI를 통해 access token을 획득합니다 (fallback).

    Returns:
        유효한 액세스 토큰 문자열.

    Raises:
        RuntimeError: 빈 토큰 반환 시.
        Exception: subprocess 실행 실패 시.
    """
    logger.info("gcloud CLI fallback으로 토큰 획득 시도 중...")
    result = subprocess.run(
        ["gcloud", "auth", "print-access-token"],
        capture_output=True,
        text=True,
        check=True,
    )
    token = result.stdout.strip()
    if not token:
        raise RuntimeError("gcloud auth print-access-token이 빈 토큰을 반환했습니다.")

    logger.info("gcloud CLI fallback 토큰 획득 완료")

    # gcloud 토큰은 만료 시간을 알 수 없으므로 기본 55분으로 설정
    expiry_ts = time.time() + 55 * 60
    _token_cache["token"] = token
    _token_cache["expiry"] = expiry_ts

    return token


def get_access_token() -> str:
    """유효한 Google Cloud access token을 반환합니다.

    캐시에 유효한 토큰(만료 5분 이상 남음)이 있으면 그대로 반환합니다.
    그렇지 않으면:
    1. ADC(Application Default Credentials) 우선 시도
    2. ADC 실패 시 gcloud CLI fallback

    .env.keys에서 GOOGLE_APPLICATION_CREDENTIALS를 자동으로 로드합니다.

    Returns:
        유효한 Bearer 토큰 문자열.

    Raises:
        RuntimeError: 모든 인증 방법이 실패한 경우.
    """
    # 캐시 유효 여부 확인
    if _is_cache_valid():
        logger.debug("캐시된 토큰 반환 (잔여: %.0f초)", _token_cache["expiry"] - time.time())  # type: ignore[operator]
        return _token_cache["token"]  # type: ignore[return-value]

    # .env.keys에서 GOOGLE_APPLICATION_CREDENTIALS / GEMINI_SA_PATH 로드 (미설정 시)
    if not os.environ.get("GOOGLE_APPLICATION_CREDENTIALS") and not os.environ.get("GEMINI_SA_PATH"):
        load_env_keys(_DEFAULT_ENV_KEYS_PATH)

    # 1순위: SA 토큰 (GEMINI_SA_PATH 또는 GOOGLE_APPLICATION_CREDENTIALS 설정 시)
    sa_path = os.environ.get("GEMINI_SA_PATH") or os.environ.get("GOOGLE_APPLICATION_CREDENTIALS")
    if sa_path:
        try:
            token = get_service_account_token()
            return token
        except Exception as sa_err:
            logger.warning(
                "SA 토큰 획득 실패: %s. ADC fallback 시도 중...",
                sa_err,
            )

    # 2순위: ADC
    try:
        token = _try_adc()
        return token
    except Exception as adc_err:
        logger.warning(
            "ADC 토큰 획득 실패: %s. gcloud CLI fallback 시도 중...",
            adc_err,
        )

    # 3순위: gcloud CLI fallback
    try:
        token = _try_gcloud_cli()
        return token
    except RuntimeError:
        raise
    except Exception as cli_err:
        raise RuntimeError(f"gcloud 인증 실패 — SA, ADC, gcloud CLI 모두 실패: {cli_err}") from cli_err
