"""HTTP client and test stub for sagellm embedding."""

from __future__ import annotations

import hashlib
import json
import math
import re
from dataclasses import dataclass, field
from urllib import error, request

# 端口分配统一走 SagePorts（isage-common）
try:
    from sage.common.config.ports import SagePorts as _SagePorts

    _DEFAULT_EMBEDDING_URL: str = f"http://127.0.0.1:{_SagePorts.EMBEDDING_DEFAULT}"
except ImportError:
    _DEFAULT_EMBEDDING_URL = "http://127.0.0.1:8090"  # SagePorts.EMBEDDING_DEFAULT fallback


@dataclass(slots=True)
class EmbeddingClient:
    """Client for OpenAI-compatible `/v1/embeddings` endpoint."""

    base_url: str = _DEFAULT_EMBEDDING_URL
    default_model: str = "sentence-transformers/all-MiniLM-L6-v2"
    timeout_s: float = 30.0

    def _post_json(self, path: str, payload: dict[str, object]) -> dict[str, object]:
        url = f"{self.base_url.rstrip('/')}{path}"
        req = request.Request(
            url=url,
            data=json.dumps(payload).encode("utf-8"),
            headers={"Content-Type": "application/json"},
            method="POST",
        )
        try:
            with request.urlopen(req, timeout=self.timeout_s) as response:  # noqa: S310
                data = response.read().decode("utf-8")
            return json.loads(data)
        except error.HTTPError as exc:
            body = exc.read().decode("utf-8", errors="replace")
            raise RuntimeError(f"Embedding server returned HTTP {exc.code}: {body}") from exc
        except error.URLError as exc:
            raise RuntimeError(
                f"Cannot connect to embedding server at {self.base_url}: {exc.reason}"
            ) from exc

    def health(self) -> bool:
        """Check embedding server health endpoint."""
        url = f"{self.base_url.rstrip('/')}/health"
        req = request.Request(url=url, method="GET")
        try:
            with request.urlopen(req, timeout=self.timeout_s) as response:  # noqa: S310
                data = response.read().decode("utf-8")
            payload = json.loads(data)
            return bool(payload.get("is_ready", False))
        except Exception:
            return False

    def embed(self, texts: list[str], model: str | None = None) -> list[list[float]]:
        """Embed a list of input texts."""
        if not texts:
            return []
        payload = {
            "input": texts,
            "model": model or self.default_model,
        }
        response = self._post_json("/v1/embeddings", payload)
        data = response.get("data", [])
        if not isinstance(data, list):
            raise RuntimeError("Invalid embedding response format: 'data' is not a list")
        return [item.get("embedding", []) for item in data if isinstance(item, dict)]

    def embed_one(self, text: str, model: str | None = None) -> list[float]:
        """Embed a single text."""
        vectors = self.embed([text], model=model)
        return vectors[0] if vectors else []


@dataclass(slots=True)
class LocalHashEmbedder:
    """Deterministic stdlib-only embedding stub — for tests and offline CI.

    Produces L2-normalised fixed-dimension vectors from text via SHA-256 token
    hashing.  Zero external dependencies (no torch, no models, no network).

    For production embedding use ``EmbeddingClient`` (HTTP) or
    ``LLMEngine(task_type="embedding")`` (local).

    Example::

        embedder = LocalHashEmbedder(dim=256)
        vec = embedder.embed("hello sagellm")
        assert abs(sum(v * v for v in vec) ** 0.5 - 1.0) < 1e-6
    """

    dim: int = 384
    _token_re: re.Pattern[str] = field(
        default_factory=lambda: re.compile(r"[\w\u4e00-\u9fa5]+"),
        init=False,
        repr=False,
        compare=False,
    )

    def embed(self, text: str) -> list[float]:
        """Embed a single text into a normalised vector."""
        tokens = self._token_re.findall(text.lower()) or [text.lower()]
        vector = [0.0] * self.dim
        for token in tokens:
            digest = hashlib.sha256(token.encode("utf-8")).digest()
            for offset in range(0, len(digest), 4):
                idx = int.from_bytes(digest[offset : offset + 4], "little") % self.dim
                vector[idx] += 1.0
        norm = math.sqrt(sum(v * v for v in vector)) or 1.0
        return [v / norm for v in vector]

    def embed_batch(self, texts: list[str]) -> list[list[float]]:
        """Embed a list of texts."""
        return [self.embed(text) for text in texts]

    def get_dim(self) -> int:
        """Return the embedding dimension."""
        return self.dim
