"""Shared Anthropic helpers for blog services."""

import json
import logging
import re
from typing import Any

import anthropic
from django.conf import settings

logger = logging.getLogger(__name__)


def get_anthropic_client() -> anthropic.Anthropic:
    key = getattr(settings, "ANTHROPIC_API_KEY", "")
    if not key:
        raise RuntimeError("ANTHROPIC_API_KEY not configured")
    return anthropic.Anthropic(api_key=key)


def get_model() -> str:
    return getattr(settings, "ANTHROPIC_MODEL", "claude-sonnet-4-20250514")


def _strip_fences(text: str) -> str:
    text = text.strip()
    if text.startswith("```"):
        text = re.sub(r"^```(?:json)?\s*", "", text)
        text = re.sub(r"\s*```$", "", text)
    return text.strip()


def _extract_balanced_json(text: str, opener: str) -> str | None:
    """Return substring of first balanced JSON array or object."""
    closer = "]" if opener == "[" else "}"
    start = text.find(opener)
    if start < 0:
        return None
    depth = 0
    in_string = False
    escape = False
    for i in range(start, len(text)):
        ch = text[i]
        if escape:
            escape = False
            continue
        if ch == "\\" and in_string:
            escape = True
            continue
        if ch == '"':
            in_string = not in_string
            continue
        if in_string:
            continue
        if ch == opener:
            depth += 1
        elif ch == closer:
            depth -= 1
            if depth == 0:
                return text[start : i + 1]
    return None


def _normalize_json_text(text: str) -> str:
    """Light cleanup before json.loads."""
    text = text.replace("\u201c", '"').replace("\u201d", '"')
    text = text.replace("\u2018", "'").replace("\u2019", "'")
    text = re.sub(r",\s*([\]}])", r"\1", text)
    return text


def loads_json_lenient(text: str, *, expect_array: bool = True) -> Any:
    """Parse Claude JSON with extraction and cleanup; raises JSONDecodeError if hopeless."""
    raw = _strip_fences(text)
    candidates: list[str] = [raw]
    if expect_array:
        block = _extract_balanced_json(raw, "[")
        if block:
            candidates.append(block)
    block_obj = _extract_balanced_json(raw, "{")
    if block_obj:
        candidates.append(block_obj)

    last_err: json.JSONDecodeError | None = None
    seen: set[str] = set()
    for candidate in candidates:
        if not candidate or candidate in seen:
            continue
        seen.add(candidate)
        normalized = _normalize_json_text(candidate)
        try:
            return json.loads(normalized)
        except json.JSONDecodeError as exc:
            last_err = exc
            continue
    if last_err:
        raise last_err
    raise json.JSONDecodeError("No JSON found in model response", text, 0)


def _repair_json_via_claude(raw_text: str, *, expect_array: bool) -> str:
    kind = "JSON array" if expect_array else "JSON object"
    client = get_anthropic_client()
    resp = client.messages.create(
        model=get_model(),
        max_tokens=8000,
        system=(
            f"You fix invalid {kind}. Return ONLY valid {kind}, no markdown, "
            "no commentary. Escape double quotes inside strings."
        ),
        messages=[
            {
                "role": "user",
                "content": f"Fix this invalid JSON:\n\n{raw_text[:20000]}",
            }
        ],
        temperature=0.0,
    )
    return resp.content[0].text


def parse_json_array(text: str) -> list:
    try:
        data = loads_json_lenient(text, expect_array=True)
    except json.JSONDecodeError:
        logger.warning("JSON array parse failed, asking Claude to repair")
        repaired = _repair_json_via_claude(text, expect_array=True)
        data = loads_json_lenient(repaired, expect_array=True)

    if isinstance(data, dict):
        for key in ("posts", "articles", "items", "topics", "results"):
            if isinstance(data.get(key), list):
                data = data[key]
                break
    if not isinstance(data, list):
        raise ValueError("Expected JSON array from Claude")
    return data


def parse_json_object(text: str) -> dict:
    try:
        data = loads_json_lenient(text, expect_array=False)
    except json.JSONDecodeError:
        logger.warning("JSON object parse failed, asking Claude to repair")
        repaired = _repair_json_via_claude(text, expect_array=False)
        data = loads_json_lenient(repaired, expect_array=False)

    if isinstance(data, list) and data:
        data = data[0]
    if not isinstance(data, dict):
        raise ValueError("Expected JSON object from Claude")
    return data


def claude_json(
    system: str,
    user: str,
    *,
    max_tokens: int = 4000,
    temperature: float = 0.5,
) -> list:
    client = get_anthropic_client()
    resp = client.messages.create(
        model=get_model(),
        max_tokens=max_tokens,
        system=system + " Escape double quotes inside JSON string values.",
        messages=[{"role": "user", "content": user}],
        temperature=temperature,
    )
    return parse_json_array(resp.content[0].text)


def claude_json_object(
    system: str,
    user: str,
    *,
    max_tokens: int = 2000,
    temperature: float = 0.3,
) -> dict:
    client = get_anthropic_client()
    resp = client.messages.create(
        model=get_model(),
        max_tokens=max_tokens,
        system=system + " Escape double quotes inside JSON string values.",
        messages=[{"role": "user", "content": user}],
        temperature=temperature,
    )
    return parse_json_object(resp.content[0].text)
