"""client.py - 아누 공용 임베딩 서비스 얇은 파이썬 클라이언트.

원칙: 서비스 미기동/오류 시 **조용한 폴백 금지**. 항상 명시적 예외를 던진다.
(0 벡터나 None 을 돌려주면 하위 파이프라인이 "임베딩이 된 것처럼" 오동작한다.)

사용:
    from client import embed_texts, embed_one
    vecs = embed_texts(["안녕하세요", "보험 상담"])   # -> list[list[float]] (각 768)
    vec = embed_one("실손의료비")                      # -> list[float] (768)

표준 라이브러리만 사용하므로 추가 의존성이 없다.
"""

from __future__ import annotations

import json
import logging
import urllib.error
import urllib.request
from typing import Any, Optional

from settings import CLIENT_BASE_URL, CLIENT_TIMEOUT, EMBEDDING_DIM

logger = logging.getLogger("anu-embedding.client")


class EmbeddingServiceError(RuntimeError):
    """임베딩 서비스 호출 실패 (연결 불가, HTTP 오류, 응답 불변식 위반)."""


def _post_json(url: str, payload: dict[str, Any], timeout: float) -> dict[str, Any]:
    """JSON POST 를 수행하고 파싱된 dict 를 반환합니다. 실패 시 예외."""
    data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
    req = urllib.request.Request(  # noqa: S310 - 고정 http 스킴 로컬 서비스
        url,
        data=data,
        headers={"Content-Type": "application/json"},
        method="POST",
    )
    try:
        with urllib.request.urlopen(req, timeout=timeout) as resp:  # noqa: S310
            body = resp.read().decode("utf-8")
    except urllib.error.HTTPError as exc:
        detail = ""
        try:
            detail = exc.read().decode("utf-8", errors="replace")
        except Exception:  # noqa: BLE001 - 본문 회수 실패는 부가정보일 뿐
            detail = ""
        logger.error("임베딩 서비스 HTTP 오류: url=%s status=%s body=%s", url, exc.code, detail)
        raise EmbeddingServiceError(
            f"임베딩 서비스 HTTP {exc.code} 오류: url={url} detail={detail}"
        ) from exc
    except urllib.error.URLError as exc:
        logger.error("임베딩 서비스 연결 실패: url=%s reason=%s", url, exc.reason)
        raise EmbeddingServiceError(
            f"임베딩 서비스에 연결할 수 없습니다: url={url} reason={exc.reason} "
            f"(서비스 기동: systemctl --user start anu-embedding)"
        ) from exc
    except TimeoutError as exc:
        logger.error("임베딩 서비스 타임아웃: url=%s timeout=%.1fs", url, timeout)
        raise EmbeddingServiceError(f"임베딩 서비스 타임아웃: url={url} timeout={timeout}s") from exc

    try:
        parsed = json.loads(body)
    except json.JSONDecodeError as exc:
        logger.error("임베딩 서비스 응답 JSON 파싱 실패: url=%s", url)
        raise EmbeddingServiceError(f"임베딩 서비스 응답 파싱 실패: url={url}") from exc

    if not isinstance(parsed, dict):
        raise EmbeddingServiceError(f"임베딩 서비스 응답이 객체가 아닙니다: url={url}")
    return parsed


def embed_texts(
    texts: list[str],
    normalize: bool = True,
    base_url: Optional[str] = None,
    timeout: Optional[float] = None,
) -> list[list[float]]:
    """텍스트 목록을 임베딩합니다.

    Returns:
        len(texts) 개의 768차원 float 리스트.

    Raises:
        ValueError: texts 가 비어 있거나 문자열 배열이 아닌 경우.
        EmbeddingServiceError: 서비스 미기동/HTTP 오류/차원 불변식 위반.
    """
    if not isinstance(texts, list) or not texts:
        raise ValueError("texts 는 1개 이상의 문자열을 담은 list 여야 합니다.")
    if any(not isinstance(t, str) for t in texts):
        raise ValueError("texts 의 모든 원소는 문자열이어야 합니다.")

    url = f"{(base_url or CLIENT_BASE_URL).rstrip('/')}/embed"
    parsed = _post_json(
        url,
        {"texts": texts, "normalize": normalize},
        timeout if timeout is not None else CLIENT_TIMEOUT,
    )

    embeddings = parsed.get("embeddings")
    if not isinstance(embeddings, list):
        raise EmbeddingServiceError(f"응답에 embeddings 배열이 없습니다: keys={sorted(parsed)}")
    if len(embeddings) != len(texts):
        raise EmbeddingServiceError(
            f"응답 벡터 개수 불일치: expected={len(texts)} actual={len(embeddings)}"
        )
    for idx, vec in enumerate(embeddings):
        if not isinstance(vec, list) or len(vec) != EMBEDDING_DIM:
            actual = len(vec) if isinstance(vec, list) else type(vec).__name__
            raise EmbeddingServiceError(
                f"임베딩 차원 불변식 위반: embeddings[{idx}] dimension={actual} expected={EMBEDDING_DIM}"
            )
    return [[float(v) for v in vec] for vec in embeddings]


def embed_one(
    text: str,
    normalize: bool = True,
    base_url: Optional[str] = None,
    timeout: Optional[float] = None,
) -> list[float]:
    """단일 텍스트를 임베딩하여 768차원 벡터를 반환합니다."""
    if not isinstance(text, str) or not text.strip():
        raise ValueError("text 는 비어 있지 않은 문자열이어야 합니다.")
    return embed_texts([text], normalize=normalize, base_url=base_url, timeout=timeout)[0]


def health(base_url: Optional[str] = None, timeout: Optional[float] = None) -> dict[str, Any]:
    """서비스 health 를 조회합니다. 미기동 시 EmbeddingServiceError."""
    url = f"{(base_url or CLIENT_BASE_URL).rstrip('/')}/health"
    try:
        with urllib.request.urlopen(  # noqa: S310
            url, timeout=timeout if timeout is not None else CLIENT_TIMEOUT
        ) as resp:
            return json.loads(resp.read().decode("utf-8"))
    except (urllib.error.URLError, TimeoutError, json.JSONDecodeError) as exc:
        logger.error("health 조회 실패: url=%s error=%s", url, exc)
        raise EmbeddingServiceError(f"임베딩 서비스 health 조회 실패: url={url} error={exc}") from exc


if __name__ == "__main__":
    import sys

    args = sys.argv[1:] or ["안녕하세요", "실손의료비 보장 범위"]
    vectors = embed_texts(args)
    for text_arg, vector in zip(args, vectors):
        print(f"{text_arg!r} -> dim={len(vector)} head={vector[:3]}")
