"""Decode recorded Spotify and Anthropic responses for demo replay."""

import base64
import json
from dataclasses import dataclass
from functools import cache
from pathlib import Path

from app.adapters.demo.scenario import FIXTURE_ROOT


@dataclass(frozen=True)
class RecordedResponse:
    """One decoded response retained from a recorded HTTP interaction."""

    method: str
    url: str
    status: int
    response_body: bytes
    response_chunks: tuple[bytes, ...]

    def json_body(self) -> object:
        """Parse the decoded response body as JSON."""
        return json.loads(self.response_body)


@dataclass(frozen=True)
class DemoCassette:
    """The recorded service responses needed by one demo scenario."""

    key: str
    spotify_responses: tuple[RecordedResponse, ...]
    intent_response_bodies: tuple[bytes, ...]
    rerank_response_chunks: tuple[bytes, ...]

    @property
    def intent_response_body(self) -> bytes:
        """Return the final intent response recorded for this scenario."""
        return self.intent_response_bodies[-1]

    @property
    def spotify_search_responses(self) -> tuple[RecordedResponse, ...]:
        """Return recorded Spotify search responses in capture order."""
        return tuple(response for response in self.spotify_responses if "/search?" in response.url)

    @property
    def spotify_taste_responses(self) -> tuple[RecordedResponse, ...]:
        """Return recorded Spotify taste responses in capture order."""
        return tuple(
            response
            for response in self.spotify_responses
            if any(
                endpoint in response.url
                for endpoint in ("/me/top/artists", "/me/top/tracks", "/me/tracks?")
            )
        )


@cache
def load_cassette(scenario_key: str) -> DemoCassette:
    """Load and decode both service cassettes for one scenario."""
    scenario_root = FIXTURE_ROOT / scenario_key
    spotify = _load_responses(scenario_root / "spotify.json")
    anthropic = _load_responses(scenario_root / "anthropic.json")
    intent_responses = tuple(
        response for response in anthropic if _is_anthropic_message(response.response_body)
    )
    rerank_responses = tuple(
        response for response in anthropic if not _is_anthropic_message(response.response_body)
    )
    if not spotify or not intent_responses or not rerank_responses:
        raise ValueError(f"Scenario cassette is incomplete: {scenario_key}")
    return DemoCassette(
        key=scenario_key,
        spotify_responses=spotify,
        intent_response_bodies=tuple(response.response_body for response in intent_responses),
        rerank_response_chunks=rerank_responses[-1].response_chunks,
    )


def _load_responses(path: Path) -> tuple[RecordedResponse, ...]:
    payload = json.loads(path.read_text(encoding="ascii"))
    if not isinstance(payload, list):
        raise ValueError(f"Cassette must contain a response list: {path}")
    return tuple(_decode_response(entry) for entry in payload)


def _decode_response(payload: object) -> RecordedResponse:
    if not isinstance(payload, dict):
        raise ValueError("Cassette response must be a mapping")
    try:
        chunks_value = payload["response_chunks_base64"]
        if not isinstance(chunks_value, list):
            raise TypeError
        chunks = tuple(_decode_base64(value) for value in chunks_value)
        return RecordedResponse(
            method=_string(payload["method"]),
            url=_string(payload["url"]),
            status=_integer(payload["status"]),
            response_body=_decode_base64(payload["response_body_base64"]),
            response_chunks=chunks,
        )
    except (KeyError, TypeError, ValueError) as error:
        raise ValueError("Cassette response is invalid") from error


def _decode_base64(value: object) -> bytes:
    return base64.b64decode(_string(value), validate=True)


def _string(value: object) -> str:
    if not isinstance(value, str):
        raise TypeError
    return value


def _integer(value: object) -> int:
    if not isinstance(value, int):
        raise TypeError
    return value


def _is_anthropic_message(body: bytes) -> bool:
    try:
        payload = json.loads(body)
    except (json.JSONDecodeError, UnicodeDecodeError):
        return False
    return isinstance(payload, dict) and payload.get("type") == "message"
