"""Repository for intelligent evaluation entities."""

import json
from collections.abc import Sequence
from dataclasses import dataclass
from enum import Enum
from typing import Optional

from sqlalchemy import func, literal
from sqlalchemy import insert as sql_insert
from sqlalchemy import update as sql_update
from sqlmodel import Session, select

from agenteval.intelligent_eval.models import (
    IntelligentEval,
    IntelligentEvalMessage,
    IntelligentEvalSession,
    IntelligentEvalSessionStatus,
    IntelligentEvalStatus,
)
from agenteval.storage.db import (
    IntelligentEvalDB,
    IntelligentEvalMessageDB,
    IntelligentEvalSessionDB,
    get_session,
    new_uuid,
    utc_now,
)


class CompareAndSetStatus(str, Enum):
    """Outcome of a conditional intelligent-evaluation write."""

    APPLIED = "applied"
    NOT_FOUND = "not_found"
    CONFLICT = "conflict"


@dataclass(frozen=True)
class CompareAndSetResult:
    """Typed result for a status update guarded by an expected status."""

    status: CompareAndSetStatus
    evaluation: Optional[IntelligentEval] = None

    @property
    def applied(self) -> bool:
        return self.status is CompareAndSetStatus.APPLIED


class IntelligentEvalRepository:
    """CRUD + lifecycle operations for intelligent evaluations."""

    def __init__(self, session: Optional[Session] = None):
        self.session = session or get_session()

    def _to_db(self, obj: IntelligentEval) -> IntelligentEvalDB:
        db = IntelligentEvalDB(
            id=obj.id,
            name=obj.name,
            target_id=obj.target_id,
            status=obj.status.value if isinstance(obj.status, IntelligentEvalStatus) else obj.status,
            goal=obj.goal,
            intent=obj.intent,
            role_description=obj.role_description,
            time_window_hours=obj.time_window_hours,
            plan_feedback=obj.plan_feedback,
            created_at=obj.created_at,
            updated_at=obj.updated_at,
            started_at=obj.started_at,
            completed_at=obj.completed_at,
        )
        db.set_seeds(obj.seeds)
        if obj.plan is not None:
            db.set_plan(obj.plan)
        if obj.report is not None:
            db.set_report(obj.report)
        return db

    def _from_db(self, db: IntelligentEvalDB) -> IntelligentEval:
        return IntelligentEval(
            id=db.id,
            name=db.name,
            target_id=db.target_id,
            status=IntelligentEvalStatus(db.status),
            goal=db.goal,
            seeds=db.get_seeds(),
            intent=db.intent,
            role_description=db.role_description,
            plan=db.get_plan(),
            plan_feedback=db.plan_feedback,
            time_window_hours=db.time_window_hours,
            report=db.get_report(),
            created_at=db.created_at,
            updated_at=db.updated_at,
            started_at=db.started_at,
            completed_at=db.completed_at,
        )

    def list_all(self) -> list[IntelligentEval]:
        statement = select(IntelligentEvalDB).order_by(IntelligentEvalDB.created_at.desc())
        return [self._from_db(r) for r in self.session.exec(statement).all()]

    def get(self, eval_id: str) -> Optional[IntelligentEval]:
        db = self.session.get(IntelligentEvalDB, eval_id)
        return self._from_db(db) if db else None

    def get_with_sessions(
        self,
        eval_id: str,
    ) -> tuple[IntelligentEval, list[IntelligentEvalSession]] | None:
        """Load an evaluation and all session summaries in one DB snapshot."""

        statement = (
            select(IntelligentEvalDB, IntelligentEvalSessionDB)
            .join(
                IntelligentEvalSessionDB,
                IntelligentEvalSessionDB.eval_id == IntelligentEvalDB.id,
                isouter=True,
            )
            .where(IntelligentEvalDB.id == eval_id)
            .order_by(IntelligentEvalSessionDB.created_at.asc())
        )
        rows = self.session.exec(statement).all()
        if not rows:
            return None
        evaluation = self._from_db(rows[0][0])
        session_repo = IntelligentEvalSessionRepository(self.session)
        sessions = [session_repo._from_db(session_db) for _, session_db in rows if session_db is not None]
        return evaluation, sessions

    def _create(self, obj: IntelligentEval) -> IntelligentEval:
        db = self._to_db(obj)
        self.session.add(db)
        self.session.commit()
        self.session.refresh(db)
        return self._from_db(db)

    def _compare_and_set_status(
        self,
        eval_id: str,
        *,
        expected_status: IntelligentEvalStatus,
        new_status: IntelligentEvalStatus,
    ) -> CompareAndSetResult:
        """Atomically transition status only when the expected state still holds."""
        now = utc_now()
        values = {
            "status": new_status.value,
            "updated_at": now,
        }
        if new_status is IntelligentEvalStatus.EXECUTING:
            values["started_at"] = func.coalesce(IntelligentEvalDB.started_at, now)
        if new_status in {
            IntelligentEvalStatus.COMPLETED,
            IntelligentEvalStatus.CANCELLED,
            IntelligentEvalStatus.FAILED,
        }:
            values["completed_at"] = now

        return self._compare_and_set_fields(
            eval_id,
            expected_status=expected_status,
            values=values,
        )

    def _compare_and_set_fields(
        self,
        eval_id: str,
        *,
        expected_status: IntelligentEvalStatus,
        values: dict[str, object],
    ) -> CompareAndSetResult:
        """Apply one conditional SQL update and classify its outcome."""
        try:
            result = self.session.exec(
                sql_update(IntelligentEvalDB)
                .where(
                    IntelligentEvalDB.id == eval_id,
                    IntelligentEvalDB.status == expected_status.value,
                )
                .values(**values)
            )
            if result.rowcount != 1:
                self.session.rollback()
                current = self.session.get(IntelligentEvalDB, eval_id)
                status = CompareAndSetStatus.NOT_FOUND if current is None else CompareAndSetStatus.CONFLICT
                return CompareAndSetResult(status=status)
            self.session.commit()
        except Exception:
            self.session.rollback()
            raise

        db = self.session.get(IntelligentEvalDB, eval_id)
        if db is None:
            return CompareAndSetResult(status=CompareAndSetStatus.NOT_FOUND)
        return CompareAndSetResult(status=CompareAndSetStatus.APPLIED, evaluation=self._from_db(db))

    def _submit_plan_if_planning(self, eval_id: str, plan: dict) -> CompareAndSetResult:
        now = utc_now()
        return self._compare_and_set_fields(
            eval_id,
            expected_status=IntelligentEvalStatus.PLANNING,
            values={
                "status": IntelligentEvalStatus.PENDING_APPROVAL.value,
                "plan": json.dumps(plan, ensure_ascii=False),
                "plan_feedback": None,
                "updated_at": now,
            },
        )

    def _reject_plan_if_pending(self, eval_id: str, feedback: str) -> CompareAndSetResult:
        now = utc_now()
        return self._compare_and_set_fields(
            eval_id,
            expected_status=IntelligentEvalStatus.PENDING_APPROVAL,
            values={
                "status": IntelligentEvalStatus.PLANNING.value,
                "plan_feedback": feedback,
                "updated_at": now,
            },
        )

    def _submit_report_if_executing(self, eval_id: str, report: dict) -> CompareAndSetResult:
        now = utc_now()
        return self._compare_and_set_fields(
            eval_id,
            expected_status=IntelligentEvalStatus.EXECUTING,
            values={
                "status": IntelligentEvalStatus.COMPLETED.value,
                "report": json.dumps(report, ensure_ascii=False),
                "updated_at": now,
                "completed_at": now,
            },
        )

class IntelligentEvalSessionRepository:
    """CRUD for intelligent eval sessions."""

    def __init__(self, session: Optional[Session] = None):
        self.session = session or get_session()

    def _from_db(self, db: IntelligentEvalSessionDB) -> IntelligentEvalSession:
        return IntelligentEvalSession(
            id=db.id,
            eval_id=db.eval_id,
            target_id=db.target_id,
            persona=db.get_persona(),
            goal=db.goal,
            dimension=db.dimension,
            status=IntelligentEvalSessionStatus(db.status),
            verdict=db.get_verdict(),
            turn_count=db.turn_count,
            created_at=db.created_at,
            closed_at=db.closed_at,
        )

    def list_by_eval(self, eval_id: str) -> list[IntelligentEvalSession]:
        statement = (
            select(IntelligentEvalSessionDB)
            .where(IntelligentEvalSessionDB.eval_id == eval_id)
            .order_by(IntelligentEvalSessionDB.created_at.asc())
        )
        return [self._from_db(r) for r in self.session.exec(statement).all()]

    def list_by_evals(self, eval_ids: Sequence[str]) -> dict[str, list[IntelligentEvalSession]]:
        """Load sessions for several evaluations with one query.

        The result contains an empty list for every requested evaluation. This
        keeps read-model assembly deterministic while avoiding one count query
        per row in the evaluation list.
        """

        grouped = {eval_id: [] for eval_id in eval_ids}
        if not grouped:
            return grouped
        statement = (
            select(IntelligentEvalSessionDB)
            .where(IntelligentEvalSessionDB.eval_id.in_(tuple(grouped)))
            .order_by(IntelligentEvalSessionDB.created_at.asc())
        )
        for row in self.session.exec(statement).all():
            grouped.setdefault(row.eval_id, []).append(self._from_db(row))
        return grouped

    def get(self, session_id: str) -> Optional[IntelligentEvalSession]:
        db = self.session.get(IntelligentEvalSessionDB, session_id)
        return self._from_db(db) if db else None

    def _create(self, obj: IntelligentEvalSession) -> IntelligentEvalSession:
        db = IntelligentEvalSessionDB(
            id=obj.id,
            eval_id=obj.eval_id,
            target_id=obj.target_id,
            goal=obj.goal,
            dimension=obj.dimension,
            status=obj.status.value if isinstance(obj.status, IntelligentEvalSessionStatus) else obj.status,
            turn_count=obj.turn_count,
        )
        db.set_persona(obj.persona)
        if obj.verdict is not None:
            db.set_verdict(obj.verdict)
        self.session.add(db)
        self.session.commit()
        self.session.refresh(db)
        return self._from_db(db)

    def _create_if_executing(
        self,
        obj: IntelligentEvalSession,
    ) -> tuple[CompareAndSetStatus, Optional[IntelligentEvalSession]]:
        """Insert a session only while its parent evaluation is executing."""
        session_id = obj.id or new_uuid()
        now = obj.created_at or utc_now()
        statement = sql_insert(IntelligentEvalSessionDB).from_select(
            [
                "id",
                "eval_id",
                "target_id",
                "persona",
                "goal",
                "dimension",
                "status",
                "turn_count",
                "created_at",
            ],
            select(
                literal(session_id),
                IntelligentEvalDB.id,
                IntelligentEvalDB.target_id,
                literal(json.dumps(obj.persona, ensure_ascii=False)),
                literal(obj.goal),
                literal(obj.dimension),
                literal(IntelligentEvalSessionStatus.RUNNING.value),
                literal(0),
                literal(now),
            ).where(
                IntelligentEvalDB.id == obj.eval_id,
                IntelligentEvalDB.status == IntelligentEvalStatus.EXECUTING.value,
            ),
        )
        try:
            result = self.session.exec(statement)
            if result.rowcount != 1:
                self.session.rollback()
                evaluation = self.session.get(IntelligentEvalDB, obj.eval_id)
                status = CompareAndSetStatus.NOT_FOUND if evaluation is None else CompareAndSetStatus.CONFLICT
                return status, None
            self.session.commit()
        except Exception:
            self.session.rollback()
            raise

        db = self.session.get(IntelligentEvalSessionDB, session_id)
        if db is None:
            return CompareAndSetStatus.NOT_FOUND, None
        return CompareAndSetStatus.APPLIED, self._from_db(db)

    def _close_if_running(
        self,
        session_id: str,
        verdict: dict,
        status: IntelligentEvalSessionStatus = IntelligentEvalSessionStatus.COMPLETED,
    ) -> tuple[CompareAndSetStatus, Optional[IntelligentEvalSession]]:
        """Close exactly one running session without overwriting a race winner."""
        now = utc_now()
        try:
            result = self.session.exec(
                sql_update(IntelligentEvalSessionDB)
                .where(
                    IntelligentEvalSessionDB.id == session_id,
                    IntelligentEvalSessionDB.status == IntelligentEvalSessionStatus.RUNNING.value,
                )
                .values(
                    status=status.value,
                    verdict=json.dumps(verdict, ensure_ascii=False),
                    closed_at=now,
                )
            )
            if result.rowcount != 1:
                self.session.rollback()
                current = self.session.get(IntelligentEvalSessionDB, session_id)
                outcome = CompareAndSetStatus.NOT_FOUND if current is None else CompareAndSetStatus.CONFLICT
                return outcome, None
            self.session.commit()
        except Exception:
            self.session.rollback()
            raise

        db = self.session.get(IntelligentEvalSessionDB, session_id)
        if db is None:
            return CompareAndSetStatus.NOT_FOUND, None
        return CompareAndSetStatus.APPLIED, self._from_db(db)

class IntelligentEvalMessageRepository:
    """CRUD for intelligent eval session messages."""

    def __init__(self, session: Optional[Session] = None):
        self.session = session or get_session()

    def _from_db(self, db: IntelligentEvalMessageDB) -> IntelligentEvalMessage:
        return IntelligentEvalMessage(
            id=db.id,
            session_id=db.session_id,
            role=db.role,
            content=db.content,
            latency_ms=db.latency_ms,
            created_at=db.created_at,
        )

    def list_by_session(self, session_id: str) -> list[IntelligentEvalMessage]:
        statement = (
            select(IntelligentEvalMessageDB)
            .where(IntelligentEvalMessageDB.session_id == session_id)
            .order_by(IntelligentEvalMessageDB.created_at.asc())
        )
        return [self._from_db(r) for r in self.session.exec(statement).all()]

    def _create(self, obj: IntelligentEvalMessage) -> IntelligentEvalMessage:
        db = IntelligentEvalMessageDB(
            id=obj.id,
            session_id=obj.session_id,
            role=obj.role,
            content=obj.content,
            latency_ms=obj.latency_ms,
            created_at=obj.created_at,
        )
        self.session.add(db)
        self.session.commit()
        self.session.refresh(db)
        return self._from_db(db)

    def _create_user_and_increment(self, obj: IntelligentEvalMessage) -> CompareAndSetStatus:
        """Persist the sent user message and consume one turn atomically."""
        session_db = self.session.get(IntelligentEvalSessionDB, obj.session_id)
        if session_db is None:
            return CompareAndSetStatus.NOT_FOUND
        if session_db.status != IntelligentEvalSessionStatus.RUNNING.value:
            return CompareAndSetStatus.CONFLICT

        message_db = IntelligentEvalMessageDB(
            id=obj.id,
            session_id=obj.session_id,
            role="user",
            content=obj.content,
            latency_ms=None,
            created_at=obj.created_at,
        )
        try:
            session_db.turn_count += 1
            self.session.add(session_db)
            self.session.add(message_db)
            self.session.commit()
            self.session.refresh(message_db)
        except Exception:
            self.session.rollback()
            raise
        obj.id = message_db.id
        return CompareAndSetStatus.APPLIED
