"""Interpretation service (Layer 3). FastAPI + Anthropic SDK.

Receives a path or url pointing to a stored EKG image, runs preprocessing,
calls Claude Sonnet 4.6 with the rubric prompt, validates the response,
returns the canonical Interpretation.
"""

from __future__ import annotations

import logging
import os
import shutil
import tempfile
from pathlib import Path
from urllib.parse import urlparse

import httpx
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

from .config import config
from typing import Any

from .pipelines.ecg_fm_verify import (
    VerifierResult,
    is_available as verifier_available,
    verify_measurements,
)
from .pipelines.image_prep import prepare
from .pipelines.llm_interpret import (
    InterpretError,
    InterpretSchemaError,
    interpret_image,
)
from .schemas import Interpretation

logging.basicConfig(level=os.environ.get("LOG_LEVEL", "INFO"))
logger = logging.getLogger(__name__)

app = FastAPI(title="ekg-tutor-interpret", version="0.1.0")


class InterpretRequest(BaseModel):
    image_url: str
    case_id: str | None = None
    expected_difficulty: int | None = None
    # When set, the verifier runs after the LLM call and (if available)
    # corrects measurements that disagree by more than the field threshold.
    # Path is a file:// URL to a WFDB record root (no extension; wfdb appends
    # .hea / .dat itself).
    signal_url: str | None = None


class InterpretResponseBody(BaseModel):
    interpretation: Interpretation
    interpretation_version: str
    latency_ms: int
    measurements_verified: dict | None = None


@app.get("/health")
async def health() -> dict[str, object]:
    return {
        "ok": True,
        "service": "ekg-tutor-interpret",
        "model_configured": bool(config.anthropic_api_key),
        "model": config.anthropic_model,
        "verifier_available": verifier_available(),
    }


def _resolve_image_path(image_url: str) -> Path:
    """Convert an image_url to a local path.

    Accepted forms:
      - file:///absolute/path
      - /absolute/path (treated as local)
      - https:// for remote images (Phase 2). Pulled to a tempfile.
    """
    parsed = urlparse(image_url)
    if parsed.scheme in ("", "file"):
        candidate = parsed.path or image_url
        path = Path(candidate)
        if not path.is_absolute():
            # Interpret as a key relative to the configured upload dir.
            path = Path(config.upload_dir) / candidate.lstrip("/")
        return path
    if parsed.scheme in ("http", "https"):
        tmp = tempfile.NamedTemporaryFile(suffix=Path(parsed.path).suffix or ".png", delete=False)
        with httpx.Client(timeout=30.0) as client:
            with client.stream("GET", image_url) as r:
                r.raise_for_status()
                with open(tmp.name, "wb") as f:
                    for chunk in r.iter_bytes():
                        f.write(chunk)
        return Path(tmp.name)
    raise HTTPException(status_code=400, detail=f"unsupported url scheme: {parsed.scheme}")


@app.post("/v1/interpret", response_model=InterpretResponseBody)
async def interpret(req: InterpretRequest) -> InterpretResponseBody:
    if not config.anthropic_api_key:
        raise HTTPException(
            status_code=503,
            detail="interpret service has no ANTHROPIC_API_KEY configured",
        )

    src_path = _resolve_image_path(req.image_url)
    if not src_path.exists():
        raise HTTPException(status_code=400, detail=f"image not found: {src_path}")

    with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
        prepared_path = Path(tmp.name)

    try:
        prepared = prepare(src_path)
        prepared.pil.save(prepared_path, format="PNG", optimize=True)
        try:
            interpretation, version, latency_ms = interpret_image(prepared_path)
        except InterpretSchemaError as e:
            logger.error("schema-failure after retries: %s", e)
            raise HTTPException(status_code=502, detail=f"interpretation_schema_failure: {e}") from e
        except InterpretError as e:
            logger.error("interpretation error: %s", e)
            raise HTTPException(status_code=502, detail=str(e)) from e

        # If preprocessing applied steps, fold them into image_quality_notes.
        if prepared.notes and not interpretation.image_quality_notes:
            interpretation = interpretation.model_copy(
                update={"image_quality_notes": "; ".join(prepared.notes)}
            )

        # Optional verifier pass: if a signal_url is provided and the ECG-FM
        # weights are loaded, reconcile the LLM's numeric reads with the
        # model's measurements. The verifier wins above-threshold disagreements;
        # the response carries the per-field comparison so the UI can flag it.
        verifier_payload: dict | None = None
        if req.signal_url:
            signal_path = _resolve_signal_path(req.signal_url)
            llm_nums = {
                "rate": float(interpretation.rate.measurement.value),
                "intervals.pr": float(interpretation.intervals.pr.measurement.value),
                "intervals.qrs": float(interpretation.intervals.qrs.measurement.value),
                "intervals.qtc": float(interpretation.intervals.qtc.measurement.value),
                "axis": float(interpretation.axis.measurement.value),
            }
            try:
                result: VerifierResult = verify_measurements(signal_path, llm_nums)
                verifier_payload = result.to_dict()
                if result.available:
                    interpretation = _apply_verifier_corrections(interpretation, result)
            except NotImplementedError as e:
                # Expected while Phase 4 weights aren't wired.
                logger.info("verifier skipped: %s", e)
                verifier_payload = {
                    "available": False,
                    "skipped_reason": str(e),
                    "model_version": None,
                    "verifications": [],
                }
            except Exception as e:
                logger.warning("verifier failed: %s", e)
                verifier_payload = {
                    "available": False,
                    "skipped_reason": f"verifier_error: {e}",
                    "model_version": None,
                    "verifications": [],
                }

        return InterpretResponseBody(
            interpretation=interpretation,
            interpretation_version=version,
            latency_ms=latency_ms,
            measurements_verified=verifier_payload,
        )
    finally:
        prepared_path.unlink(missing_ok=True)


def _resolve_signal_path(signal_url: str) -> Path:
    parsed = urlparse(signal_url)
    if parsed.scheme in ("", "file"):
        path = Path(parsed.path or signal_url)
        if not path.is_absolute():
            path = Path(config.upload_dir) / (parsed.path or signal_url).lstrip("/")
        return path
    raise HTTPException(status_code=400, detail=f"signal_url scheme not supported: {parsed.scheme}")


def _apply_verifier_corrections(
    interpretation: Interpretation,
    verifier: VerifierResult,
) -> Interpretation:
    """Mutate the interpretation in place where the verifier disagrees by
    more than the configured threshold. Threshold-bounded so we don't churn
    on noise — see THRESHOLDS in ecg_fm_verify.py."""
    overrides: dict[str, Any] = {}
    for v in verifier.verifications:
        if not v.correction_applied:
            continue
        if v.field == "rate":
            overrides["rate"] = interpretation.rate.model_copy(
                update={
                    "measurement": interpretation.rate.measurement.model_copy(
                        update={"value": v.verifier_value}
                    )
                }
            )
        elif v.field.startswith("intervals."):
            interval_key = v.field.split(".", 1)[1]
            current = getattr(interpretation.intervals, interval_key)
            updated = current.model_copy(
                update={
                    "measurement": current.measurement.model_copy(
                        update={"value": v.verifier_value}
                    )
                }
            )
            overrides["intervals"] = interpretation.intervals.model_copy(
                update={interval_key: updated}
            )
        elif v.field == "axis":
            overrides["axis"] = interpretation.axis.model_copy(
                update={
                    "measurement": interpretation.axis.measurement.model_copy(
                        update={"value": v.verifier_value}
                    )
                }
            )
    if overrides:
        return interpretation.model_copy(update=overrides)
    return interpretation
