"""Call Claude Sonnet 4.6 with the prepared image + interpretation prompt.

Returns a validated Interpretation. Retries once on schema-validation
failure with a short corrective follow-up; gives up after that.
"""

from __future__ import annotations

import base64
import json
import logging
import time
from pathlib import Path

import anthropic
from pydantic import ValidationError

from ..config import config
from ..schemas import Interpretation


logger = logging.getLogger(__name__)

_PROMPT_PATH = Path(__file__).resolve().parent.parent / "prompts" / "interpretation_v1.md"


def _load_prompt() -> str:
    return _PROMPT_PATH.read_text(encoding="utf-8")


_USER_INSTRUCTIONS = """
You are looking at a 12-lead EKG image plus, sometimes, a rhythm strip below
the 12-lead. Produce a single JSON object that validates against the
Interpretation schema. Do not include markdown code fences. Do not include
prose outside the JSON. Respond with JSON only.

Key constraints:
- schema_version MUST be "1.0".
- All required fields must be present unless image_quality is "poor" and
  you've populated cannot_interpret with a reason.
- Measurements use the units shown in the schema (bpm, ms, degrees, mm).
- Lead names are exactly: I, II, III, aVR, aVL, aVF, V1–V6, rhythm_strip.
- concepts_illustrated must be slugs from the project's concept taxonomy
  (rate_calculation_300_rule, sinus_rhythm_criteria, stemi_recognition_anterior, etc).
""".strip()


_RETRY_INSTRUCTIONS_TEMPLATE = """
Your previous response failed schema validation:

{errors}

Re-emit the SAME JSON with those issues corrected. Do not change the
clinical interpretation — only fix the field shapes that didn't validate.
""".strip()


class InterpretError(Exception):
    pass


class InterpretSchemaError(InterpretError):
    pass


def interpret_image(image_path: str | Path) -> tuple[Interpretation, str, int]:
    """Send the image to Claude and return (interpretation, version_tag, latency_ms).

    version_tag identifies the prompt+model combination so the api can store
    interpretation_version on the case. A schema bump or prompt rewrite
    changes this tag.
    """
    if not config.anthropic_api_key:
        raise InterpretError("ANTHROPIC_API_KEY not configured")

    path = Path(image_path)
    if not path.exists():
        raise InterpretError(f"image not found: {image_path}")

    image_b64 = base64.standard_b64encode(path.read_bytes()).decode("ascii")
    media_type = _guess_media_type(path)

    client = anthropic.Anthropic(
        api_key=config.anthropic_api_key,
        timeout=config.interpretation_timeout_s,
    )
    system_prompt = _load_prompt()

    messages: list[dict] = [
        {
            "role": "user",
            "content": [
                {
                    "type": "image",
                    "source": {
                        "type": "base64",
                        "media_type": media_type,
                        "data": image_b64,
                    },
                },
                {"type": "text", "text": _USER_INSTRUCTIONS},
            ],
        }
    ]

    start = time.monotonic()
    last_error: str | None = None

    for attempt in range(config.interpretation_max_retries + 1):
        response = client.messages.create(
            model=config.anthropic_model,
            max_tokens=4096,
            temperature=0.2,
            system=system_prompt,
            messages=messages,
        )
        text = _collect_text(response)
        try:
            data = _parse_json_or_die(text)
            parsed = Interpretation.model_validate(data)
            latency_ms = int((time.monotonic() - start) * 1000)
            version = f"interpretation_v1+{config.anthropic_model}"
            return parsed, version, latency_ms
        except (ValidationError, ValueError) as e:
            last_error = str(e)
            logger.warning("validation failure on attempt %d: %s", attempt, e)
            if attempt >= config.interpretation_max_retries:
                break
            # Build a corrective follow-up turn.
            messages.append({"role": "assistant", "content": text})
            messages.append(
                {
                    "role": "user",
                    "content": _RETRY_INSTRUCTIONS_TEMPLATE.format(errors=last_error[:1500]),
                }
            )

    raise InterpretSchemaError(f"interpretation failed schema after retries: {last_error}")


def _collect_text(response) -> str:
    parts = []
    for block in response.content:
        if getattr(block, "type", None) == "text":
            parts.append(block.text)
    return "".join(parts).strip()


def _parse_json_or_die(text: str) -> dict:
    # Tolerate accidental markdown fences from the model despite the prompt
    # explicitly forbidding them.
    cleaned = text
    if cleaned.startswith("```"):
        cleaned = cleaned.split("```", 2)[-1]
        if cleaned.startswith("json"):
            cleaned = cleaned[4:]
        if cleaned.endswith("```"):
            cleaned = cleaned[:-3]
    cleaned = cleaned.strip()
    try:
        return json.loads(cleaned)
    except json.JSONDecodeError as e:
        raise ValueError(f"response was not valid JSON: {e}") from e


def _guess_media_type(path: Path) -> str:
    ext = path.suffix.lower()
    if ext in (".jpg", ".jpeg"):
        return "image/jpeg"
    if ext == ".webp":
        return "image/webp"
    if ext == ".gif":
        return "image/gif"
    return "image/png"
