from datetime import datetime, timedelta
from typing import Annotated, Optional
from uuid import UUID

from arthur_common.models.common_schemas import PaginationParameters
from arthur_common.models.response_schemas import ListAgenticAnnotationsResponse
from arthur_common.models.task_eval_schemas import (
    ContinuousEvalResponse,
    ContinuousEvalVariableMappingResponse,
    ListContinuousEvalsResponse,
)
from fastapi import APIRouter, Depends, HTTPException, Path, Query, status
from pydantic import AfterValidator
from sqlalchemy.orm import Session

from dependencies import (
    get_db_session,
    get_org_scope,
    get_validated_task,
)
from repositories.continuous_eval_test_run_repository import (
    ContinuousEvalTestRunRepository,
)
from repositories.continuous_evals_repository import ContinuousEvalsRepository
from repositories.llm_evals_repository import LLMEvalsRepository
from repositories.trace_transform_repository import TraceTransformRepository
from routers.route_handler import GenaiEngineRoute
from routers.v2 import multi_validator
from schemas.enums import PermissionLevelsEnum
from schemas.internal_schemas import Task, User
from schemas.request_schemas import (
    ContinuousEvalCreateRequest,
    ContinuousEvalListFilterRequest,
    ContinuousEvalRunResultsListFilterRequest,
    CreateTestRunRequest,
    UpdateContinuousEvalRequest,
)
from schemas.response_schemas import (
    AgenticAnnotationAnalyticsResponse,
    ContinuousEvalRerunResponse,
    ContinuousEvalTestRunResponse,
    ListContinuousEvalTestRunsResponse,
)
from utils.url_encoding import decode_path_param
from utils.users import enforce_org_scope, permission_checker
from utils.utils import common_pagination_parameters

continuous_eval_routes = APIRouter(
    prefix="/api/v1",
    route_class=GenaiEngineRoute,
)


def _assert_transform_accessible(
    transform_repo: TraceTransformRepository,
    transform_id: UUID,
    org_scope: UUID | None,
) -> None:
    """404 if the transform doesn't exist or is outside the caller's org.

    Used by handlers that take a `transform_id` from path or body where the
    route-level decorator only scopes a sibling `task_id` (or no task_id at
    all). Without this guard a tenant could read or pin a cross-org
    transform by guessing its UUID.
    """
    if transform_repo.get_transform_by_id(transform_id, org_scope=org_scope) is None:
        raise HTTPException(
            status_code=404,
            detail=f"Transform {transform_id} not found.",
        )


@continuous_eval_routes.get(
    "/continuous_evals/{eval_id}",
    summary="Get a continuous eval by id",
    description="Get a continuous eval by id",
    response_model=ContinuousEvalResponse,
    response_model_exclude_none=True,
    tags=["Continuous Evals"],
)
@permission_checker(permissions=PermissionLevelsEnum.TASK_READ.value)
def get_continuous_eval_by_id(
    eval_id: UUID = Path(
        ...,
        description="The id of the continuous eval to retrieve.",
        title="Continuous Eval ID",
    ),
    db_session: Session = Depends(get_db_session),
    current_user: User | None = Depends(multi_validator.validate_api_multi_auth),
    org_scope: UUID | None = Depends(get_org_scope),
) -> ContinuousEvalResponse:
    try:
        continuous_eval_repo = ContinuousEvalsRepository(db_session)
        continuous_eval = continuous_eval_repo.get_continuous_eval_by_id(
            eval_id=eval_id,
            org_scope=org_scope,
        )
        return continuous_eval.to_response_model()
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


@continuous_eval_routes.get(
    "/tasks/{task_id}/continuous_evals",
    summary="Get all continuous evals for a specific task",
    description="Get all continuous evals for a specific task",
    response_model=ListContinuousEvalsResponse,
    response_model_exclude_none=True,
    tags=["Continuous Evals"],
)
@permission_checker(permissions=PermissionLevelsEnum.TASK_READ.value)
@enforce_org_scope()
def list_continuous_evals(
    pagination_parameters: Annotated[
        PaginationParameters,
        Depends(common_pagination_parameters),
    ],
    filter_request: Annotated[
        ContinuousEvalListFilterRequest,
        Depends(ContinuousEvalListFilterRequest.from_query_parameters),
    ],
    db_session: Session = Depends(get_db_session),
    current_user: User | None = Depends(multi_validator.validate_api_multi_auth),
    task: Task = Depends(get_validated_task),
) -> ListContinuousEvalsResponse:
    try:
        continuous_eval_repo = ContinuousEvalsRepository(db_session)
        continuous_evals, total_count = continuous_eval_repo.list_continuous_evals(
            task.id,
            pagination_parameters,
            filter_request,
        )
        return ListContinuousEvalsResponse(
            evals=[
                continuous_eval.to_response_model()
                for continuous_eval in continuous_evals
            ],
            count=total_count,
        )
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


@continuous_eval_routes.get(
    "/tasks/{task_id}/continuous_evals/results",
    summary="Get all continuous eval run results for a specific task",
    description="Get all continuous eval run results for a specific task",
    response_model=ListAgenticAnnotationsResponse,
    response_model_exclude_none=True,
    tags=["Continuous Evals"],
)
@permission_checker(permissions=PermissionLevelsEnum.TASK_READ.value)
@enforce_org_scope()
def list_continuous_eval_run_results(
    pagination_parameters: Annotated[
        PaginationParameters,
        Depends(common_pagination_parameters),
    ],
    filter_request: Annotated[
        ContinuousEvalRunResultsListFilterRequest,
        Depends(ContinuousEvalRunResultsListFilterRequest.from_query_parameters),
    ],
    db_session: Session = Depends(get_db_session),
    current_user: User | None = Depends(multi_validator.validate_api_multi_auth),
    task: Task = Depends(get_validated_task),
) -> ListAgenticAnnotationsResponse:
    try:
        continuous_eval_repo = ContinuousEvalsRepository(db_session)
        agentic_annotations, total_count = (
            continuous_eval_repo.list_continuous_eval_run_results(
                task.id,
                pagination_parameters,
                filter_request,
            )
        )
        return ListAgenticAnnotationsResponse(
            annotations=[
                annotation.to_response_model() for annotation in agentic_annotations
            ],
            count=total_count,
        )
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


@continuous_eval_routes.get(
    "/tasks/{task_id}/continuous_evals/transforms/{transform_id}/llm_evals/{eval_name}/versions/{eval_version}/variables",
    summary="Get all variables and mappings for a continuous eval",
    description="Get all variables and mappings for a continuous eval",
    response_model=ContinuousEvalVariableMappingResponse,
    response_model_exclude_none=True,
    tags=["Continuous Evals"],
)
@permission_checker(permissions=PermissionLevelsEnum.TASK_READ.value)
@enforce_org_scope()
def get_continuous_eval_variables_and_mappings(
    transform_id: Annotated[
        UUID,
        Path(
            description="The id of the transform to get the continuous eval variables and mappings for.",
            title="Transform ID",
        ),
    ],
    eval_name: Annotated[str, Path(), AfterValidator(decode_path_param)],
    eval_version: Annotated[
        str,
        Path(
            description="The version of the llm eval to get the continuous eval variables and mappings for.",
            title="LLM Eval Version",
        ),
    ],
    db_session: Session = Depends(get_db_session),
    current_user: User | None = Depends(multi_validator.validate_api_multi_auth),
    task: Task = Depends(get_validated_task),
    org_scope: UUID | None = Depends(get_org_scope),
) -> ContinuousEvalVariableMappingResponse:
    try:
        # Validate the llm eval exists and hasn't been deleted
        llm_eval_repo = LLMEvalsRepository(db_session)
        llm_eval = llm_eval_repo.get_llm_item(
            task.id,
            eval_name,
            eval_version,
        )
        if llm_eval.deleted_at is not None:
            raise HTTPException(
                status_code=400,
                detail=f"LLM Eval {llm_eval.name} (version {llm_eval.version}) has been deleted.",
            )

        # Validate the transform exists and is in the caller's org
        transform_repo = TraceTransformRepository(db_session)
        _assert_transform_accessible(transform_repo, transform_id, org_scope)

        transform_definition = transform_repo.get_latest_definition(transform_id)
        eval_vars = set(llm_eval.variables)
        transform_vars = {v.variable_name for v in transform_definition.variables}
        matching_vars = list(eval_vars & transform_vars)

        return ContinuousEvalVariableMappingResponse(
            matching_variables=matching_vars,
            transform_variables=list(transform_vars),
            eval_variables=list(eval_vars),
        )

    except HTTPException:
        raise
    except ValueError as e:
        if "not found" in str(e).lower():
            raise HTTPException(status_code=404, detail=str(e))
        else:
            raise HTTPException(status_code=400, detail=str(e))
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


@continuous_eval_routes.post(
    "/tasks/{task_id}/continuous_evals",
    summary="Create a continuous eval",
    description="Create a continuous eval",
    response_model=ContinuousEvalResponse,
    response_model_exclude_none=True,
    tags=["Continuous Evals"],
)
@permission_checker(permissions=PermissionLevelsEnum.TASK_WRITE.value)
@enforce_org_scope()
def create_continuous_eval(
    create_request: ContinuousEvalCreateRequest,
    db_session: Session = Depends(get_db_session),
    current_user: User | None = Depends(multi_validator.validate_api_multi_auth),
    task: Task = Depends(get_validated_task),
    org_scope: UUID | None = Depends(get_org_scope),
) -> ContinuousEvalResponse:
    try:
        # Validate the llm eval exists and hasn't been deleted
        llm_eval_repo = LLMEvalsRepository(db_session)
        llm_eval_version = (
            str(create_request.llm_eval_version)
            if isinstance(create_request.llm_eval_version, int)
            else create_request.llm_eval_version
        )
        llm_eval = llm_eval_repo.get_llm_item(
            task.id,
            create_request.llm_eval_name,
            llm_eval_version,
        )

        if llm_eval.deleted_at is not None:
            raise HTTPException(
                status_code=400,
                detail=f"LLM Eval {llm_eval.name} (version {llm_eval.version}) has been deleted.",
            )

        # set the version to the integer version of the llm eval
        create_request.llm_eval_version = llm_eval.version

        # Validate the transform exists and is in the caller's org
        transform_repo = TraceTransformRepository(db_session)
        _assert_transform_accessible(
            transform_repo,
            create_request.transform_id,
            org_scope,
        )

        if create_request.transform_version_id is not None:
            # Validate version belongs to this transform (raises 404 if not found)
            validation_definition = transform_repo.get_version_by_id(
                create_request.transform_id,
                create_request.transform_version_id,
            ).definition
        else:
            validation_definition = transform_repo.get_latest_definition(
                create_request.transform_id,
            )

        continuous_eval_repo = ContinuousEvalsRepository(db_session)

        continuous_eval_repo.validate_transform_variable_mapping(
            validation_definition,
            llm_eval,
            create_request.transform_variable_mapping,
        )

        continuous_eval = continuous_eval_repo.create_continuous_eval(
            task.id,
            create_request,
        )
        return continuous_eval.to_response_model()
    except ValueError as e:
        if "not found" in str(e).lower():
            raise HTTPException(status_code=404, detail=str(e))
        else:
            raise HTTPException(status_code=400, detail=str(e))
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


@continuous_eval_routes.patch(
    "/continuous_evals/{eval_id}",
    summary="Update a continuous eval",
    description="Update a continuous eval",
    response_model=ContinuousEvalResponse,
    response_model_exclude_none=True,
    tags=["Continuous Evals"],
)
@permission_checker(permissions=PermissionLevelsEnum.TASK_WRITE.value)
def update_continuous_eval(
    update_request: UpdateContinuousEvalRequest,
    eval_id: UUID = Path(
        ...,
        description="The id of the continuous eval to update.",
        title="Continuous Eval ID",
    ),
    db_session: Session = Depends(get_db_session),
    current_user: User | None = Depends(multi_validator.validate_api_multi_auth),
    org_scope: UUID | None = Depends(get_org_scope),
) -> ContinuousEvalResponse:
    try:
        continuous_eval_repo = ContinuousEvalsRepository(db_session)
        llm_eval_repo = LLMEvalsRepository(db_session)
        transform_repo = TraceTransformRepository(db_session)

        existing_eval = continuous_eval_repo.get_continuous_eval_by_id(
            eval_id, org_scope=org_scope
        )
        llm_eval = None
        llm_eval_version: str | int | None = None

        if update_request.llm_eval_version is not None:
            llm_eval_name = existing_eval.llm_eval_name
            if update_request.llm_eval_name is not None:
                llm_eval_name = update_request.llm_eval_name

            # Validate the llm eval exists and hasn't been deleted
            llm_eval_version = (
                str(update_request.llm_eval_version)
                if isinstance(update_request.llm_eval_version, int)
                else update_request.llm_eval_version
            )
            llm_eval = llm_eval_repo.get_llm_item(
                existing_eval.task_id,
                llm_eval_name,
                llm_eval_version,
            )

            if llm_eval.deleted_at is not None:
                raise HTTPException(
                    status_code=400,
                    detail=f"LLM Eval {llm_eval.name} (version {llm_eval.version}) has been deleted.",
                )

            # set the version to the integer version of the llm eval
            update_request.llm_eval_version = llm_eval.version

        # Validate that transform_version_id belongs to the transform (when mapping is not being updated)
        if (
            "transform_version_id" in update_request.model_fields_set
            and update_request.transform_version_id is not None
            and update_request.transform_variable_mapping is None
        ):
            transform_id = (
                update_request.transform_id
                if update_request.transform_id is not None
                else existing_eval.transform_id
            )
            _assert_transform_accessible(transform_repo, transform_id, org_scope)
            transform_repo.get_version_by_id(
                transform_id,
                update_request.transform_version_id,
            )

        # Validate the transform variable mapping
        if update_request.transform_variable_mapping is not None:
            transform_id = (
                update_request.transform_id
                if update_request.transform_id is not None
                else existing_eval.transform_id
            )
            _assert_transform_accessible(transform_repo, transform_id, org_scope)

            # If a version is being pinned (new or existing), validate and use its snapshot
            effective_version_id = (
                update_request.transform_version_id
                if "transform_version_id" in update_request.model_fields_set
                else existing_eval.transform_version_id
            )
            if effective_version_id is not None:
                validation_definition = transform_repo.get_version_by_id(
                    transform_id,
                    effective_version_id,
                ).definition
            else:
                validation_definition = transform_repo.get_latest_definition(
                    transform_id,
                )

            if llm_eval is None:
                llm_eval_name = (
                    update_request.llm_eval_name
                    if update_request.llm_eval_name is not None
                    else existing_eval.llm_eval_name
                )
                llm_eval_version = (
                    update_request.llm_eval_version
                    if update_request.llm_eval_version is not None
                    else existing_eval.llm_eval_version
                )
                llm_eval = llm_eval_repo.get_llm_item(
                    existing_eval.task_id,
                    llm_eval_name,
                    str(llm_eval_version),
                )
                if llm_eval.deleted_at is not None:
                    raise HTTPException(
                        status_code=400,
                        detail=f"LLM Eval {llm_eval.name} (version {llm_eval.version}) has been deleted.",
                    )

            continuous_eval_repo.validate_transform_variable_mapping(
                validation_definition,
                llm_eval,
                update_request.transform_variable_mapping,
            )

        # Update the continuous eval
        continuous_eval = continuous_eval_repo.update_continuous_eval(
            eval_id,
            update_request,
        )
        return continuous_eval.to_response_model()
    except ValueError as e:
        if "not found" in str(e).lower():
            raise HTTPException(status_code=404, detail=str(e))
        else:
            raise HTTPException(status_code=400, detail=str(e))
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


@continuous_eval_routes.post(
    "/continuous_evals/results/{run_id}/rerun",
    summary="Rerun a failed continuous eval",
    description="Rerun a failed continuous eval",
    response_model=ContinuousEvalRerunResponse,
    response_model_exclude_none=True,
    tags=["Continuous Evals"],
)
@permission_checker(permissions=PermissionLevelsEnum.TASK_WRITE.value)
def rerun_continuous_eval(
    run_id: UUID = Path(
        ...,
        description="The id of the continuous eval run to rerun.",
        title="Run ID",
    ),
    db_session: Session = Depends(get_db_session),
    current_user: User | None = Depends(multi_validator.validate_api_multi_auth),
    org_scope: UUID | None = Depends(get_org_scope),
) -> ContinuousEvalRerunResponse:
    try:
        continuous_eval_repo = ContinuousEvalsRepository(db_session)
        return continuous_eval_repo.rerun_continuous_eval_by_annotation_id(
            run_id, org_scope=org_scope
        )
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


@continuous_eval_routes.delete(
    "/continuous_evals/{eval_id}",
    summary="Delete a continuous eval",
    description="Delete a continuous eval",
    status_code=status.HTTP_204_NO_CONTENT,
    responses={
        status.HTTP_204_NO_CONTENT: {"description": "Continuous eval deleted."},
    },
    tags=["Continuous Evals"],
)
@permission_checker(permissions=PermissionLevelsEnum.TASK_WRITE.value)
def delete_continuous_eval(
    eval_id: UUID = Path(
        ...,
        description="The id of the continuous eval to delete.",
        title="Continuous Eval ID",
    ),
    db_session: Session = Depends(get_db_session),
    current_user: User | None = Depends(multi_validator.validate_api_multi_auth),
    org_scope: UUID | None = Depends(get_org_scope),
) -> None:
    try:
        continuous_eval_repo = ContinuousEvalsRepository(db_session)
        # Validate ownership via the lookup first (raises 404 for tenants on cross-org IDs).
        if org_scope is not None:
            continuous_eval_repo.get_continuous_eval_by_id(eval_id, org_scope=org_scope)
        continuous_eval_repo.delete_continuous_eval(eval_id)
    except ValueError as e:
        if "not found" in str(e).lower():
            raise HTTPException(status_code=404, detail=str(e))
        else:
            raise HTTPException(status_code=400, detail=str(e))
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


@continuous_eval_routes.get(
    "/tasks/{task_id}/continuous_evals/analytics/daily",
    summary="Get daily aggregated analytics for agentic annotations",
    description="Returns daily counts of passed/failed/error/skipped annotations and total cost per day",
    response_model=AgenticAnnotationAnalyticsResponse,
    response_model_exclude_none=True,
    tags=["Continuous Evals"],
)
@permission_checker(permissions=PermissionLevelsEnum.TASK_READ.value)
@enforce_org_scope()
def get_daily_annotation_analytics(
    start_time: Annotated[
        Optional[datetime],
        Query(description="Start time (inclusive). Defaults to 30 days ago."),
    ] = None,
    end_time: Annotated[
        Optional[datetime],
        Query(description="End time (exclusive). Defaults to now."),
    ] = None,
    db_session: Session = Depends(get_db_session),
    current_user: User = Depends(multi_validator.validate_api_multi_auth),
    task: Task = Depends(get_validated_task),
) -> AgenticAnnotationAnalyticsResponse:
    try:
        # Set defaults: last 30 days if not provided
        if end_time is None:
            end_time = datetime.now()
        if start_time is None:
            start_time = end_time - timedelta(days=30)

        # Validate time range
        if start_time >= end_time:
            raise HTTPException(
                status_code=400,
                detail="start_time must be before end_time",
            )

        # Get analytics from repository (already returns response models)
        continuous_eval_repo = ContinuousEvalsRepository(db_session)
        stats = continuous_eval_repo.get_daily_annotation_analytics(
            task.id,
            start_time,
            end_time,
        )

        return AgenticAnnotationAnalyticsResponse(
            stats=stats,
            count=len(stats),
        )
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


# ============================================================================
# Test Run Endpoints
# ============================================================================


@continuous_eval_routes.post(
    "/continuous_evals/{eval_id}/test_runs",
    summary="Create and start a test run for a continuous eval",
    description="Run a continuous eval against specific traces as a test. Results are stored separately from production annotations.",
    response_model=ContinuousEvalTestRunResponse,
    response_model_exclude_none=True,
    tags=["Continuous Eval Test Runs"],
)
@permission_checker(permissions=PermissionLevelsEnum.TASK_WRITE.value)
def create_test_run(
    create_request: CreateTestRunRequest,
    eval_id: UUID = Path(
        ...,
        description="The id of the continuous eval to test.",
        title="Continuous Eval ID",
    ),
    db_session: Session = Depends(get_db_session),
    current_user: User | None = Depends(multi_validator.validate_api_multi_auth),
    org_scope: UUID | None = Depends(get_org_scope),
) -> ContinuousEvalTestRunResponse:
    try:
        # Look up the continuous eval (org-scoped for tenant callers) and get its task_id
        continuous_eval_repo = ContinuousEvalsRepository(db_session)
        continuous_eval = continuous_eval_repo.get_continuous_eval_by_id(
            eval_id, org_scope=org_scope
        )

        test_run_repo = ContinuousEvalTestRunRepository(db_session)
        test_run = test_run_repo.create_test_run(
            continuous_eval_id=eval_id,
            task_id=continuous_eval.task_id,
            trace_ids=create_request.trace_ids,
        )
        return ContinuousEvalTestRunResponse(
            id=test_run.id,
            continuous_eval_id=test_run.continuous_eval_id,
            task_id=test_run.task_id,
            status=test_run.status,
            total_count=test_run.total_count,
            completed_count=test_run.completed_count,
            passed_count=test_run.passed_count,
            failed_count=test_run.failed_count,
            error_count=test_run.error_count,
            skipped_count=test_run.skipped_count,
            created_at=test_run.created_at,
            updated_at=test_run.updated_at,
        )
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


@continuous_eval_routes.get(
    "/continuous_evals/{eval_id}/test_runs",
    summary="List test runs for a continuous eval",
    description="Get all test runs for a specific continuous eval.",
    response_model=ListContinuousEvalTestRunsResponse,
    response_model_exclude_none=True,
    tags=["Continuous Eval Test Runs"],
)
@permission_checker(permissions=PermissionLevelsEnum.TASK_READ.value)
def list_test_runs(
    pagination_parameters: Annotated[
        PaginationParameters,
        Depends(common_pagination_parameters),
    ],
    eval_id: UUID = Path(
        ...,
        description="The id of the continuous eval.",
        title="Continuous Eval ID",
    ),
    db_session: Session = Depends(get_db_session),
    current_user: User | None = Depends(multi_validator.validate_api_multi_auth),
    org_scope: UUID | None = Depends(get_org_scope),
) -> ListContinuousEvalTestRunsResponse:
    try:
        # Validate ownership of the parent continuous eval before listing test runs.
        if org_scope is not None:
            ContinuousEvalsRepository(db_session).get_continuous_eval_by_id(
                eval_id, org_scope=org_scope
            )
        test_run_repo = ContinuousEvalTestRunRepository(db_session)
        test_runs = test_run_repo.list_test_runs(
            continuous_eval_id=eval_id,
            pagination_parameters=pagination_parameters,
            org_scope=org_scope,
        )
        count = test_run_repo.count_test_runs(eval_id)
        return ListContinuousEvalTestRunsResponse(
            test_runs=[
                ContinuousEvalTestRunResponse(
                    id=tr.id,
                    continuous_eval_id=tr.continuous_eval_id,
                    task_id=tr.task_id,
                    status=tr.status,
                    total_count=tr.total_count,
                    completed_count=tr.completed_count,
                    passed_count=tr.passed_count,
                    failed_count=tr.failed_count,
                    error_count=tr.error_count,
                    skipped_count=tr.skipped_count,
                    created_at=tr.created_at,
                    updated_at=tr.updated_at,
                )
                for tr in test_runs
            ],
            count=count,
        )
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


@continuous_eval_routes.get(
    "/continuous_evals/test_runs/{test_run_id}",
    summary="Get a test run by id",
    description="Get the status and counters for a specific test run. Use this endpoint for polling progress.",
    response_model=ContinuousEvalTestRunResponse,
    response_model_exclude_none=True,
    tags=["Continuous Eval Test Runs"],
)
@permission_checker(permissions=PermissionLevelsEnum.TASK_READ.value)
def get_test_run(
    test_run_id: UUID = Path(
        ...,
        description="The id of the test run.",
        title="Test Run ID",
    ),
    db_session: Session = Depends(get_db_session),
    current_user: User | None = Depends(multi_validator.validate_api_multi_auth),
    org_scope: UUID | None = Depends(get_org_scope),
) -> ContinuousEvalTestRunResponse:
    try:
        test_run_repo = ContinuousEvalTestRunRepository(db_session)
        test_run = test_run_repo.get_test_run(test_run_id, org_scope=org_scope)
        return ContinuousEvalTestRunResponse(
            id=test_run.id,
            continuous_eval_id=test_run.continuous_eval_id,
            task_id=test_run.task_id,
            status=test_run.status,
            total_count=test_run.total_count,
            completed_count=test_run.completed_count,
            passed_count=test_run.passed_count,
            failed_count=test_run.failed_count,
            error_count=test_run.error_count,
            skipped_count=test_run.skipped_count,
            created_at=test_run.created_at,
            updated_at=test_run.updated_at,
        )
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


@continuous_eval_routes.get(
    "/continuous_evals/test_runs/{test_run_id}/results",
    summary="Get individual test case results for a test run",
    description="Get the per-trace results of a test run, including scores, reasons, and input variables.",
    response_model=ListAgenticAnnotationsResponse,
    response_model_exclude_none=True,
    tags=["Continuous Eval Test Runs"],
)
@permission_checker(permissions=PermissionLevelsEnum.TASK_READ.value)
def get_test_run_results(
    pagination_parameters: Annotated[
        PaginationParameters,
        Depends(common_pagination_parameters),
    ],
    test_run_id: UUID = Path(
        ...,
        description="The id of the test run.",
        title="Test Run ID",
    ),
    db_session: Session = Depends(get_db_session),
    current_user: User | None = Depends(multi_validator.validate_api_multi_auth),
    org_scope: UUID | None = Depends(get_org_scope),
) -> ListAgenticAnnotationsResponse:
    try:
        test_run_repo = ContinuousEvalTestRunRepository(db_session)
        # Ownership validated up-front via the test run lookup; raises 404 if
        # the test run is in a different org.
        if org_scope is not None:
            test_run_repo.get_test_run(test_run_id, org_scope=org_scope)
        annotations = test_run_repo.get_test_run_results(
            test_run_id=test_run_id,
            pagination_parameters=pagination_parameters,
            org_scope=org_scope,
        )
        count = test_run_repo.count_test_run_results(test_run_id)
        return ListAgenticAnnotationsResponse(
            annotations=[annotation.to_response_model() for annotation in annotations],
            count=count,
        )
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


@continuous_eval_routes.delete(
    "/continuous_evals/test_runs/{test_run_id}",
    summary="Delete a test run",
    description="Delete a test run and all its associated annotation results.",
    status_code=status.HTTP_204_NO_CONTENT,
    responses={
        status.HTTP_204_NO_CONTENT: {"description": "Test run deleted."},
    },
    tags=["Continuous Eval Test Runs"],
)
@permission_checker(permissions=PermissionLevelsEnum.TASK_WRITE.value)
def delete_test_run(
    test_run_id: UUID = Path(
        ...,
        description="The id of the test run to delete.",
        title="Test Run ID",
    ),
    db_session: Session = Depends(get_db_session),
    current_user: User | None = Depends(multi_validator.validate_api_multi_auth),
    org_scope: UUID | None = Depends(get_org_scope),
) -> None:
    try:
        test_run_repo = ContinuousEvalTestRunRepository(db_session)
        test_run_repo.delete_test_run(test_run_id, org_scope=org_scope)
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))
