from __future__ import annotations

from typing import Sequence, TYPE_CHECKING, Any, Literal

from adaptive_sdk.graphql_client import (
    GraderData,
    GraderCreateInput,
    CustomConfigInput,
    GraderConfigInput,
    GraderTypeEnum,
    JudgeConfigInput,
    PrebuiltConfigInput,
    RemoteConfigInput,
    PrebuiltCriteriaKey,
    MetricGetOrCreate,
    JudgeExampleInput,
    JudgeExampleInputTurnEntry,
    TestRemoteEnvTestRemoteEnvRemoteEnvTestOffline,
    TestRemoteEnvTestRemoteEnvRemoteEnvTestOnline,
    RemoteEnvCreate,
)
from adaptive_sdk import input_types

from .base_resource import SyncAPIResource, AsyncAPIResource, UseCaseResource

if TYPE_CHECKING:
    from adaptive_sdk.client import Adaptive, AsyncAdaptive


class GraderCreator(SyncAPIResource, UseCaseResource):  # type: ignore[misc]
    """Helper class for creating different types of graders with a clean API."""

    def __init__(self, client: Adaptive) -> None:
        SyncAPIResource.__init__(self, client)
        UseCaseResource.__init__(self, client)

    def binary_judge(
        self,
        *,
        key: str,
        criteria: str,
        judge_model: str,
        feedback_key: str,
        name: str | None = None,
        examples: list[input_types.JudgeExampleInput] | None = None,
        use_case: str | None = None,
    ) -> GraderData:
        """
        Creates a new judge-based grader.

        Args:
            key: Unique key for the grader.
            criteria: Natural-language explanation of what should be evaluated as a binary pass/fail.
            judge_model: Model key of the judge model.
            feedback_key: Key for the feedback this grader writes to.
            examples: List of annotated examples for few-shot prompting.
            name: Human-readable grader name. If omitted, derived from key.
            use_case: Explicit use-case key. Falls back to client.default_use_case.
        """
        # Parse examples if provided
        parsed_examples = []
        if examples:
            for ex in examples:
                if not all(k in ex for k in ("input", "output", "passes")):
                    raise ValueError("Each example must contain 'input', 'output', and 'passes' keys")
                input_turns = [JudgeExampleInputTurnEntry(role=t["role"], content=t["content"]) for t in ex["input"]]
                parsed_examples.append(
                    JudgeExampleInput(
                        input=input_turns,
                        output=ex["output"],
                        reasoning=ex.get("reasoning"),
                        **{"pass": bool(ex["passes"])},
                    )
                )

        # Create judge config
        judge_config = JudgeConfigInput(
            model=judge_model,
            criteria=criteria,
            examples=parsed_examples,
            systemTemplate=r"""You are an expert evaluator that evaluates completions generated by an AI model on a fixed criterion.
You will be given all elements of an interaction between human an AI model:
    The full context of the conversation so far leading up to the last user turn/question is under the CONTEXT header. It may contain extra contextual information.
    The last user turn/question is under the USER QUESTION header. It may contain extra contextual information.
    The model's completion is under the COMPLETION TO EVALUATE header.
    The evaluation criterion is under the EVALUATION CRITERION section.
{{#if examples.length}}

CRITICAL: The annotations below are GROUND TRUTH provided by expert human annotators. You MUST follow them exactly, even if they seem counter-intuitive to you.

In order to to analyze and score a completion, you always run the following steps without exception:
First, you read the CONTEXT, USER QUESTION and COMPLETION TO EVALUATE.
Then, you MUST check if there is an annotation that matches (or is very similar to) the current case:
  - If the USER QUESTION and COMPLETION TO EVALUATE match an annotation, you MUST use the annotation's score and reasoning. Do NOT apply your own judgment.
  - If there is a similar annotation, you MUST follow the same reasoning pattern and scoring approach, even if it contradicts your intuition.
  - The annotations define what is considered PASS/FAIL for this specific task. Your personal understanding of the criterion is IRRELEVANT.
Then, ONLY if no similar annotation exists, you analyze the COMPLETION TO EVALUATE and assign a score according to the criterion.

Rules to follow:
- You must always evaluate the COMPLETION TO EVALUATE based solely on the USER QUESTION, and never on an intermediary question that might have been asked in the CONTEXT. The CONTEXT is there for context only.
- Do not include text that is in the CONTEXT to make your judgement; you are evaluating the COMPLETION TO EVALUATE text only.
- You must not use the original instructions given to the model in the CONTEXT for your judgement. Focus only on the ANNOTATIONS AND EVALUATION CRITERION without any other influencing factors.
- You are forbidden to return a score other than PASS, FAIL or NA for each criterion.
- If the criterion is conditional, and is not applicable to the specific USER QUESTION + COMPLETION TO EVALUATE pair, you must score it as NA.
- Return a single score, no matter how many things are evaluated or contemplated in the criterion. A PASS means the completion complied with everything.
- ANNOTATIONS ARE ABSOLUTE TRUTH. If an annotation says something is PASS, it is PASS, regardless of what you think.
{{else}}

In order to to analyze and score a completion, you always run the following steps without exception:
First, you read the CONTEXT, USER QUESTION and COMPLETION TO EVALUATE.
Then, you analyze the COMPLETION TO EVALUATE, and assign it a PASS, FAIL or NA score according to the criterion: FAIL if the completion does not meet the criterion, PASS if it does, and NA if the criterion is not applicable to the example.

Rules to follow:
- You must always evaluate the COMPLETION TO EVALUATE based solely on the USER QUESTION, and never on an intermediary question that might have been asked in the CONTEXT. The CONTEXT is there for context only.
- Do not include text that is in the CONTEXT to make your judgement; you are evaluating the COMPLETION TO EVALUATE text only.
- You must not use the original instructions given to the model in the CONTEXT for your judgement. Focus only on the EVALUATION CRITERION.
- You are forbidden to return a score other than PASS, FAIL or NA for each criterion.
- If the criterion is conditional, and is not applicable to the specific USER QUESTION + COMPLETION TO EVALUATE pair, you must score it as NA.
- Return a single score, no matter how many things are evaluated or contemplated in the criterion. A PASS means the completion complied with everything.
{{/if}}



Finally, output an explanation for your judgement and the score for the criterion, as exemplified below.
Your output should be a well-formatted JSON string that conforms to the JSON schema below. Do not output anything else other than the JSON string.

Here is the output JSON schema you must strictly follow, with field descriptions and value types. All fields are required.
{
  "reasoning": str,
  "score": Literal["PASS", "FAIL", "NA"]
}

reasoning: Reasoning string to support the rationale behind the score.{{#if examples.length}} If using an annotation, you MUST reference it explicitly (e.g., "Based on ANNOTATION 0...").{{/if}}
score: The literal score for the sample

Evaluate only the final COMPLETION TO EVALUATE with regard to the USER QUESTION shown. Do not return any preamble or explanations. Return exactly one valid JSON string.
{{#each examples}}

### ANNOTATION {{@index}} ###
CONTEXT
{{{context_str}}}
USER QUESTION
{{{user_question}}}
COMPLETION TO EVALUATE
{{{completion}}}
EVALUATION CRITERION
{{{../criteria}}}
OUTPUT
{{{output_json}}}
{{/each}}""",
            userTemplate=r"""CONTEXT
{{{context_str_without_last_user}}}

USER QUESTION
{{{last_user_turn_content}}}

COMPLETION TO EVALUATE
{{{completion}}}
{{#if examples.length}}

INSTRUCTIONS:
1. FIRST: Check if this exact case (or a very similar case) appears in the ANNOTATIONS above. If it does, you MUST use that annotation's score and reasoning. Do NOT second-guess it.
2. If similar cases exist in the annotations, follow the same logic and scoring pattern they demonstrate, even if it contradicts common sense.
3. ONLY if no relevant annotation exists, apply the general criterion: {{{criteria}}}

Remember: Annotations override everything else, including your intuition and the general criterion.
{{else}}

EVALUATION CRITERION
{{{criteria}}}
{{/if}}

OUTPUT SCHEMA
{{{output_schema}}}

OUTPUT""",
        )

        # Create grader config
        grader_config = GraderConfigInput(judge=judge_config)

        input_obj = GraderCreateInput(
            name=name or key,
            key=key,
            graderType=GraderTypeEnum.JUDGE,
            graderConfig=grader_config,
            metric=MetricGetOrCreate(existing=feedback_key),
        )

        return self._client._gql_client.create_grader(
            use_case=self.use_case_key(use_case), input=input_obj
        ).create_grader

    def prebuilt_judge(
        self,
        *,
        key: str,
        type: Literal["FAITHFULNESS"],
        judge_model: str,
        name: str | None = None,
        use_case: str | None = None,
    ) -> GraderData:
        """
        Creates a new prebuilt grader.

        Args:
            key: Unique key for the grader.
            type: Type of prebuild AI Judge Grader.
            judge_model: Model key of the judge model.
            name: Human-readable grader name. If omitted, derived from key.
            use_case: Explicit use-case key. Falls back to client.default_use_case.
        """
        # Create prebuilt config
        assert type == "FAITHFULNESS", "Only faithfulness prebuilt grader is supported for now"
        prebuilt_config = PrebuiltConfigInput(
            key=PrebuiltCriteriaKey(type),
            model=judge_model,
        )

        # Create grader config
        grader_config = GraderConfigInput(prebuilt=prebuilt_config)

        input_obj = GraderCreateInput(
            name=name or key,
            key=key,
            graderType=GraderTypeEnum.PREBUILT,
            graderConfig=grader_config,
            metric=None,
        )

        return self._client._gql_client.create_grader(
            use_case=self.use_case_key(use_case), input=input_obj
        ).create_grader

    def external_endpoint(
        self,
        *,
        key: str,
        url: str,
        feedback_key: str,
        name: str | None = None,
        use_case: str | None = None,
    ) -> GraderData:
        """
        Creates a new external feedback endpoint grader.

        Args:
            key: Unique key for the grader.
            url: URL of the remote grading service.
            feedback_key: Key for the feedback this grader writes to.
            name: Human-readable grader name. If omitted, derived from key.
            use_case: Explicit use-case key. Falls back to client.default_use_case.
        """
        # Create remote config
        remote_config = RemoteConfigInput(url=url)

        # Create grader config
        grader_config = GraderConfigInput(remote=remote_config)

        input_obj = GraderCreateInput(
            name=name or key,
            key=key,
            graderType=GraderTypeEnum.REMOTE,
            graderConfig=grader_config,
            metric=MetricGetOrCreate(existing=feedback_key),
        )

        return self._client._gql_client.create_grader(
            use_case=self.use_case_key(use_case), input=input_obj
        ).create_grader

    def custom(
        self,
        *,
        key: str,
        feedback_key: str,
        description: str | None = None,
        name: str | None = None,
        use_case: str | None = None,
    ) -> GraderData:
        """
        Creates a new custom grader.

        Args:
            key: Unique key for the grader.
            feedback_key: Key for the feedback this grader writes to.
            description: Description of what grader does.
            name: Human-readable grader name. If omitted, derived from key.
            use_case: Explicit use-case key. Falls back to client.default_use_case.
        """
        grader_config = GraderConfigInput(custom=CustomConfigInput(description=description))

        input_obj = GraderCreateInput(
            name=name or key,
            key=key,
            graderType=GraderTypeEnum.CUSTOM,
            graderConfig=grader_config,
            metric=MetricGetOrCreate(existing=feedback_key),
        )

        return self._client._gql_client.create_grader(
            use_case=self.use_case_key(use_case), input=input_obj
        ).create_grader


class AsyncGraderCreator(AsyncAPIResource, UseCaseResource):  # type: ignore[misc]
    """Async helper class for creating different types of graders with a clean API."""

    def __init__(self, client: AsyncAdaptive) -> None:
        AsyncAPIResource.__init__(self, client)
        UseCaseResource.__init__(self, client)

    async def binary_judge(
        self,
        *,
        key: str,
        criteria: str,
        judge_model: str,
        feedback_key: str,
        name: str | None = None,
        examples: list[input_types.JudgeExampleInput] | None = None,
        use_case: str | None = None,
    ) -> GraderData:
        """
        Creates a new judge-based grader.

        Args:
            key: Unique key for the grader.
            criteria: Natural-language explanation of what should be evaluated as a binary pass/fail.
            judge_model: Model key of the judge model.
            feedback_key: Key for the feedback this grader writes to.
            examples: List of annotated examples for few-shot prompting.
            name: Human-readable grader name. If omitted, derived from key.
            use_case: Explicit use-case key. Falls back to client.default_use_case.
        """
        # Parse examples if provided
        parsed_examples = []
        if examples:
            for ex in examples:
                if not all(k in ex for k in ("input", "output", "passes")):
                    raise ValueError("Each example must contain 'input', 'output', and 'passes' keys")
                input_turns = [JudgeExampleInputTurnEntry(role=t["role"], content=t["content"]) for t in ex["input"]]
                parsed_examples.append(
                    JudgeExampleInput(
                        input=input_turns,
                        output=ex["output"],
                        reasoning=ex.get("reasoning"),
                        **{"pass": bool(ex["passes"])},
                    )
                )

        # Create judge config
        judge_config = JudgeConfigInput(
            model=judge_model,
            criteria=criteria,
            examples=parsed_examples,
            systemTemplate=r"""You are an expert evaluator that evaluates completions generated by an AI model on a fixed criterion.
You will be given all elements of an interaction between human an AI model:
    The full context of the conversation so far leading up to the last user turn/question is under the CONTEXT header. It may contain extra contextual information.
    The last user turn/question is under the USER QUESTION header. It may contain extra contextual information.
    The model's completion is under the COMPLETION TO EVALUATE header.
    The evaluation criterion is under the EVALUATION CRITERION section.
{{#if examples.length}}

CRITICAL: The annotations below are GROUND TRUTH provided by expert human annotators. You MUST follow them exactly, even if they seem counter-intuitive to you.

In order to to analyze and score a completion, you always run the following steps without exception:
First, you read the CONTEXT, USER QUESTION and COMPLETION TO EVALUATE.
Then, you MUST check if there is an annotation that matches (or is very similar to) the current case:
  - If the USER QUESTION and COMPLETION TO EVALUATE match an annotation, you MUST use the annotation's score and reasoning. Do NOT apply your own judgment.
  - If there is a similar annotation, you MUST follow the same reasoning pattern and scoring approach, even if it contradicts your intuition.
  - The annotations define what is considered PASS/FAIL for this specific task. Your personal understanding of the criterion is IRRELEVANT.
Then, ONLY if no similar annotation exists, you analyze the COMPLETION TO EVALUATE and assign a score according to the criterion.

Rules to follow:
- You must always evaluate the COMPLETION TO EVALUATE based solely on the USER QUESTION, and never on an intermediary question that might have been asked in the CONTEXT. The CONTEXT is there for context only.
- Do not include text that is in the CONTEXT to make your judgement; you are evaluating the COMPLETION TO EVALUATE text only.
- You must not use the original instructions given to the model in the CONTEXT for your judgement. Focus only on the ANNOTATIONS AND EVALUATION CRITERION without any other influencing factors.
- You are forbidden to return a score other than PASS, FAIL or NA for each criterion.
- If the criterion is conditional, and is not applicable to the specific USER QUESTION + COMPLETION TO EVALUATE pair, you must score it as NA.
- Return a single score, no matter how many things are evaluated or contemplated in the criterion. A PASS means the completion complied with everything.
- ANNOTATIONS ARE ABSOLUTE TRUTH. If an annotation says something is PASS, it is PASS, regardless of what you think.
{{else}}

In order to to analyze and score a completion, you always run the following steps without exception:
First, you read the CONTEXT, USER QUESTION and COMPLETION TO EVALUATE.
Then, you analyze the COMPLETION TO EVALUATE, and assign it a PASS, FAIL or NA score according to the criterion: FAIL if the completion does not meet the criterion, PASS if it does, and NA if the criterion is not applicable to the example.

Rules to follow:
- You must always evaluate the COMPLETION TO EVALUATE based solely on the USER QUESTION, and never on an intermediary question that might have been asked in the CONTEXT. The CONTEXT is there for context only.
- Do not include text that is in the CONTEXT to make your judgement; you are evaluating the COMPLETION TO EVALUATE text only.
- You must not use the original instructions given to the model in the CONTEXT for your judgement. Focus only on the EVALUATION CRITERION.
- You are forbidden to return a score other than PASS, FAIL or NA for each criterion.
- If the criterion is conditional, and is not applicable to the specific USER QUESTION + COMPLETION TO EVALUATE pair, you must score it as NA.
- Return a single score, no matter how many things are evaluated or contemplated in the criterion. A PASS means the completion complied with everything.
{{/if}}



Finally, output an explanation for your judgement and the score for the criterion, as exemplified below.
Your output should be a well-formatted JSON string that conforms to the JSON schema below. Do not output anything else other than the JSON string.

Here is the output JSON schema you must strictly follow, with field descriptions and value types. All fields are required.
{
  "reasoning": str,
  "score": Literal["PASS", "FAIL", "NA"]
}

reasoning: Reasoning string to support the rationale behind the score.{{#if examples.length}} If using an annotation, you MUST reference it explicitly (e.g., "Based on ANNOTATION 0...").{{/if}}
score: The literal score for the sample

Evaluate only the final COMPLETION TO EVALUATE with regard to the USER QUESTION shown. Do not return any preamble or explanations. Return exactly one valid JSON string.
{{#each examples}}

### ANNOTATION {{@index}} ###
CONTEXT
{{{context_str}}}
USER QUESTION
{{{user_question}}}
COMPLETION TO EVALUATE
{{{completion}}}
EVALUATION CRITERION
{{{../criteria}}}
OUTPUT
{{{output_json}}}
{{/each}}""",
            userTemplate=r"""CONTEXT
{{{context_str_without_last_user}}}

USER QUESTION
{{{last_user_turn_content}}}

COMPLETION TO EVALUATE
{{{completion}}}
{{#if examples.length}}

INSTRUCTIONS:
1. FIRST: Check if this exact case (or a very similar case) appears in the ANNOTATIONS above. If it does, you MUST use that annotation's score and reasoning. Do NOT second-guess it.
2. If similar cases exist in the annotations, follow the same logic and scoring pattern they demonstrate, even if it contradicts common sense.
3. ONLY if no relevant annotation exists, apply the general criterion: {{{criteria}}}

Remember: Annotations override everything else, including your intuition and the general criterion.
{{else}}

EVALUATION CRITERION
{{{criteria}}}
{{/if}}

OUTPUT SCHEMA
{{{output_schema}}}

OUTPUT""",
        )

        # Create grader config
        grader_config = GraderConfigInput(judge=judge_config)

        input_obj = GraderCreateInput(
            name=name or key,
            key=key,
            graderType=GraderTypeEnum.JUDGE,
            graderConfig=grader_config,
            metric=MetricGetOrCreate(existing=feedback_key),
        )

        return (
            await self._client._gql_client.create_grader(use_case=self.use_case_key(use_case), input=input_obj)
        ).create_grader

    async def prebuilt_judge(
        self,
        *,
        key: str,
        type: Literal["FAITHFULNESS"],
        judge_model: str,
        name: str | None = None,
        use_case: str | None = None,
    ) -> GraderData:
        """
        Creates a new prebuilt grader.

        Args:
            key: Unique key for the grader.
            type: Type of prebuild AI Judge Grader.
            judge_model: Model key of the judge model.
            name: Human-readable grader name. If omitted, derived from key.
            use_case: Explicit use-case key. Falls back to client.default_use_case.
        """
        # Create prebuilt config
        assert type == "FAITHFULNESS", "Only faithfulness prebuilt grader is supported for now"
        prebuilt_config = PrebuiltConfigInput(
            key=PrebuiltCriteriaKey(type),
            model=judge_model,
        )

        # Create grader config
        grader_config = GraderConfigInput(prebuilt=prebuilt_config)

        input_obj = GraderCreateInput(
            name=name or key,
            key=key,
            graderType=GraderTypeEnum.PREBUILT,
            graderConfig=grader_config,
            metric=None,
        )

        return (
            await self._client._gql_client.create_grader(use_case=self.use_case_key(use_case), input=input_obj)
        ).create_grader

    async def external_endpoint(
        self,
        *,
        key: str,
        url: str,
        feedback_key: str,
        name: str | None = None,
        use_case: str | None = None,
    ) -> GraderData:
        """
        Creates a new external feedback endpoint grader.

        Args:
            key: Unique key for the grader.
            url: URL of the remote grading service.
            feedback_key: Existing key for the feedback this grader writes to.
            name: Human-readable grader name. If omitted, derived from key.
            use_case: Explicit use-case key. Falls back to client.default_use_case.
        """
        # Create remote config
        remote_config = RemoteConfigInput(url=url)

        # Create grader config
        grader_config = GraderConfigInput(remote=remote_config)

        input_obj = GraderCreateInput(
            name=name or key,
            key=key,
            graderType=GraderTypeEnum.REMOTE,
            graderConfig=grader_config,
            metric=MetricGetOrCreate(existing=feedback_key),
        )

        return (
            await self._client._gql_client.create_grader(use_case=self.use_case_key(use_case), input=input_obj)
        ).create_grader

    async def custom(
        self,
        *,
        key: str,
        feedback_key: str,
        description: str | None = None,
        name: str | None = None,
        use_case: str | None = None,
    ) -> GraderData:
        """
        Creates a new custom grader.

        Args:
            key: Unique key for the grader.
            feedback_key: Key for the feedback this grader writes to.
            description: Description of what grader does.
            name: Human-readable grader name. If omitted, derived from key.
            use_case: Explicit use-case key. Falls back to client.default_use_case.
        """
        grader_config = GraderConfigInput(custom=CustomConfigInput(description=description))

        input_obj = GraderCreateInput(
            name=name or key,
            key=key,
            graderType=GraderTypeEnum.CUSTOM,
            graderConfig=grader_config,
            metric=MetricGetOrCreate(existing=feedback_key),
        )

        return (
            await self._client._gql_client.create_grader(use_case=self.use_case_key(use_case), input=input_obj)
        ).create_grader


class Graders(SyncAPIResource, UseCaseResource):  # type: ignore[misc]
    """Resource to interact with grader definitions used to evaluate model completions."""

    def __init__(self, client: Adaptive) -> None:
        SyncAPIResource.__init__(self, client)
        UseCaseResource.__init__(self, client)

        # Nested creator for clean API
        self.create = GraderCreator(client)

    def delete(self, *, grader_key: str, use_case: str | None = None) -> bool:
        """Delete a grader. Returns True on success."""
        result = self._gql_client.delete_grader(use_case=self.use_case_key(use_case), id=grader_key).delete_grader
        return result.success

    def lock(self, *, grader_key: str, locked: bool, use_case: str | None = None) -> GraderData:
        """Lock or unlock a grader.

        Args:
            grader_key: ID or key of the grader.
            locked: Whether to lock (True) or unlock (False) the grader.
            use_case: Explicit use-case key. Falls back to client.default_use_case.
        """
        return self._gql_client.lock_grader(
            use_case=self.use_case_key(use_case), id=grader_key, locked=locked
        ).lock_grader

    def list(self, *, use_case: str | None = None) -> Sequence[GraderData]:
        """List all graders for the given use case."""
        return self._gql_client.list_graders(use_case=self.use_case_key(use_case)).graders

    def get(self, *, grader_key: str, use_case: str | None = None) -> GraderData | None:
        """Retrieve a specific grader by ID or key."""
        return self._gql_client.get_grader(id=grader_key, use_case=self.use_case_key(use_case)).grader

    def test_external_endpoint(
        self, url: str
    ) -> TestRemoteEnvTestRemoteEnvRemoteEnvTestOnline | TestRemoteEnvTestRemoteEnvRemoteEnvTestOffline:
        """Test external endpoint to check if it is reachable from Adaptive and returns a valid response."""
        return self._gql_client.test_remote_env(RemoteEnvCreate(url=url)).test_remote_env


class AsyncGraders(AsyncAPIResource, UseCaseResource):  # type: ignore[misc]
    """Asynchronous resource to interact with grader definitions used to evaluate model completions."""

    def __init__(self, client: AsyncAdaptive) -> None:
        AsyncAPIResource.__init__(self, client)
        UseCaseResource.__init__(self, client)

        # Nested creator for clean API
        self.create = AsyncGraderCreator(client)

    async def delete(self, *, grader_key: str, use_case: str | None = None) -> bool:
        """Delete a grader. Returns True on success."""
        result = (
            await self._gql_client.delete_grader(use_case=self.use_case_key(use_case), id=grader_key)
        ).delete_grader
        return result.success

    async def lock(self, *, grader_key: str, locked: bool, use_case: str | None = None) -> GraderData:
        """Lock or unlock a grader."""
        return (
            await self._gql_client.lock_grader(use_case=self.use_case_key(use_case), id=grader_key, locked=locked)
        ).lock_grader

    async def list(self, *, use_case: str | None = None) -> Sequence[GraderData]:
        """List all graders for the given use case."""
        return (await self._gql_client.list_graders(use_case=self.use_case_key(use_case))).graders

    async def get(self, *, grader_key: str, use_case: str | None = None) -> GraderData | None:
        """Retrieve a specific grader by ID or key."""
        return (await self._gql_client.get_grader(id=grader_key, use_case=self.use_case_key(use_case))).grader

    async def test_external_endpoint(
        self, url: str
    ) -> TestRemoteEnvTestRemoteEnvRemoteEnvTestOnline | TestRemoteEnvTestRemoteEnvRemoteEnvTestOffline:
        """Test external endpoint to check if it is reachable from Adaptive and returns a valid response."""
        return (await self._gql_client.test_remote_env(RemoteEnvCreate(url=url))).test_remote_env
