"""
LLM reasoning layer.

Production concerns specific to calling an external LLM API:
1. Timeouts — never let one slow LLM call hold a worker indefinitely.
2. Retries — transient network/5xx errors should retry with backoff,
   but only a bounded number of times (LLM_MAX_RETRIES), not forever.
3. Graceful degradation — if the LLM is unreachable after retries, the
   user should still get the raw model numbers with a template-based
   explanation, not a 502 for the entire request. The ML predictions
   are the product; the LLM is a presentation layer on top of them.
4. No hallucination — the system prompt explicitly forbids inventing
   numbers, and the user prompt only contains values that came from the
   aggregator, never free text the model could "fill in."
"""

import time

import anthropic

from app.core.config import get_settings
from app.core.logging_config import get_logger

settings = get_settings()
logger = get_logger(__name__)

SYSTEM_PROMPT = """You are an agricultural decision assistant helping Indian \
farmers and agricultural officers understand machine learning model output \
about crop suitability.

Strict rules you must follow:
- Use ONLY the numbers and labels provided to you. Never invent or estimate \
a number that was not given.
- Do not override or contradict the model outputs. Your job is to explain \
them, not to second-guess them.
- If a model output is marked "degraded" or has null values, say so plainly \
rather than guessing a substitute number.
- If conflicts are listed, mention them honestly — do not smooth over \
genuine uncertainty.
- Use simple, direct language suitable for someone without a technical \
background. Avoid jargon like "anomaly score" without explaining what it means.
- Structure your response in exactly six sections, in this order:
  1. Crop suitability summary
  2. Climate assessment
  3. Expected yield
  4. Market insight
  5. Risk explanation
  6. Alternative crop suggestions
- Keep the whole response under 350 words.
"""


def _build_user_prompt(district: str, crop: str, month: int, model_outputs: dict) -> str:
    return f"""Crop: {crop}
District: {district}
Month: {month}

Model outputs:
- Climate: {model_outputs['climate']}
- Feasibility: {model_outputs['feasibility']}
- Yield: {model_outputs['yield_prediction']}
- Trend: {model_outputs['trend']}
- Market: {model_outputs['market']}
- Alternative crops: {model_outputs['recommendation']}
- Conflicts detected: {model_outputs['conflicts']}
- Models that had issues this run: {model_outputs.get('degraded_models', [])}

Write the six-section explanation now, following the system rules exactly."""


def _fallback_explanation(district: str, crop: str, month: int, model_outputs: dict) -> str:
    """
    Used only if the LLM is unreachable after all retries. A plain
    template ensures the user still receives the actual model results —
    degraded presentation, not a failed request.
    """
    feas = model_outputs["feasibility"]
    yld = model_outputs["yield_prediction"]
    climate = model_outputs["climate"]
    market = model_outputs["market"]
    alts = model_outputs["recommendation"].get("alternatives", [])
    alt_names = ", ".join(a["crop"] for a in alts) if alts else "none available"

    return (
        f"AI explanation service is temporarily unavailable, showing raw model results.\n\n"
        f"1. Crop suitability summary: {crop} in {district} for month {month} is "
        f"classified as '{feas.get('label', 'unknown')}' "
        f"(confidence {feas.get('confidence', 0):.0%}).\n"
        f"2. Climate assessment: forecast rainfall {climate.get('forecast_rainfall_mm')} mm, "
        f"anomaly level: {climate.get('anomaly_label')}.\n"
        f"3. Expected yield: {yld.get('expected_yield_ton_ha')} tonnes/hectare.\n"
        f"4. Market insight: price forecast {market.get('price_forecast_per_quintal')} "
        f"per quintal, demand level: {market.get('demand_level')}.\n"
        f"5. Risk explanation: {'; '.join(model_outputs.get('conflicts', [])) or 'no conflicts detected'}.\n"
        f"6. Alternative crop suggestions: {alt_names}."
    )


class LLMService:
    def __init__(self) -> None:
        self._client: anthropic.Anthropic | None = None

    def init(self) -> None:
        if not settings.ANTHROPIC_API_KEY:
            logger.warning(
                "ANTHROPIC_API_KEY is not set — LLM explanations will use "
                "the fallback template until a key is configured."
            )
            self._client = None
            return
        self._client = anthropic.Anthropic(
            api_key=settings.ANTHROPIC_API_KEY,
            timeout=settings.LLM_TIMEOUT_SECONDS,
        )

    def get_explanation(self, district: str, crop: str, month: int, model_outputs: dict) -> str:
        if self._client is None:
            return _fallback_explanation(district, crop, month, model_outputs)

        user_prompt = _build_user_prompt(district, crop, month, model_outputs)
        last_error: Exception | None = None

        for attempt in range(1, settings.LLM_MAX_RETRIES + 2):
            try:
                response = self._client.messages.create(
                    model=settings.LLM_MODEL,
                    max_tokens=settings.LLM_MAX_TOKENS,
                    system=SYSTEM_PROMPT,
                    messages=[{"role": "user", "content": user_prompt}],
                )
                text_blocks = [b.text for b in response.content if b.type == "text"]
                return "\n".join(text_blocks).strip()

            except anthropic.RateLimitError as exc:
                last_error = exc
                wait = min(2 ** attempt, 10)
                logger.warning(f"LLM rate limited, retrying in {wait}s (attempt {attempt})")
                time.sleep(wait)

            except anthropic.APIConnectionError as exc:
                last_error = exc
                wait = min(2 ** attempt, 10)
                logger.warning(f"LLM connection error, retrying in {wait}s (attempt {attempt})")
                time.sleep(wait)

            except anthropic.APIStatusError as exc:
                last_error = exc
                if 500 <= exc.status_code < 600:
                    wait = min(2 ** attempt, 10)
                    logger.warning(f"LLM server error {exc.status_code}, retrying in {wait}s")
                    time.sleep(wait)
                else:
                    # 4xx errors (bad request, auth failure) will not succeed
                    # on retry — fail fast instead of wasting attempts.
                    logger.error(f"LLM client error {exc.status_code}, not retrying: {exc}")
                    break

        logger.error(f"LLM call failed after retries, using fallback explanation: {last_error}")
        return _fallback_explanation(district, crop, month, model_outputs)


# Single shared instance — initialized once during app startup
llm_service = LLMService()
