"""LLM-based reranker — reorders candidates by relevance via LLMProvider.

Ported from advisor_api reranker.py (317 lines → LLM-only backend).
Cohere and ONNX backends are anti-scope for v0.

Graceful degradation: if the LLM call fails, returns original ranking unchanged.
"""

import json
import logging

from arandu.config import MemoryConfig
from arandu.protocols import LLMProvider
from arandu.read.retrieval import RetrievalCandidate
from arandu.write.extract import _strip_markdown_fences

logger = logging.getLogger(__name__)


async def rerank_with_llm(
    query: str,
    candidates: list[RetrievalCandidate],
    llm: LLMProvider,
    config: MemoryConfig,
) -> list[RetrievalCandidate]:
    """Rerank candidates using LLM relevance scoring.

    Sends top-N candidates to the LLM for scoring. Falls back to original
    ranking on any failure (timeout, parse error, wrong length).

    Args:
        query: The user's query text.
        candidates: Pre-ranked candidates from retrieval.
        llm: Injected LLM provider.
        config: Memory configuration (reranker_model, topk_facts).

    Returns:
        Reranked candidates list. On failure, returns original list unchanged.
    """
    if not candidates or len(candidates) <= 1:
        return candidates

    # Select pool for reranking (top candidates only)
    pool_size = min(len(candidates), config.topk_facts)
    pool = candidates[:pool_size]

    # Build fact display strings for the prompt
    fact_lines = []
    for i, c in enumerate(pool):
        f = c.fact
        display = f"{f.entity_name or ''}: {f.fact_text or ''}"
        fact_lines.append(f"{i + 1}. {display}")

    facts_block = "\n".join(fact_lines)
    prompt_text = (
        "Rate the relevance of each memory fact to the query on a scale of 0.0 to 1.0.\n"
        'Return ONLY a JSON object: {"scores": [score1, score2, ...]}\n'
        f"The scores array must have exactly {len(pool)} elements, one per fact.\n\n"
        f'Query: "{query}"\n\nFacts:\n{facts_block}'
    )

    try:
        raw = await llm.complete(
            messages=[{"role": "user", "content": prompt_text}],
            temperature=0,
            response_format={"type": "json_object"},
            max_tokens=300,
        )

        raw = _strip_markdown_fences(raw)
        data = json.loads(raw)
        scores = data.get("scores", [])

        if not isinstance(scores, list) or len(scores) != len(pool):
            logger.warning(
                "reranker: unexpected scores length=%s, expected=%s, falling back",
                len(scores) if isinstance(scores, list) else "N/A",
                len(pool),
            )
            return candidates

        # Apply reranker scores
        for i, c in enumerate(pool):
            try:
                c.scores["reranker"] = max(0.0, min(1.0, float(scores[i])))
                c.final_score = c.scores["reranker"]
            except (TypeError, ValueError):
                c.scores["reranker"] = 0.0

        # Sort pool by reranker score descending
        reranked_pool = sorted(pool, key=lambda x: x.scores.get("reranker", 0.0), reverse=True)

        # Build final list: reranked pool + remaining candidates not in pool
        pool_ids = {id(c) for c in pool}
        rest = [c for c in candidates if id(c) not in pool_ids]
        return reranked_pool + rest

    except Exception as e:
        logger.warning("reranker: failed: %s, falling back to first-pass ranking", e)
        return candidates
