"""
Classification and theme-detection via Anthropic Haiku.

Uses claude-haiku-4-5 for cheap, fast classification decisions.
Fails fast on API errors — the caller (tool layer) decides whether
to buffer or surface the error.

NOTE: Unit tests mock the Anthropic client at the boundary.
The quality of classification decisions is not unit-testable;
integration tests verify end-to-end behavior against a live model.
"""

from __future__ import annotations

import json

import anthropic

from .models import ClassifyResult, ThemeChangeResult


_MODEL = "claude-haiku-4-5-20251001"
_MAX_TOKENS = 512

_CLASSIFY_SYSTEM = """
You are a classification assistant. You must respond ONLY with a valid
JSON object — no preamble, no explanation, no markdown fences.

Classify the user message into one of two destinations:
- "discussion": exploratory, design-oriented, not immediately actionable
  as a code change
- "issue": directly requires a commit (bug, feature, refactor, rfc)

Categories (only for discussion):
  "Architecture", "UX / UI", "Ideas"

Issue labels (only for issue):
  "bug", "rfc", "feature", "refactor", "chore"

Confidence:
  "high": clear-cut
  "medium": reasonable interpretation
  "low": ambiguous — Claude should use its own judgment

Response schema (strict):
{
  "destination": "discussion" | "issue",
  "category": "Architecture" | "UX / UI" | "Ideas" | null,
  "issue_label": "bug" | "rfc" | "feature" | "refactor" | "chore" | null,
  "suggested_title": "<72 char topic noun phrase, not a question>",
  "confidence": "high" | "medium" | "low"
}
""".strip()

_THEME_SYSTEM = """
You are a thread-analysis assistant. You must respond ONLY with a valid
JSON object — no preamble, no explanation, no markdown fences.

Analyze whether the new message represents a meaningful theme change
from the existing Discussion thread.

A theme change occurs when:
- A distinctly different system, concept, or concern is introduced
- The conversation shifts from design to implementation (or vice versa)
- A second unrelated problem or proposal emerges

Gradual drift within the same topic is NOT a theme change.

Response schema (strict):
{
  "theme_changed": true | false,
  "confidence": "high" | "medium" | "low",
  "fork_point_comment_id": "<comment node ID where theme shifted>" | null,
  "original_theme": "<brief description of original theme>",
  "new_theme": "<brief description of new theme>",
  "suggested_title": "<72 char topic noun phrase for new thread>"
}
""".strip()


class ClassificationError(RuntimeError):
    """Raised when Haiku returns an unparseable or invalid response."""


def _call_haiku(api_key: str, system: str, user_message: str) -> str:
    """
    Call Haiku with the given system prompt and user message.

    Raises ClassificationError if the API call fails or returns
    an empty response.
    """
    client = anthropic.Anthropic(api_key=api_key)

    try:
        message = client.messages.create(
            model=_MODEL,
            max_tokens=_MAX_TOKENS,
            system=system,
            messages=[{"role": "user", "content": user_message}],
        )
    except anthropic.APIError as exc:
        raise ClassificationError(
            f"Anthropic API error during classification: {exc}"
        ) from exc

    if not message.content:
        raise ClassificationError("Haiku returned an empty response.")

    return message.content[0].text


def _parse_json_response(raw: str, model_class: type) -> object:
    """
    Parse a JSON response from Haiku into the given Pydantic model.

    Raises ClassificationError on parse or validation failure.
    """
    # Strip accidental markdown fences
    text = raw.strip()
    if text.startswith("```"):
        lines = text.split("\n")
        text = "\n".join(
            l for l in lines if not l.startswith("```")
        ).strip()

    try:
        data = json.loads(text)
    except json.JSONDecodeError as exc:
        raise ClassificationError(
            f"Haiku response is not valid JSON: {exc}\nRaw: {raw!r}"
        ) from exc

    try:
        return model_class.model_validate(data)
    except Exception as exc:
        raise ClassificationError(
            f"Haiku response failed validation: {exc}\nRaw: {raw!r}"
        ) from exc


def classify_turn(api_key: str, text: str) -> ClassifyResult:
    """
    Classify a single conversation turn.

    Args:
        api_key: Anthropic API key
        text: The user's message text

    Returns ClassifyResult. Raises ClassificationError on failure.
    """
    if not text.strip():
        raise ValueError("text must not be empty.")

    raw = _call_haiku(api_key, _CLASSIFY_SYSTEM, text)
    return _parse_json_response(raw, ClassifyResult)  # type: ignore[return-value]


def detect_theme_change(
    api_key: str,
    thread_comments: list[dict],
    new_turn_text: str,
) -> ThemeChangeResult:
    """
    Detect whether new_turn_text represents a theme change from
    the existing thread_comments.

    Args:
        api_key: Anthropic API key
        thread_comments: list of {"id": str, "body": str, "createdAt": str}
        new_turn_text: the new message to evaluate

    Returns ThemeChangeResult. Raises ClassificationError on failure.
    """
    if not new_turn_text.strip():
        raise ValueError("new_turn_text must not be empty.")

    # Format thread for context — include comment IDs so Haiku can
    # identify the fork point
    thread_text = "\n\n---\n\n".join(
        f"[Comment ID: {c['id']}]\n{c['body']}"
        for c in thread_comments
    )

    user_message = (
        f"EXISTING THREAD:\n\n{thread_text}\n\n"
        f"---\n\nNEW MESSAGE:\n\n{new_turn_text}"
    )

    raw = _call_haiku(api_key, _THEME_SYSTEM, user_message)
    return _parse_json_response(raw, ThemeChangeResult)  # type: ignore[return-value]
