"""Read projections for intelligent evaluations.

The lifecycle module owns writes and state transitions.  This module is the
read seam used by HTTP adapters: it turns domain entities into stable,
purpose-specific projections and keeps session-count/detail assembly out of
routers.  Message bodies are intentionally not part of the detail projection;
transcripts remain available through the dedicated messages endpoint.
"""

from collections.abc import Sequence
from datetime import datetime
from typing import Any

from pydantic import BaseModel, Field
from sqlmodel import Session

from agenteval.intelligent_eval.models import (
    IntelligentEval,
    IntelligentEvalMessage,
    IntelligentEvalSession,
    IntelligentEvalSessionStatus,
    IntelligentEvalStatus,
)
from agenteval.intelligent_eval.repository import (
    IntelligentEvalMessageRepository,
    IntelligentEvalRepository,
    IntelligentEvalSessionRepository,
)


class IntelligentEvalProjection(BaseModel):
    """Fields shared by list and detail representations."""

    id: str
    name: str
    target_id: str
    status: IntelligentEvalStatus
    goal: str
    seeds: dict[str, Any] = Field(default_factory=dict)
    intent: str
    role_description: str
    plan: dict[str, Any] | None = None
    plan_feedback: str | None = None
    time_window_hours: int
    report: dict[str, Any] | None = None
    created_at: datetime | None = None
    updated_at: datetime | None = None
    started_at: datetime | None = None
    completed_at: datetime | None = None


class IntelligentEvalListItem(IntelligentEvalProjection):
    """Compact projection used by collection and mutation responses."""

    session_count: int = 0
    completed_sessions: int = 0


class IntelligentEvalSessionSummary(BaseModel):
    """Session metadata shown in an evaluation detail view.

    This deliberately contains no chat messages.  Callers that need a
    transcript must use ``GET /{eval_id}/sessions/{session_id}/messages``.
    """

    id: str
    eval_id: str
    target_id: str
    persona: dict[str, Any] = Field(default_factory=dict)
    goal: str
    dimension: str | None = None
    status: IntelligentEvalSessionStatus
    verdict: dict[str, Any] | None = None
    turn_count: int = 0
    created_at: datetime | None = None
    closed_at: datetime | None = None


class IntelligentEvalDetail(IntelligentEvalListItem):
    """Full evaluation projection, including session metadata only."""

    sessions: list[IntelligentEvalSessionSummary] = Field(default_factory=list)


def _require_id(eval_obj: IntelligentEval) -> str:
    if eval_obj.id is None:
        raise ValueError("intelligent evaluation projection requires an id")
    return eval_obj.id


def _base_fields(eval_obj: IntelligentEval) -> dict[str, Any]:
    """Copy domain data into the stable projection field set."""

    return {
        "id": _require_id(eval_obj),
        "name": eval_obj.name,
        "target_id": eval_obj.target_id,
        "status": eval_obj.status,
        "goal": eval_obj.goal,
        "seeds": eval_obj.seeds,
        "intent": eval_obj.intent,
        "role_description": eval_obj.role_description,
        "plan": eval_obj.plan,
        "plan_feedback": eval_obj.plan_feedback,
        "time_window_hours": eval_obj.time_window_hours,
        "report": eval_obj.report,
        "created_at": eval_obj.created_at,
        "updated_at": eval_obj.updated_at,
        "started_at": eval_obj.started_at,
        "completed_at": eval_obj.completed_at,
    }


def _session_summary(session_obj: IntelligentEvalSession) -> IntelligentEvalSessionSummary:
    if session_obj.id is None:
        raise ValueError("intelligent evaluation session projection requires an id")
    return IntelligentEvalSessionSummary.model_validate(session_obj.model_dump())


def project_list_item(
    eval_obj: IntelligentEval,
    sessions: Sequence[IntelligentEvalSession],
) -> IntelligentEvalListItem:
    """Build a list projection from an evaluation and its session metadata."""

    return IntelligentEvalListItem(
        **_base_fields(eval_obj),
        session_count=len(sessions),
        completed_sessions=sum(1 for item in sessions if item.status is IntelligentEvalSessionStatus.COMPLETED),
    )


def project_detail(
    eval_obj: IntelligentEval,
    sessions: Sequence[IntelligentEvalSession],
) -> IntelligentEvalDetail:
    """Build a detail projection without embedding transcript messages."""

    item = project_list_item(eval_obj, sessions)
    return IntelligentEvalDetail(
        **item.model_dump(),
        sessions=[_session_summary(item) for item in sessions],
    )


class IntelligentEvalReadModel:
    """Read interface for stable intelligent-evaluation projections.

    The current implementation uses the existing session repository.  Query
    batching can be added behind this seam without changing Router callers.
    """

    def __init__(self, session: Session):
        self._evals = IntelligentEvalRepository(session)
        self._sessions = IntelligentEvalSessionRepository(session)
        self._messages = IntelligentEvalMessageRepository(session)

    def list_item(self, eval_obj: IntelligentEval) -> IntelligentEvalListItem:
        return project_list_item(eval_obj, self._sessions.list_by_eval(_require_id(eval_obj)))

    def list_items(self, evals: Sequence[IntelligentEval]) -> list[IntelligentEvalListItem]:
        """Project a collection after one batched session lookup."""

        sessions_by_eval = self._sessions.list_by_evals([_require_id(item) for item in evals])
        return [project_list_item(item, sessions_by_eval[item.id]) for item in evals if item.id is not None]

    def detail_by_id(self, eval_id: str) -> IntelligentEvalDetail | None:
        """Read one evaluation and its summaries with one snapshot query."""

        snapshot = self._evals.get_with_sessions(eval_id)
        return project_detail(*snapshot) if snapshot is not None else None

    def sessions_by_eval(self, eval_id: str) -> list[IntelligentEvalSession] | None:
        """Return session summaries, or ``None`` when the parent is unknown."""

        if self._evals.get(eval_id) is None:
            return None
        return self._sessions.list_by_eval(eval_id)

    def messages_by_session(self, eval_id: str, session_id: str) -> list[IntelligentEvalMessage] | None:
        """Read a transcript only when the session belongs to the evaluation."""

        if self._evals.get(eval_id) is None:
            return None
        session_obj = self._sessions.get(session_id)
        if session_obj is None or session_obj.eval_id != eval_id:
            return None
        return self._messages.list_by_session(session_id)

    def report_by_eval(self, eval_id: str) -> tuple[str, dict[str, Any]] | None:
        """Return the report payload with its evaluation name for renderers."""

        eval_obj = self._evals.get(eval_id)
        if eval_obj is None or eval_obj.report is None:
            return None
        return eval_obj.name, eval_obj.report
