# #region DatasetReviewSessionRepository [C:5] [TYPE Module] [SEMANTICS dataset, repository, session, aggregate, persistence]
# @BRIEF Persist and retrieve dataset review session aggregates, including readiness, findings, semantic decisions, clarification state, previews, and run contexts.
# @LAYER Domain
# @RELATION DEPENDS_ON -> [DatasetReviewSession]
# @RELATION DEPENDS_ON -> [DatasetProfile]
# @RELATION DEPENDS_ON -> [ValidationFinding]
# @RELATION DEPENDS_ON -> [CompiledPreview]
# @RELATION DISPATCHES -> [SessionRepositoryMutations]
# @PRE  repository operations execute within authenticated request or task scope.
# @POST  session aggregate reads are structurally consistent and writes preserve ownership and version semantics.
# @SIDE_EFFECT  reads and writes SQLAlchemy-backed session aggregates.
# @DATA_CONTRACT  Input[SessionMutation] -> Output[PersistedSessionAggregate]
# @INVARIANT  answers, mapping approvals, preview artifacts, and launch snapshots are never attributed to the wrong user or session.
# @RATIONALE  Original 627-line file exceeded INV_7 (400-line module limit). Extracted mutation operations into _mutations sub-module.
# @REJECTED  Keeping all repository operations in one file because it exceeded the fractal limit.

from datetime import UTC, datetime
from typing import Any, cast

from sqlalchemy import or_
from sqlalchemy.orm import Session, joinedload
from sqlalchemy.orm.exc import StaleDataError

from src.core.logger import belief_scope, logger
from src.models.dataset_review import (
    ClarificationQuestion,
    ClarificationSession,
    CompiledPreview,
    DatasetProfile,
    DatasetReviewSession,
    DatasetRunContext,
    ExecutionMapping,
    ImportedFilter,
    SemanticFieldEntry,
    SessionCollaborator,
    TemplateVariable,
    ValidationFinding,
)
from src.services.dataset_review.event_logger import SessionEventLogger

logger = cast(Any, logger)


# #region DatasetReviewSessionVersionConflictError [C:2] [TYPE Class]
# @BRIEF Signal optimistic-lock conflicts for dataset review session mutations.
class DatasetReviewSessionVersionConflictError(ValueError):
    def __init__(self, session_id: str, expected_version: int, actual_version: int):
        self.session_id = session_id
        self.expected_version = expected_version
        self.actual_version = actual_version
        super().__init__(
            f"Session version conflict: expected {expected_version}, actual {actual_version}"
        )


# #endregion DatasetReviewSessionVersionConflictError


# #region DatasetReviewSessionRepository [C:4] [TYPE Class]
# @BRIEF Enforce ownership-scoped persistence and retrieval for dataset review session aggregates.
# @RELATION DEPENDS_ON -> [DatasetReviewSession]
# @RELATION DEPENDS_ON -> [SessionEventLogger]
# @PRE  constructor receives a live SQLAlchemy session and callers provide authenticated user scope.
# @POST  repository methods return ownership-scoped aggregates or persisted child records without changing domain meaning.
# @SIDE_EFFECT  mutates and queries the persistence layer through the injected database session.
class DatasetReviewSessionRepository:
    # region init_repo [TYPE Function]
    # @PURPOSE: Bind one live SQLAlchemy session to the repository instance.
    # @PRE  db_session is not None
    # @POST  Repository instance initialized with valid session
    def __init__(self, db: Session):
        self.db = db
        self.event_logger = SessionEventLogger(db)

    # endregion init_repo

    # region get_owned_session [TYPE Function]
    # @PURPOSE: Resolve one owner-scoped dataset review session for mutation paths.
    # @PRE  session_id and user_id are non-empty identifiers from the authenticated ownership scope.
    # @POST  returns the owned session or raises a deterministic access error.
    def _get_owned_session(self, session_id: str, user_id: str) -> DatasetReviewSession:
        with belief_scope("DatasetReviewSessionRepository.get_owned_session"):
            logger.reason("Resolving owner-scoped dataset review session", extra={"session_id": session_id, "user_id": user_id})
            session = (
                self.db.query(DatasetReviewSession)
                .filter(DatasetReviewSession.session_id == session_id, DatasetReviewSession.user_id == user_id)
                .first()
            )
            if not session:
                logger.explore("Owner-scoped dataset review session lookup failed", extra={"session_id": session_id, "user_id": user_id})
                raise ValueError("Session not found or access denied")
            logger.reflect("Owner-scoped dataset review session resolved", extra={"session_id": session.session_id})
            return session

    # endregion get_owned_session

    # region create_sess [TYPE Function]
    # @PURPOSE: Persist an initial dataset review session shell.
    # @POST  session is committed, refreshed, and returned with persisted identifiers.
    def create_session(self, session: DatasetReviewSession) -> DatasetReviewSession:
        with belief_scope("DatasetReviewSessionRepository.create_session"):
            logger.reason("Persisting dataset review session shell", extra={"user_id": session.user_id, "environment_id": session.environment_id})
            self.db.add(session)
            self.db.commit()
            self.db.refresh(session)
            logger.reflect("Dataset review session shell persisted", extra={"session_id": session.session_id})
            return session

    # endregion create_sess

    # region require_session_version [TYPE Function]
    # @PURPOSE: Enforce optimistic-lock version matching before a session mutation is persisted.
    # @POST  returns the same session when versions match; otherwise raises deterministic conflict error.
    def require_session_version(self, session: DatasetReviewSession, expected_version: int) -> DatasetReviewSession:
        with belief_scope("DatasetReviewSessionRepository.require_session_version"):
            actual_version = int(getattr(session, "version", 0) or 0)
            logger.reason("Checking optimistic-lock version", extra={"session_id": session.session_id, "expected_version": expected_version, "actual_version": actual_version})
            if actual_version != expected_version:
                logger.explore("Rejected mutation due to stale session version", extra={"session_id": session.session_id, "expected_version": expected_version, "actual_version": actual_version})
                raise DatasetReviewSessionVersionConflictError(str(session.session_id), expected_version, actual_version)
            logger.reflect("Optimistic-lock version accepted", extra={"session_id": session.session_id, "version": actual_version})
            return session

    # endregion require_session_version

    # region bump_session_version [TYPE Function]
    # @PURPOSE: Increment optimistic-lock version after a successful session mutation is assembled.
    # @POST  session version increments monotonically.
    def bump_session_version(self, session: DatasetReviewSession) -> int:
        with belief_scope("DatasetReviewSessionRepository.bump_session_version"):
            next_version = int(getattr(session, "version", 0) or 0) + 1
            session.version = next_version
            session.last_activity_at = datetime.now(UTC)
            logger.reflect("Prepared incremented session version", extra={"session_id": session.session_id, "version": next_version})
            return next_version

    # endregion bump_session_version

    # region commit_session_mutation [TYPE Function]
    # @PURPOSE: Commit one prepared session mutation and translate stale writes into deterministic conflicts.
    # @POST  session mutation is committed with one version increment or a deterministic conflict error is raised.
    def commit_session_mutation(
        self, session: DatasetReviewSession, *, refresh_targets: list[Any] | None = None, expected_version: int | None = None,
    ) -> DatasetReviewSession:
        with belief_scope("DatasetReviewSessionRepository.commit_session_mutation"):
            observed_version = int(expected_version if expected_version is not None else getattr(session, "version", 0) or 0)
            logger.reason("Committing session mutation with optimistic lock", extra={"session_id": session.session_id, "observed_version": observed_version})
            self.bump_session_version(session)
            try:
                self.db.commit()
            except StaleDataError as exc:
                self.db.rollback()
                actual_version_row = self.db.query(DatasetReviewSession.version).filter(DatasetReviewSession.session_id == session.session_id).first()
                actual_version = int(actual_version_row[0] or 0) if actual_version_row else 0
                logger.explore("Session commit rejected by optimistic lock", extra={"session_id": session.session_id, "expected_version": observed_version, "actual_version": actual_version})
                raise DatasetReviewSessionVersionConflictError(session.session_id, observed_version, actual_version) from exc
            self.db.refresh(session)
            for target in refresh_targets or []:
                self.db.refresh(target)
            logger.reflect("Session mutation committed", extra={"session_id": session.session_id, "version": getattr(session, "version", None)})
            return session

    # endregion commit_session_mutation

    # region load_detail [TYPE Function]
    # @PURPOSE: Return the full session aggregate for API and frontend resume flows.
    # @POST  Returns SessionDetail with all fields populated or None.
    def load_session_detail(self, session_id: str, user_id: str) -> DatasetReviewSession | None:
        with belief_scope("DatasetReviewSessionRepository.load_session_detail"):
            logger.reason("Loading dataset review session detail", extra={"session_id": session_id, "user_id": user_id})
            session = (
                self.db.query(DatasetReviewSession)
                .outerjoin(SessionCollaborator, DatasetReviewSession.session_id == SessionCollaborator.session_id)
                .options(
                    joinedload(DatasetReviewSession.profile),
                    joinedload(DatasetReviewSession.findings),
                    joinedload(DatasetReviewSession.collaborators),
                    joinedload(DatasetReviewSession.semantic_sources),
                    joinedload(DatasetReviewSession.semantic_fields).joinedload(SemanticFieldEntry.candidates),
                    joinedload(DatasetReviewSession.imported_filters),
                    joinedload(DatasetReviewSession.template_variables),
                    joinedload(DatasetReviewSession.execution_mappings),
                    joinedload(DatasetReviewSession.clarification_sessions).joinedload(ClarificationSession.questions).joinedload(ClarificationQuestion.options),
                    joinedload(DatasetReviewSession.clarification_sessions).joinedload(ClarificationSession.questions).joinedload(ClarificationQuestion.answer),
                    joinedload(DatasetReviewSession.previews),
                    joinedload(DatasetReviewSession.run_contexts),
                    joinedload(DatasetReviewSession.events),
                )
                .filter(DatasetReviewSession.session_id == session_id)
                .filter(or_(DatasetReviewSession.user_id == user_id, SessionCollaborator.user_id == user_id))
                .first()
            )
            logger.reflect("Session detail lookup completed", extra={"session_id": session_id, "found": bool(session)})
            return session

    # endregion load_detail

    # region save_profile_and_findings [TYPE Function]
    # @PURPOSE: Persist profile state and replace validation findings for an owned session.
    # @POST  stored profile matches the current session and findings are replaced.
    def save_profile_and_findings(
        self, session_id: str, user_id: str, profile: DatasetProfile, findings: list[ValidationFinding], expected_version: int | None = None,
    ) -> DatasetReviewSession:
        from src.services.dataset_review.repositories.repository_pkg._mutations import save_profile_and_findings as _save
        return _save(
            self.db, self.event_logger, self._get_owned_session, self.require_session_version,
            self.commit_session_mutation, session_id, user_id, profile, findings, expected_version,
        )

    # endregion save_profile_and_findings

    # region save_recovery_state [TYPE Function]
    # @PURPOSE: Persist imported filters, template variables, and initial execution mappings.
    def save_recovery_state(
        self, session_id: str, user_id: str, imported_filters: list[ImportedFilter],
        template_variables: list[TemplateVariable], execution_mappings: list[ExecutionMapping],
        expected_version: int | None = None,
    ) -> DatasetReviewSession:
        from src.services.dataset_review.repositories.repository_pkg._mutations import save_recovery_state as _save
        return _save(
            self.db, self._get_owned_session, self.require_session_version,
            self.commit_session_mutation, self.load_session_detail,
            session_id, user_id, imported_filters, template_variables, execution_mappings, expected_version,
        )

    # endregion save_recovery_state

    # region save_preview [TYPE Function]
    # @PURPOSE: Persist a preview snapshot and mark prior session previews stale.
    def save_preview(
        self, session_id: str, user_id: str, preview: CompiledPreview, expected_version: int | None = None,
    ) -> CompiledPreview:
        from src.services.dataset_review.repositories.repository_pkg._mutations import save_preview as _save
        return _save(
            self.db, self._get_owned_session, self.require_session_version,
            self.commit_session_mutation, session_id, user_id, preview, expected_version,
        )

    # endregion save_preview

    # region save_run_context [TYPE Function]
    # @PURPOSE: Persist an immutable launch audit snapshot for an owned session.
    def save_run_context(
        self, session_id: str, user_id: str, run_context: DatasetRunContext, expected_version: int | None = None,
    ) -> DatasetRunContext:
        from src.services.dataset_review.repositories.repository_pkg._mutations import save_run_context as _save
        return _save(
            self.db, self._get_owned_session, self.require_session_version,
            self.commit_session_mutation, session_id, user_id, run_context, expected_version,
        )

    # endregion save_run_context

    # region list_user_sess [TYPE Function]
    # @PURPOSE: List review sessions owned by a specific user ordered by most recent update.
    def list_sessions_for_user(self, user_id: str) -> list[DatasetReviewSession]:
        with belief_scope("DatasetReviewSessionRepository.list_sessions_for_user"):
            logger.reason("Listing dataset review sessions for owner scope", extra={"user_id": user_id})
            sessions = (
                self.db.query(DatasetReviewSession)
                .filter(DatasetReviewSession.user_id == user_id)
                .order_by(DatasetReviewSession.updated_at.desc())
                .all()
            )
            logger.reflect("Session list assembled", extra={"user_id": user_id, "session_count": len(sessions)})
            return sessions

    # endregion list_user_sess


# #endregion DatasetReviewSessionRepository

# #endregion DatasetReviewSessionRepository
