"""
embedding_service.py

OpenAI Embedding API를 사용하는 임베딩 서비스.
- 단일 텍스트 임베딩: get_embedding()
- 배치 임베딩: get_embeddings_batch()
"""

import logging
import os
import time

import openai

logger = logging.getLogger(__name__)

_EMBEDDING_MODEL = "text-embedding-3-small"
_EMBEDDING_DIMENSIONS = 1536
_BATCH_SIZE = 100
_MAX_RETRIES = 3
_BACKOFF_BASE = 1  # 초 단위: 1, 2, 4 (지수 백오프)


def _get_api_key() -> str:
    """
    환경변수에서 OpenAI API 키를 읽어 반환한다.

    Raises:
        ValueError: OPENAI_API_KEY 환경변수가 설정되지 않은 경우.
    """
    api_key = os.environ.get("OPENAI_API_KEY")
    if not api_key:
        raise ValueError(
            "OPENAI_API_KEY 환경변수가 설정되지 않았습니다. " "올바른 API 키를 OPENAI_API_KEY 환경변수에 설정하세요."
        )
    return api_key


def _call_embeddings_with_retry(
    client: openai.OpenAI,
    texts: list[str],
) -> list[list[float]]:
    """
    OpenAI embeddings.create를 호출하며 에러 시 재시도한다.

    재시도 정책:
    - openai.RateLimitError (HTTP 429): 지수 백오프(1, 2, 4초) 후 재시도
    - openai.APIError: 최대 3회(초기 시도 포함) 재시도
    - 3회 모두 실패 시 원래 예외를 raise

    Args:
        client: OpenAI 클라이언트 인스턴스.
        texts: 임베딩할 텍스트 목록.

    Returns:
        임베딩 벡터 목록. 각 벡터는 1536차원 float 리스트.

    Raises:
        openai.APIError: 최대 재시도 횟수 초과 시.
        openai.RateLimitError: 최대 재시도 횟수 초과 시.
    """
    last_exception: Exception | None = None

    for attempt in range(_MAX_RETRIES):
        try:
            response = client.embeddings.create(
                input=texts,
                model=_EMBEDDING_MODEL,
                dimensions=_EMBEDDING_DIMENSIONS,
            )
            vectors: list[list[float]] = [item.embedding for item in response.data]
            return vectors

        except openai.RateLimitError as exc:
            last_exception = exc
            backoff = _BACKOFF_BASE * (2**attempt)  # 1, 2, 4
            logger.warning(
                "Rate limit 에러 발생 (시도 %d/%d). %d초 후 재시도합니다.",
                attempt + 1,
                _MAX_RETRIES,
                backoff,
            )
            time.sleep(backoff)

        except openai.APIError as exc:
            last_exception = exc
            logger.warning(
                "API 에러 발생 (시도 %d/%d): %s",
                attempt + 1,
                _MAX_RETRIES,
                exc,
            )

    # 모든 재시도 소진 → 마지막 예외를 re-raise
    logger.error("최대 재시도 횟수(%d회) 초과. 에러를 raise합니다.", _MAX_RETRIES)
    assert last_exception is not None
    raise last_exception


def get_embedding(text: str) -> list[float]:
    """
    단일 텍스트의 임베딩 벡터를 반환한다.

    Args:
        text: 임베딩할 텍스트 문자열.

    Returns:
        1536차원 float 리스트.

    Raises:
        ValueError: OPENAI_API_KEY 환경변수가 설정되지 않은 경우.
        openai.APIError: API 호출이 3회 모두 실패한 경우.
    """
    api_key = _get_api_key()
    client = openai.OpenAI(api_key=api_key)

    logger.debug("단일 텍스트 임베딩 요청: %r", text[:50])
    vectors = _call_embeddings_with_retry(client, [text])
    return vectors[0]


def get_embeddings_batch(texts: list[str]) -> list[list[float]]:
    """
    여러 텍스트의 임베딩 벡터를 배치로 반환한다.

    100개를 초과하는 경우 100개씩 분할하여 여러 번 API를 호출한다.

    Args:
        texts: 임베딩할 텍스트 문자열 목록.

    Returns:
        각 텍스트에 대응하는 1536차원 벡터 목록.
        입력이 빈 리스트인 경우 빈 리스트를 반환한다.

    Raises:
        ValueError: OPENAI_API_KEY 환경변수가 설정되지 않은 경우.
        openai.APIError: API 호출이 3회 모두 실패한 경우.
    """
    if not texts:
        return []

    api_key = _get_api_key()
    client = openai.OpenAI(api_key=api_key)

    all_vectors: list[list[float]] = []
    total = len(texts)

    for batch_start in range(0, total, _BATCH_SIZE):
        batch = texts[batch_start : batch_start + _BATCH_SIZE]
        logger.debug(
            "배치 임베딩 요청: %d~%d / 총 %d개",
            batch_start + 1,
            batch_start + len(batch),
            total,
        )
        batch_vectors = _call_embeddings_with_retry(client, batch)
        all_vectors.extend(batch_vectors)

    return all_vectors
