"""
埋め込みサービス実装
テキストの埋め込みベクトル生成とキャッシュ機能
"""

import asyncio
import hashlib
from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional

import numpy as np

from ..config import get_config
from ..utils.cache import LRUCache
from ..utils.logging import get_memory_logger

logger = get_memory_logger(__name__)


class EmbeddingService:
    """埋め込みサービス基底クラス"""

    def __init__(self, cache_size: int = 1000, cache_ttl_hours: int = 24):
        self.cache = LRUCache(max_size=cache_size)
        self.cache_ttl = timedelta(hours=cache_ttl_hours)
        self.embedding_lock = asyncio.Lock()

    async def get_embedding(self, text: str) -> Optional[np.ndarray]:
        """テキストの埋め込みベクトルを取得"""
        if not text or not text.strip():
            return None

        # キャッシュキーを生成
        cache_key = self._get_cache_key(text)

        # キャッシュから取得を試行
        cached_result = self.cache.get(cache_key)
        if cached_result:
            embedding, timestamp = cached_result
            # TTL チェック
            if datetime.utcnow() - timestamp < self.cache_ttl:
                logger.debug("Embedding cache hit", extra_data={"cache_key": cache_key[:16] + "..."})
                return embedding  # type: ignore[no-any-return]
            else:
                # 期限切れエントリを削除
                self.cache.delete(cache_key)

        # 新しい埋め込みを生成
        async with self.embedding_lock:
            embedding = await self._generate_embedding(text)

            if embedding is not None:
                # キャッシュに保存
                self.cache.set(cache_key, (embedding, datetime.utcnow()))
                logger.debug(
                    "Embedding generated and cached",
                    extra_data={"cache_key": cache_key[:16] + "...", "embedding_dim": len(embedding)},
                )

            return embedding

    async def get_embeddings_batch(self, texts: List[str], batch_size: int = 10) -> List[Optional[np.ndarray]]:
        """複数テキストの埋め込みを一括取得"""
        results = []

        # バッチ処理
        for i in range(0, len(texts), batch_size):
            batch_texts = texts[i : i + batch_size]
            batch_results = await asyncio.gather(*[self.get_embedding(text) for text in batch_texts])
            results.extend(batch_results)

        return results

    def _get_cache_key(self, text: str) -> str:
        """キャッシュキーを生成"""
        # テキストのハッシュを使用
        text_hash = hashlib.sha256(text.encode("utf-8")).hexdigest()
        return f"embedding:{text_hash}"

    async def _generate_embedding(self, text: str) -> Optional[np.ndarray]:
        """埋め込みベクトルを生成（サブクラスで実装）"""
        raise NotImplementedError

    def get_cache_stats(self) -> Dict[str, Any]:
        """キャッシュ統計を取得"""
        return {
            "cache_size": len(self.cache.cache),
            "cache_max_size": self.cache.capacity,
            "cache_hit_ratio": getattr(self.cache, "get_hit_ratio", lambda: 0.0)(),
        }


class OpenAIEmbeddingService(EmbeddingService):
    """OpenAI Embeddings API を使用した埋め込みサービス"""

    def __init__(self, api_key: str, model: str = "text-embedding-3-small", **kwargs: Any) -> None:
        super().__init__(**kwargs)
        self.api_key = api_key
        self.model = model
        self._client: Optional[Any] = None

        # 基本的な検証：APIキーの形式チェック
        if not api_key or not isinstance(api_key, str):
            raise ValueError("API key must be a non-empty string")

        # OpenAI APIキーの基本形式チェック
        if not api_key.startswith(("sk-", "sk-proj-")):
            raise ValueError(f"Invalid OpenAI API key format: {api_key[:10]}... (must start with 'sk-' or 'sk-proj-')")

        logger.info(f"OpenAIEmbeddingService initialized with model: {model}")

    async def _check_api_key(self) -> None:
        """APIキーの有効性を起動時に検証（embedding生成を1回試行）"""
        try:
            await self._generate_embedding("APIキー検証用テキスト")
        except Exception as e:
            raise RuntimeError(f"OpenAI APIキーが不正です: {e}")

    async def _get_client(self) -> Any:
        """OpenAI クライアントを遅延初期化"""
        if self._client is None:
            try:
                import openai

                # テストモード検知：外部API呼び出しのみをモック化
                import os
                import sys

                is_test_mode = (
                    "pytest" in sys.modules
                    or os.getenv("TESTING", "").lower() in ("1", "true", "yes")
                    or os.getenv("ENVIRONMENT", "").lower() == "test"
                    or "--test" in sys.argv
                )

                if is_test_mode:
                    # MockクライアントでAPI呼び出し部分のみをモック化
                    logger.info("Test mode detected, using mock OpenAI client")
                    self._client = MockOpenAIClient()
                else:
                    self._client = openai.AsyncOpenAI(api_key=self.api_key)
            except ImportError:
                logger.error("OpenAI package not installed", error_code="OPENAI_IMPORT_ERROR")
                raise
        return self._client

    async def _generate_embedding(self, text: str) -> Optional[np.ndarray]:
        """OpenAI APIで埋め込みを生成"""
        try:
            client = await self._get_client()

            response = await client.embeddings.create(model=self.model, input=text)

            embedding = np.array(response.data[0].embedding, dtype=np.float32)

            logger.debug(
                "OpenAI embedding generated",
                extra_data={"model": self.model, "text_length": len(text), "embedding_dim": len(embedding)},
            )

            return embedding

        except Exception as e:
            # OpenAI認証エラー時は明示的に例外を投げてトランザクション失敗扱いにする
            if hasattr(e, "status_code") and getattr(e, "status_code") == 401:
                logger.error(
                    "OpenAI認証エラー（APIキー不正）",
                    error_code="OPENAI_AUTH_ERROR",
                    model=self.model,
                    text_length=len(text),
                    error=str(e),
                )
                raise RuntimeError("OpenAI APIキーが不正です（401 Unauthorized）")
            logger.error(
                "Failed to generate OpenAI embedding",
                error_code="OPENAI_EMBEDDING_ERROR",
                model=self.model,
                text_length=len(text),
                error=str(e),
            )
            raise


class SentenceTransformerEmbeddingService(EmbeddingService):
    """Sentence Transformers を使用した埋め込みサービス"""

    def __init__(self, model_name: str = "all-MiniLM-L6-v2", device: str = "cpu", **kwargs: Any) -> None:
        super().__init__(**kwargs)
        self.model_name = model_name
        self.device = device
        self._model: Optional[Any] = None

    async def _get_model(self) -> Any:
        """モデルを遅延初期化"""
        if self._model is None:
            try:
                from sentence_transformers import SentenceTransformer

                self._model = SentenceTransformer(self.model_name, device=self.device)
                logger.info(
                    "SentenceTransformer model loaded",
                    extra_data={"model_name": self.model_name, "device": self.device},
                )
            except ImportError:
                logger.error(
                    "SentenceTransformers package not installed", error_code="SENTENCE_TRANSFORMER_IMPORT_ERROR"
                )
                raise
        return self._model

    async def _generate_embedding(self, text: str) -> Optional[np.ndarray]:
        """Sentence Transformersで埋め込みを生成"""
        try:
            model = await self._get_model()

            # 非同期実行のため、ループで実行
            loop = asyncio.get_event_loop()
            embedding = await loop.run_in_executor(None, lambda: model.encode([text])[0])

            embedding = np.array(embedding, dtype=np.float32)

            logger.debug(
                "SentenceTransformer embedding generated",
                extra_data={"model_name": self.model_name, "text_length": len(text), "embedding_dim": len(embedding)},
            )

            return embedding

        except Exception as e:
            logger.error(
                "Failed to generate SentenceTransformer embedding",
                error_code="SENTENCE_TRANSFORMER_EMBEDDING_ERROR",
                model_name=self.model_name,
                text_length=len(text),
                error=str(e),
            )
            return None


class MockOpenAIClient:
    """テスト用OpenAIクライアントモック"""

    class MockEmbeddings:
        async def create(self, model: str, input: str) -> Any:
            """モック埋め込み生成"""
            # テキストハッシュベースの決定的な埋め込み生成
            text_hash = hashlib.sha256(input.encode("utf-8")).hexdigest()
            seed = int(text_hash[:8], 16)
            np.random.seed(seed)

            # モデルに応じた次元数
            if "large" in model:
                dim = 3072
            elif "small" in model:
                dim = 1536
            else:
                dim = 1536

            embedding = np.random.normal(0, 1, dim).astype(np.float32)
            # 正規化
            norm = np.linalg.norm(embedding)
            if norm > 0:
                embedding = embedding / norm

            class MockResponse:
                def __init__(self, embedding):
                    class MockData:
                        def __init__(self, embedding):
                            self.embedding = embedding.tolist()

                    self.data = [MockData(embedding)]

            return MockResponse(embedding)

    def __init__(self):
        self.embeddings = self.MockEmbeddings()


class MockEmbeddingService(EmbeddingService):
    """テスト用モック埋め込みサービス"""

    def __init__(self, embedding_dim: int = 384, **kwargs: Any) -> None:
        super().__init__(**kwargs)
        self.embedding_dim = embedding_dim

    async def _generate_embedding(self, text: str) -> Optional[np.ndarray]:
        """固定次元のランダム埋め込みを生成"""
        try:
            # テキストのハッシュをシードに使用して再現可能に
            text_hash = hashlib.sha256(text.encode("utf-8")).hexdigest()
            seed = int(text_hash[:8], 16)

            np.random.seed(seed)
            embedding = np.random.normal(0, 1, self.embedding_dim).astype(np.float32)

            # 正規化
            norm = np.linalg.norm(embedding)
            if norm > 0:
                embedding = embedding / norm

            logger.debug(
                "Mock embedding generated", extra_data={"text_length": len(text), "embedding_dim": len(embedding)}
            )

            return embedding

        except Exception as e:
            logger.error(
                "Failed to generate mock embedding",
                error_code="MOCK_EMBEDDING_ERROR",
                text_length=len(text),
                error=str(e),
            )
            return None


def create_embedding_service(config: Optional[Dict[str, Any]] = None) -> EmbeddingService:
    """設定に基づいて埋め込みサービスを作成"""
    if config is None:
        config_obj = get_config()
        embedding_config = config_obj.embedding.__dict__  # Convert dataclass to dict
    else:
        embedding_config = config.get("embedding", {})

    # "service"優先、なければ"provider"も許容
    service_type = embedding_config.get("service") or embedding_config.get("provider", "mock")

    # SECURITY: Mask sensitive data in logs
    safe_config = {k: ("***MASKED***" if "key" in k.lower() else v) for k, v in embedding_config.items()}
    logger.info(f"[create_embedding_service] embedding_config: {safe_config}, service_type: {service_type}")

    if service_type == "openai":
        api_key = embedding_config.get("api_key")
        if not api_key:
            raise RuntimeError("OpenAI API key not configured in embedding.api_key")

        logger.info("[create_embedding_service] OpenAIEmbeddingService selected")
        return OpenAIEmbeddingService(
            api_key=api_key,
            model=embedding_config.get("model", "text-embedding-3-small"),
            cache_size=embedding_config.get("cache_size", 1000),
            cache_ttl_hours=embedding_config.get("cache_ttl_hours", 24),
        )

    elif service_type == "sentence_transformer":
        logger.info("[create_embedding_service] SentenceTransformerEmbeddingService selected")
        return SentenceTransformerEmbeddingService(
            model_name=embedding_config.get("model_name", "all-MiniLM-L6-v2"),
            device=embedding_config.get("device", "cpu"),
            cache_size=embedding_config.get("cache_size", 1000),
            cache_ttl_hours=embedding_config.get("cache_ttl_hours", 24),
        )

    elif service_type == "mock":
        logger.info("[create_embedding_service] MockEmbeddingService selected (explicit)")
        return MockEmbeddingService(
            embedding_dim=embedding_config.get("embedding_dim", 384),
            cache_size=embedding_config.get("cache_size", 1000),
            cache_ttl_hours=embedding_config.get("cache_ttl_hours", 24),
        )

    else:
        raise RuntimeError(
            f"Unknown embedding service type: {service_type}. Supported types: openai, sentence_transformer, mock"
        )
