"""Extract and assess upstream pagination signals.

Detect pagination cursors, offsets, page numbers, or arbitrary
next-parameter maps in upstream tool responses using per-upstream
configuration and heuristic discovery.  Exports
``assess_pagination`` (canonical assessment) and
``extract_pagination_state`` (compatibility wrapper).
"""

from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass
from typing import Any

from sift_gateway.config.settings import PaginationConfig
from sift_gateway.pagination.contract import (
    PAGINATION_WARNING_INCOMPLETE_RESULT_SET,
    RETRIEVAL_STATUS_COMPLETE,
    RETRIEVAL_STATUS_PARTIAL,
    UPSTREAM_PARTIAL_REASON_MORE_PAGES_AVAILABLE,
    UPSTREAM_PARTIAL_REASON_NEXT_TOKEN_MISSING,
    UPSTREAM_PARTIAL_REASON_SIGNAL_INCONCLUSIVE,
    RetrievalStatus,
    UpstreamPartialReason,
)
from sift_gateway.pagination.discovery import discover_pagination
from sift_gateway.pagination.path_eval import evaluate_path as _evaluate_path


@dataclass(frozen=True)
class PaginationState:
    """Immutable pagination state stored alongside an artifact.

    Contains everything needed to construct the next upstream
    call when the LLM invokes ``artifact.next_page``.

    Attributes:
        upstream_prefix: Namespace prefix of the upstream.
        tool_name: Original upstream tool name (unqualified).
        original_args: Arguments from the original tool call
            (without gateway-reserved keys).
        next_params: Parameter overrides for the next call
            (e.g. ``{"after": "QVFIU..."}``).
        page_number: Zero-based page number of this artifact.
    """

    upstream_prefix: str
    tool_name: str
    original_args: dict[str, Any]
    next_params: dict[str, Any]
    page_number: int

    def to_dict(self) -> dict[str, Any]:
        """Serialize to a JSON-safe dict for DB storage.

        Returns:
            Dict representation of the pagination state.
        """
        return {
            "upstream_prefix": self.upstream_prefix,
            "tool_name": self.tool_name,
            "original_args": self.original_args,
            "next_params": self.next_params,
            "page_number": self.page_number,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> PaginationState:
        """Deserialize from a stored dict.

        Args:
            data: Dict as produced by ``to_dict``.

        Returns:
            Reconstructed ``PaginationState``.
        """
        return cls(
            upstream_prefix=str(data["upstream_prefix"]),
            tool_name=str(data["tool_name"]),
            original_args=dict(data.get("original_args") or {}),
            next_params=dict(data.get("next_params") or {}),
            page_number=int(data.get("page_number", 0)),
        )


@dataclass(frozen=True)
class PaginationAssessment:
    """Canonical pagination assessment for one upstream response.

    Attributes:
        state: Next-page state when a follow-up call can be issued.
        has_more: Whether additional records are known to exist.
        retrieval_status: Completion status (``PARTIAL`` or
            ``COMPLETE``).
        partial_reason: Optional reason when status is PARTIAL.
        warning: Optional warning code (e.g.
            ``INCOMPLETE_RESULT_SET``).
        page_number: Zero-based page number for this response.
        detector: Optional detector diagnostics to expose in response
            pagination metadata.
    """

    state: PaginationState | None
    has_more: bool
    retrieval_status: RetrievalStatus
    partial_reason: UpstreamPartialReason | None
    warning: str | None
    page_number: int
    detector: dict[str, Any] | None = None


def _discovery_detector_payload(discovered: Any) -> dict[str, Any]:
    """Build compact diagnostics payload for discovery-mode decisions."""
    payload: dict[str, Any] = {
        "mode": "discovery",
        "limit_hit": bool(getattr(discovered, "limit_hit", False)),
    }
    strategy = getattr(discovered, "strategy", None)
    if isinstance(strategy, str) and strategy:
        payload["strategy"] = strategy
    confidence = getattr(discovered, "confidence", None)
    if isinstance(confidence, (int, float)):
        payload["confidence"] = float(confidence)
    rejected_reason = getattr(discovered, "rejected_reason", None)
    if isinstance(rejected_reason, str) and rejected_reason:
        payload["rejected_reason"] = rejected_reason
    rejected_param = getattr(discovered, "rejected_param", None)
    if isinstance(rejected_param, str) and rejected_param:
        payload["rejected_param"] = rejected_param
    return payload


def _has_more(
    json_value: Any,
    has_more_path: str | None,
) -> bool:
    """Check whether more pages exist.

    When ``has_more_path`` is configured, evaluate it and
    check for a truthy, non-empty value.  When not configured,
    assume more pages exist (caller decides).

    Args:
        json_value: The JSON content value from the response.
        has_more_path: Optional JSONPath for the has-more signal.

    Returns:
        True if more pages likely exist.
    """
    if not has_more_path:
        return True
    val = _evaluate_path(json_value, has_more_path)
    if val is None:
        return False
    if isinstance(val, str) and not val.strip():
        return False
    if isinstance(val, bool):
        return val
    return not (isinstance(val, list) and len(val) == 0)


def _has_more_signal(
    json_value: Any,
    has_more_path: str | None,
) -> bool | None:
    """Return has-more signal or ``None`` when signal is unknown.

    Args:
        json_value: The JSON content value from the response.
        has_more_path: Optional JSONPath for has-more signal.

    Returns:
        ``True`` or ``False`` when a signal path is configured,
        otherwise ``None``.
    """
    if not has_more_path:
        return None
    return _has_more(json_value, has_more_path)


def _next_params_cursor(
    *,
    json_value: Any,
    pagination_config: PaginationConfig,
    original_args: dict[str, Any],
) -> dict[str, Any] | None:
    del original_args
    cursor_value = _evaluate_path(
        json_value,
        pagination_config.cursor_response_path or "",
    )
    if cursor_value is None:
        return None
    if isinstance(cursor_value, str) and not cursor_value.strip():
        return None
    return {pagination_config.cursor_param_name or "after": cursor_value}


def _next_params_offset(
    *,
    json_value: Any,
    pagination_config: PaginationConfig,
    original_args: dict[str, Any],
) -> dict[str, Any] | None:
    del json_value
    param = pagination_config.offset_param_name or "offset"
    size_param = pagination_config.page_size_param_name or "limit"
    current_offset = original_args.get(param, 0)
    if not isinstance(current_offset, int):
        current_offset = 0
    page_size = original_args.get(size_param, 0)
    if not isinstance(page_size, int) or page_size <= 0:
        return None
    return {param: current_offset + page_size}


def _next_params_page_number(
    *,
    json_value: Any,
    pagination_config: PaginationConfig,
    original_args: dict[str, Any],
) -> dict[str, Any] | None:
    del json_value
    param = pagination_config.page_param_name or "page"
    current_page = original_args.get(param, 1)
    if not isinstance(current_page, int):
        current_page = 1
    return {param: current_page + 1}


def _next_params_param_map(
    *,
    json_value: Any,
    pagination_config: PaginationConfig,
    original_args: dict[str, Any],
) -> dict[str, Any] | None:
    del original_args
    raw_map = pagination_config.next_params_response_paths or {}
    if not raw_map:
        return None
    next_params: dict[str, Any] = {}
    for param_name, response_path in raw_map.items():
        value = _evaluate_path(json_value, response_path)
        if value is None:
            return None
        if isinstance(value, str) and not value.strip():
            return None
        next_params[param_name] = value
    return next_params


def _build_next_params(
    *,
    strategy: str,
    json_value: Any,
    pagination_config: PaginationConfig,
    original_args: dict[str, Any],
) -> dict[str, Any] | None:
    """Build next-page argument overrides for a pagination strategy.

    Args:
        strategy: Configured pagination strategy.
        json_value: JSON content value from the response.
        pagination_config: Pagination config for this upstream.
        original_args: Original forwarded arguments.

    Returns:
        Dict of next-page arg overrides, or ``None`` when no
        follow-up request can be built.
    """
    strategy_builders: dict[
        str,
        Callable[..., dict[str, Any] | None],
    ] = {
        "cursor": _next_params_cursor,
        "offset": _next_params_offset,
        "page_number": _next_params_page_number,
        "param_map": _next_params_param_map,
    }
    builder = strategy_builders.get(strategy)
    if builder is None:
        return None
    return builder(
        json_value=json_value,
        pagination_config=pagination_config,
        original_args=original_args,
    )


def assess_pagination(
    *,
    json_value: Any,
    pagination_config: PaginationConfig | None,
    original_args: dict[str, Any],
    upstream_prefix: str,
    tool_name: str,
    upstream_meta: Any | None = None,
    page_number: int = 0,
) -> PaginationAssessment | None:
    """Assess pagination completion and next-page availability.

    Args:
        json_value: The JSON content value from the upstream
            response envelope.
        pagination_config: Optional per-upstream pagination settings.
            In the MCP gateway path this comes from
            ``UpstreamConfig.pagination``.  Non-MCP callers (such as
            a CLI adapter) can pass ``None`` to use discovery-only mode.
        original_args: Original tool call arguments (reserved
            keys already stripped).
        upstream_prefix: Upstream namespace prefix.
        tool_name: Unqualified upstream tool name.
        upstream_meta: Optional upstream metadata payload for
            additional discovery signals (e.g. HTTP headers).
        page_number: Zero-based page number of the current
            artifact (0 for the initial call).

    Returns:
        A canonical ``PaginationAssessment``, or ``None`` when no
        pagination evidence is available.
    """
    has_more_signal: bool | None
    next_params: dict[str, Any] | None
    if pagination_config is None:
        # No configured strategy: use transport-agnostic discovery.
        discovered = discover_pagination(
            json_value=json_value,
            original_args=original_args,
            upstream_meta=upstream_meta,
        )
        detector_payload = _discovery_detector_payload(discovered)
        has_more_signal = discovered.has_more
        next_params = discovered.next_params
        # Explicit terminal signal should always mark completion,
        # even when no next params can be derived.
        if has_more_signal is False:
            return PaginationAssessment(
                state=None,
                has_more=False,
                retrieval_status=RETRIEVAL_STATUS_COMPLETE,
                partial_reason=None,
                warning=None,
                page_number=page_number,
                detector=detector_payload,
            )
        if next_params is None:
            if has_more_signal is True:
                return PaginationAssessment(
                    state=None,
                    has_more=False,
                    retrieval_status=RETRIEVAL_STATUS_PARTIAL,
                    partial_reason=UPSTREAM_PARTIAL_REASON_NEXT_TOKEN_MISSING,
                    warning=PAGINATION_WARNING_INCOMPLETE_RESULT_SET,
                    page_number=page_number,
                    detector=detector_payload,
                )
            # Keep page-0 behavior conservative (no warnings or
            # completeness claim when no signal exists at all), but on
            # follow-up pages always emit an assessment so stale
            # pagination state is cleared from merged responses.
            if page_number == 0:
                return None
            return PaginationAssessment(
                state=None,
                has_more=False,
                retrieval_status=RETRIEVAL_STATUS_COMPLETE,
                partial_reason=None,
                warning=None,
                page_number=page_number,
                detector=detector_payload,
            )
    else:
        # MCP-configured strategy path: honor explicit config.
        has_more_signal = _has_more_signal(
            json_value,
            pagination_config.has_more_response_path,
        )
        # Explicit terminal signal from configured upstream.
        if has_more_signal is False:
            return PaginationAssessment(
                state=None,
                has_more=False,
                retrieval_status=RETRIEVAL_STATUS_COMPLETE,
                partial_reason=None,
                warning=None,
                page_number=page_number,
            )
        next_params = _build_next_params(
            strategy=pagination_config.strategy,
            json_value=json_value,
            pagination_config=pagination_config,
            original_args=original_args,
        )

    if next_params is not None:
        state = PaginationState(
            upstream_prefix=upstream_prefix,
            tool_name=tool_name,
            original_args=original_args,
            next_params=next_params,
            page_number=page_number,
        )
        return PaginationAssessment(
            state=state,
            has_more=True,
            retrieval_status=RETRIEVAL_STATUS_PARTIAL,
            partial_reason=UPSTREAM_PARTIAL_REASON_MORE_PAGES_AVAILABLE,
            warning=PAGINATION_WARNING_INCOMPLETE_RESULT_SET,
            page_number=page_number,
        )

    partial_reason: UpstreamPartialReason = (
        UPSTREAM_PARTIAL_REASON_SIGNAL_INCONCLUSIVE
    )
    if has_more_signal is True:
        partial_reason = UPSTREAM_PARTIAL_REASON_NEXT_TOKEN_MISSING

    if pagination_config is None:
        return None

    return PaginationAssessment(
        state=None,
        has_more=False,
        retrieval_status=RETRIEVAL_STATUS_PARTIAL,
        partial_reason=partial_reason,
        warning=PAGINATION_WARNING_INCOMPLETE_RESULT_SET,
        page_number=page_number,
    )


def extract_pagination_state(
    *,
    json_value: Any,
    pagination_config: PaginationConfig | None,
    original_args: dict[str, Any],
    upstream_prefix: str,
    tool_name: str,
    upstream_meta: Any | None = None,
    page_number: int = 0,
) -> PaginationState | None:
    """Extract next-page state from an upstream response.

    Inspects the JSON content value of an upstream response
    using the configured pagination strategy and JSONPath
    mappings.  This compatibility wrapper returns only the
    next-page state and omits canonical status fields.

    Args:
        json_value: The JSON content value from the upstream
            response envelope.
        pagination_config: Optional per-upstream pagination settings.
        original_args: Original tool call arguments (reserved
            keys already stripped).
        upstream_prefix: Upstream namespace prefix.
        tool_name: Unqualified upstream tool name.
        upstream_meta: Optional upstream metadata payload for
            additional discovery signals (e.g. HTTP headers).
        page_number: Zero-based page number of the current
            artifact (0 for the initial call).

    Returns:
        A ``PaginationState`` with ``next_params`` populated,
        or ``None`` when a follow-up call cannot be issued.
    """
    assessment = assess_pagination(
        json_value=json_value,
        pagination_config=pagination_config,
        original_args=original_args,
        upstream_prefix=upstream_prefix,
        tool_name=tool_name,
        upstream_meta=upstream_meta,
        page_number=page_number,
    )
    if assessment is None:
        return None
    return assessment.state
