import json
import random
from typing import Dict, Any, Optional
import structlog

from ...shared.clients.openai_client import openai_client
from ...shared.utils.metrics import OperationMetrics
from ...shared.exceptions.base import BeerPairingException
from .config import BeerPairingServiceConfig
from .models.requests import BeerPairingRequest

logger = structlog.get_logger("beer_pairing.service")

VERSION = "2.0.0"


class BeerPairingService:
    def __init__(self, config: BeerPairingServiceConfig):
        self.config = config
        self._rng = random.SystemRandom()

    @staticmethod
    def _analytics_context(
        app_service: str,
        city: str = "City not set",
        zavedenia_id: int = -1,
        **data_input: Any
    ) -> Dict[str, Any]:
        return {
            "city": city,
            "zavedenia_id": zavedenia_id,
            "data_input": {
                "app_service": app_service,
                **{key: value for key, value in data_input.items() if value is not None}
            }
        }

    async def _lookup_untappd_rating(self, beer_name: str, model_override: Optional[str] = None) -> Optional[str]:
        """Lookup Untappd rating for a beer"""
        metrics = OperationMetrics("beer_pairing_untappd_lookup")
        metrics.set_items_count(1)

        model_name = model_override or self.config.untappd_lookup_model

        prompt = (
            "You are a cicerone assistant with reliable access to Untappd community ratings.\n"
            "Provide the Untappd rating for the beer listed below.\n\n"
            f"Beer: {beer_name}\n\n"
            "Return ONLY valid JSON in this format:\n"
            '{"beer_rating": "<rating number like 4.1 or Unknown if not found>"}\n\n'
            "Rules:\n"
            "- Use one decimal place when a rating exists.\n"
            "- If there is no reliable rating, respond with Unknown."
        )

        try:
            result, openai_metrics = await openai_client.json_completion(
                api_key=self.config.openai_api_key,
                model=model_name,
                prompt=prompt,
                operation_id="untappd_rating_lookup",
                service="untappd_lookup",
                max_tokens=self.config.max_tokens,
                temperature=self.config.temperature,
                timeout=self.config.timeout,
                analytics_context=self._analytics_context(
                    "untappd_lookup",
                    "City not set",
                    -1,
                    beer_name=beer_name,
                )
            )

            metrics.set_tokens(
                openai_metrics.get("input_tokens", 0),
                openai_metrics.get("output_tokens", 0)
            )
            metrics.set_cost(openai_metrics.get("cost", 0.0))
            metrics.set_model(openai_metrics.get("model", ""))
            metrics.log_completion()

            rating = result.get("beer_rating")
            if isinstance(rating, str):
                return rating.strip()
            return None

        except Exception as exc:
            metrics.set_error(str(exc))
            metrics.log_completion()
            logger.warning(
                "untappd_rating_lookup_failed",
                beer=beer_name,
                error=str(exc),
                model=model_name
            )
            return None

    async def match_beers(self, request: BeerPairingRequest) -> Dict[str, Any]:
        """Match beers to food dishes"""
        metrics = OperationMetrics("beer_pairing_match")
        metrics.set_items_count(len(request.foods))

        request_model = request.model.strip() if request.model and request.model.strip() else None
        pairing_model = request_model or self.config.beer_pairing_model
        untappd_model = request_model or self.config.untappd_lookup_model

        user_prompt_text = (request.user_prompt or "").strip()

        logger.info(
            "beer_pairing_started",
            language=request.language,
            foods=len(request.foods),
            beer_candidates=len(request.beers),
            model=pairing_model,
            user_prompt_provided=bool(user_prompt_text)
        )

        try:
            def normalize_beer_name(value: Any) -> str:
                return value.strip().casefold() if isinstance(value, str) else ""

            beers_payload = [beer.model_dump(exclude_none=True) for beer in request.beers]
            self._rng.shuffle(beers_payload)

            prompt_payload = {
                "language": request.language,
                "foods": [food.model_dump(exclude_none=True) for food in request.foods],
                "beers": beers_payload,
            }

            beer_price_map = {
                normalize_beer_name(beer.get("name")): beer.get("price")
                for beer in prompt_payload["beers"]
                if beer.get("name")
            }

            untappd_rating_cache: Dict[str, Optional[str]] = {}
            diversity_seed = self._rng.randint(1, 1_000_000)

            prompt_sections = [
                "You are a master cicerone crafting beer pairings for a restaurant menu. "
                "Respect the diner's preferred language when writing your explanations.\n\n",
                f"Output language: {request.language}.\n\n",
                f"Foods to pair: {json.dumps(prompt_payload['foods'], ensure_ascii=False, separators=(',', ':'))}\n\n",
                f"Beer list (only choose from these for matches): {json.dumps(prompt_payload['beers'], ensure_ascii=False, separators=(',', ':'))}\n\n",
                f"Variation seed: {diversity_seed}. When several beers are equally suitable, use this seed to rotate your choices.\n",
                "Recommend between three and five beers per food; if fewer than three beers truly fit, leave matches empty. "
                "If diner instructions ask for a different quantity, override this default and satisfy their request.\n",
                "Always include at least one value-friendly and one premium-priced option when pricing data exists.\n\n",
            ]

            if user_prompt_text:
                prompt_sections.append(
                    "Additional diner instructions (interpret and apply these as explicit pairing constraints):\n"
                    f"{user_prompt_text}\n"
                    "Translate the natural language guidance into actionable rules and honor them when possible. "
                    "Keep the tone and structure identical to the standard response; do not add meta commentary or restate the instructions verbatim in notes. "
                    "Only reflect the guidance when it naturally fits within the usual reasoning, and explicitly adjust the number of matches when requested.\n\n"
                )

            prompt_sections.append(
                "Return ONLY valid JSON in this exact structure:\n"
                "{\n"
                f'  "language": "{request.language}",\n'
                '  "results": [\n'
                '    {\n'
                f'      "food": "<dish name in {request.language}>",\n'
                '      "matches": [\n'
                '        {\n'
                '          "beer": "<beer name from list>",\n'
                '          "reason": "<why they match>",\n'
                '          "beer_rating": "Unknown",\n'
                '          "price": "Unknown"\n'
                '        }\n'
                '      ],\n'
                '      "suggestions": [],\n'
                '      "notes": "<optional summary>"\n'
                '    }\n'
                '  ]\n'
                '}\n\n'
                "Rules:\n"
                "- Use only beers from the provided list for matches.\n"
                "- Keep matches array empty when no listed beer fits.\n"
                "- Set beer_rating and price to Unknown (will be filled later).\n"
                f"- All explanations must be in {request.language}."
            )

            prompt = "".join(prompt_sections)

            result, openai_metrics = await openai_client.json_completion(
                api_key=self.config.openai_api_key,
                model=pairing_model,
                prompt=prompt,
                operation_id="beer_pairing",
                service="beer_pairing",
                max_tokens=self.config.max_tokens,
                temperature=self.config.temperature,
                timeout=self.config.timeout,
                analytics_context=self._analytics_context(
                    "beer_pairing",
                    request.city,
                    request.zavedenia_id,
                    request_body=request.model_dump(exclude_none=True),
                    language=request.language,
                    foods_count=len(request.foods),
                    beers_count=len(request.beers),
                    user_prompt=user_prompt_text or None,
                )
            )

            metrics.set_tokens(openai_metrics.get("input_tokens", 0), openai_metrics.get("output_tokens", 0))
            metrics.set_cost(openai_metrics.get("cost", 0.0))
            metrics.set_model(openai_metrics.get("model", ""))
            metrics.log_completion()

            logger.info(
                "beer_pairing_completed",
                language=request.language,
                foods=len(request.foods),
                model=openai_metrics.get("model")
            )

            result.setdefault("results", [])
            for item in result["results"]:
                item.setdefault("matches", [])
                matches = item.get("matches") or []

                selection_pool = matches[:min(len(matches), 5)]
                if user_prompt_text:
                    item["matches"] = selection_pool
                else:
                    if len(selection_pool) >= 2:
                        selected = self._rng.sample(selection_pool, min(2, len(selection_pool)))
                        self._rng.shuffle(selected)
                        item["matches"] = selected
                    else:
                        item["matches"] = []

                matches = item.get("matches") or []
                for match in matches:
                    match.setdefault("beer_rating", None)
                    normalized_beer = normalize_beer_name(match.get("beer"))

                    price_value = beer_price_map.get(normalized_beer)
                    match["price"] = price_value if price_value else None

                    rating_value = match.get("beer_rating")
                    rating_is_unknown = (
                        rating_value is None or
                        (isinstance(rating_value, str) and rating_value.strip().lower() == "unknown")
                    )

                    if rating_is_unknown and normalized_beer and self.config.untappd_lookup_enabled:
                        cached_rating = untappd_rating_cache.get(normalized_beer)
                        if cached_rating is None:
                            fetched_rating = await self._lookup_untappd_rating(
                                match.get("beer", ""),
                                model_override=untappd_model
                            )
                            cached_rating = (
                                fetched_rating.strip()
                                if isinstance(fetched_rating, str) and fetched_rating.strip()
                                else "Unknown"
                            )
                            untappd_rating_cache[normalized_beer] = cached_rating

                        if cached_rating and cached_rating.strip().lower() != "unknown":
                            match["beer_rating"] = cached_rating
                        elif match.get("beer_rating") is None:
                            match["beer_rating"] = cached_rating

                if item.get("matches"):
                    item.pop("suggestions", None)
                if item.get("suggestions") is None:
                    item.pop("suggestions", None)

            return {
                "language": result.get("language", request.language),
                "results": result.get("results", [])
            }

        except Exception as exc:
            metrics.set_error(str(exc))
            metrics.log_completion()
            logger.error("beer_pairing_failed", language=request.language, error=str(exc))
            raise BeerPairingException(f"Beer pairing generation failed: {exc}")
