"""AI service — summarization, tagging, NER, and embeddings via Claude + OpenAI."""
from __future__ import annotations

import json

import anthropic
import openai
import structlog

from app.config import Settings
from app.exceptions import AIServiceError

log = structlog.get_logger(__name__)

_PIPELINE_VERSION = "1.0.0"

# ── Prompts ───────────────────────────────────────────────────────────────────

_ANALYSIS_SYSTEM = (
    "أنت مساعد متخصص في تحليل المحتوى الرقمي وتلخيصه. "
    "تجيب دائماً بصيغة JSON صارمة بدون أي نص خارجها."
)

_ANALYSIS_PROMPT = """\
حلّل النص أو المحتوى التالي وأعِد استجابة JSON بالشكل الآتي بالضبط:

{{
  "summary": "ملخص من ٣ إلى ٥ جمل بالعربية الفصحى يُركّز على الأفكار الرئيسية",
  "keywords": ["كلمة_مفتاحية_١", "كلمة_مفتاحية_٢"],
  "tags": ["وسم_١", "وسم_٢", "وسم_٣"],
  "entities": {{
    "people": ["اسم_شخص_١"],
    "places": ["مكان_١"],
    "orgs": ["مؤسسة_١"]
  }},
  "language": "ar"
}}

القواعد:
- الملخص: بالعربية الفصحى، بدون حشو، بدون معلومات من خارج النص
- الوسوم: من ٢ إلى ٧ وسوم، بدون أل التعريف، بالمفرد قدر الإمكان
- الكلمات المفتاحية: من ٣ إلى ١٠ كلمات مفتاحية
- الكيانات: فقط ما ذُكر صراحةً في المحتوى
- إذا كان المحتوى بالإنجليزية: أنتج الملخص والوسوم بالعربية

المحتوى:
{content}
"""

_EMBEDDING_INPUT_MAX_CHARS = 8000  # safe limit for embedding API


class AIService:
    def __init__(self, settings: Settings) -> None:
        self._settings = settings
        # Instantiate once per worker process (not per request)
        self._anthropic = anthropic.Anthropic(
            api_key=settings.anthropic_api_key.get_secret_value()
        )
        self._openai = openai.OpenAI(
            api_key=settings.openai_api_key.get_secret_value()
        )

    async def analyze(
        self,
        content: str,
        url: str = "",
        title: str = "",
    ) -> dict:
        """
        Run AI analysis on content.
        Returns dict with: summary, keywords, tags, entities, prompt_tokens, completion_tokens.
        Raises AIServiceError on failure.
        """
        # Truncate to token budget
        truncated = content[: self._settings.max_tokens_per_analysis * 4]  # ~4 chars/token

        # Build context
        context_parts = []
        if title:
            context_parts.append(f"العنوان: {title}")
        if url:
            context_parts.append(f"الرابط: {url}")
        if truncated:
            context_parts.append(f"المحتوى:\n{truncated}")
        context = "\n\n".join(context_parts)

        prompt = _ANALYSIS_PROMPT.format(content=context)

        try:
            response = self._anthropic.messages.create(
                model=self._settings.llm_model,
                max_tokens=1024,
                system=_ANALYSIS_SYSTEM,
                messages=[{"role": "user", "content": prompt}],
            )
        except anthropic.RateLimitError as exc:
            raise AIServiceError(f"Claude rate limit: {exc}", is_permanent=False) from exc
        except anthropic.APIError as exc:
            raise AIServiceError(f"Claude API error: {exc}", is_permanent=False) from exc

        raw_text = response.content[0].text if response.content else ""
        prompt_tokens = response.usage.input_tokens
        completion_tokens = response.usage.output_tokens

        # Parse JSON response
        try:
            # Strip markdown code fences if present
            clean = raw_text.strip()
            if clean.startswith("```"):
                clean = clean.split("```")[1]
                if clean.startswith("json"):
                    clean = clean[4:]
            parsed = json.loads(clean.strip())
        except (json.JSONDecodeError, IndexError) as exc:
            log.warning("ai.parse_error", raw=raw_text[:200], error=str(exc))
            # Return graceful fallback
            parsed = {
                "summary": title or url,
                "keywords": [],
                "tags": [],
                "entities": {"people": [], "places": [], "orgs": []},
                "language": "ar",
            }

        return {
            "summary": str(parsed.get("summary", ""))[:1000],
            "keywords": [str(k) for k in parsed.get("keywords", [])[:10]],
            "tags": [str(t) for t in parsed.get("tags", [])[:7]],
            "entities": {
                "people": [str(p) for p in parsed.get("entities", {}).get("people", [])[:20]],
                "places": [str(p) for p in parsed.get("entities", {}).get("places", [])[:20]],
                "orgs":   [str(o) for o in parsed.get("entities", {}).get("orgs",   [])[:20]],
            },
            "prompt_tokens": prompt_tokens,
            "completion_tokens": completion_tokens,
            "pipeline_version": _PIPELINE_VERSION,
            "llm_model": self._settings.llm_model,
        }

    def generate_embedding(self, text: str) -> list[float]:
        """
        Generate a dense embedding vector for `text`.
        Truncates input to safe length. Raises AIServiceError on failure.
        """
        truncated = text[:_EMBEDDING_INPUT_MAX_CHARS]
        try:
            response = self._openai.embeddings.create(
                model=self._settings.embedding_model,
                input=truncated,
                dimensions=self._settings.embedding_dimensions,
            )
            return response.data[0].embedding  # type: ignore[return-value]
        except openai.RateLimitError as exc:
            raise AIServiceError(f"OpenAI rate limit: {exc}", is_permanent=False) from exc
        except openai.APIError as exc:
            raise AIServiceError(f"OpenAI API error: {exc}", is_permanent=False) from exc

    @staticmethod
    def build_embedding_input(title: str, summary: str, keywords: list[str]) -> str:
        """Compose the text input sent to the embedding model."""
        parts = []
        if title:
            parts.append(f"العنوان: {title}")
        if summary:
            parts.append(f"الملخص: {summary}")
        if keywords:
            parts.append(f"الكلمات المفتاحية: {' | '.join(keywords)}")
        return "\n".join(parts)

    @staticmethod
    def pipeline_version() -> str:
        return _PIPELINE_VERSION
