"""Anthropic recommender adapter backed by recorded response chunks."""

import asyncio
import json
from collections.abc import AsyncGenerator
from contextvars import ContextVar

from app.adapters.anthropic.llm import (
    IntentOutput,
    RecommendationObjectParser,
    RerankOutput,
    to_intent,
)
from app.adapters.demo.cassette import DemoCassette, load_cassette
from app.adapters.demo.scenario import select_replay_scenario
from app.config import Settings
from app.domain.models import (
    ConversationTurn,
    Intent,
    PreviousRecommendation,
    RerankSelection,
    Track,
)
from app.ports.protocols import RecommenderOutputError


class DemoRecommender:
    """Replay recorded intent and rerank output through live validation."""

    def __init__(self, settings: Settings) -> None:
        """Bind replay pacing and task-local scenario state."""
        self.settings = settings
        self._cassette: ContextVar[DemoCassette | None] = ContextVar(
            "demo_recommender_cassette",
            default=None,
        )

    async def create_intent(
        self,
        query: str,
        history: tuple[ConversationTurn, ...],
        previous_recommendations: tuple[PreviousRecommendation, ...],
        taste_summary: str,
        candidate_count: int,
    ) -> Intent:
        """Parse the selected scenario's recorded structured intent."""
        match = select_replay_scenario(query, bool(previous_recommendations))
        cassette = load_cassette(match.key)
        self._cassette.set(cassette)
        return parse_recorded_intent(cassette.intent_response_body)

    async def stream_rerank(
        self,
        intent: Intent,
        grounded_tracks: tuple[Track, ...],
        taste_summary: str,
        history: tuple[ConversationTurn, ...],
        selection_count: int,
        correction: str | None = None,
    ) -> AsyncGenerator[RerankSelection]:
        """Replay recorded SSE chunks through the live incremental parser."""
        cassette = self._cassette.get()
        if cassette is None:
            raise RecommenderOutputError("Demo rerank has no selected scenario")
        parser = RecommendationObjectParser()
        async for text_delta in _text_deltas(
            cassette.rerank_response_chunks,
            self.settings.demo_chunk_delay_seconds,
        ):
            for selection in parser.feed(text_delta):
                yield RerankSelection(selection.track_id, selection.justification)
        try:
            validated = RerankOutput.model_validate_json(parser.complete_text)
        except ValueError as error:
            raise RecommenderOutputError("Recorded rerank response is invalid") from error
        if len(validated.recommendations) > selection_count:
            raise RecommenderOutputError("Recorded rerank returned too many selections")


def parse_recorded_intent(response_body: bytes) -> Intent:
    """Parse an Anthropic message body through the live intent output model."""
    try:
        response = json.loads(response_body)
        content = response["content"]
        text = content[0]["text"]
        if not isinstance(text, str):
            raise TypeError
        return to_intent(IntentOutput.model_validate_json(text))
    except (IndexError, KeyError, TypeError, ValueError) as error:
        raise RecommenderOutputError("Recorded intent response is invalid") from error


async def _text_deltas(
    chunks: tuple[bytes, ...],
    delay_seconds: float,
) -> AsyncGenerator[str]:
    buffer = b""
    for chunk_index, chunk in enumerate(chunks):
        if chunk_index and delay_seconds > 0:
            await asyncio.sleep(delay_seconds)
        buffer += chunk
        buffer = buffer.replace(b"\r\n", b"\n")
        while b"\n\n" in buffer:
            event, buffer = buffer.split(b"\n\n", 1)
            text_delta = _event_text_delta(event)
            if text_delta is not None:
                yield text_delta
    if buffer:
        text_delta = _event_text_delta(buffer)
        if text_delta is not None:
            yield text_delta


def _event_text_delta(event: bytes) -> str | None:
    data_lines = [line[5:].strip() for line in event.splitlines() if line.startswith(b"data:")]
    if not data_lines:
        return None
    try:
        payload = json.loads(b"\n".join(data_lines))
    except (json.JSONDecodeError, UnicodeDecodeError):
        return None
    if not isinstance(payload, dict) or payload.get("type") != "content_block_delta":
        return None
    delta = payload.get("delta")
    if not isinstance(delta, dict) or delta.get("type") != "text_delta":
        return None
    text = delta.get("text")
    return text if isinstance(text, str) else None
