import json
import os
from pathlib import Path
from typing import Any

from claude_agent_sdk import (
    ClaudeAgentOptions,
    ResultMessage,
    query,
)
from pydantic import BaseModel, create_model

from harbor.cli.quality_checker.models import (
    QualityCheckModel,
    QualityCheckResult,
    Rubric,
    load_rubric,
)
from harbor.models.task.paths import TaskPaths

SUPPORTED_MODELS = {"sonnet", "opus", "haiku"}


def _make_response_model(rubric: Rubric) -> type[BaseModel]:
    """Build a Pydantic model with explicit named fields for each rubric criterion."""
    fields: dict[str, Any] = {c.name: (QualityCheckModel, ...) for c in rubric.criteria}
    return create_model("QualityCheckResponse", **fields)


class QualityChecker:
    def __init__(
        self,
        task_dir: Path,
        model_name: str,
        rubric_path: Path | None = None,
    ):
        if model_name not in SUPPORTED_MODELS:
            raise ValueError(
                f"Unsupported model '{model_name}'. "
                f"Must be one of: {', '.join(sorted(SUPPORTED_MODELS))}"
            )

        self._task_dir = Path(task_dir)
        self._model_name = model_name
        self._rubric = load_rubric(rubric_path)
        self._response_model = _make_response_model(self._rubric)

        if not self._task_dir.exists() or not self._task_dir.is_dir():
            raise FileNotFoundError(
                f"Task directory '{task_dir}' not found or is not a directory"
            )

        self._task_paths = TaskPaths(self._task_dir)
        if not self._task_paths.is_valid():
            raise ValueError(f"Directory '{task_dir}' is not a valid task directory.")

    def _build_criteria_guidance(self) -> str:
        return "\n".join(f"- {c.name}: {c.guidance}" for c in self._rubric.criteria)

    def _build_json_structure(self) -> str:
        example = {
            c.name: {"outcome": "pass|fail|not_applicable", "explanation": "..."}
            for c in self._rubric.criteria
        }
        return json.dumps(example, indent=2)

    def _build_prompt(self) -> str:
        template_path = Path(__file__).parent / "check_prompt.txt"
        template = template_path.read_text()

        # Read task files
        instruction = self._task_paths.instruction_path.read_text()
        if self._task_paths.solve_path.exists():
            try:
                solution = self._task_paths.solve_path.read_text()
            except UnicodeDecodeError:
                solution = "<solve.sh exists but could not be decoded as UTF-8>"
        else:
            solution = "No solution/solve.sh file present"

        # Read all test files
        test_files = []
        if self._task_paths.tests_dir.exists():
            for test_file in sorted(self._task_paths.tests_dir.rglob("*test*")):
                if test_file.is_file():
                    rel_path = test_file.relative_to(self._task_paths.tests_dir)
                    try:
                        content = test_file.read_text()
                    except UnicodeDecodeError:
                        # skip binary files
                        continue
                    test_files.append(f"=== {rel_path} ===\n{content}")
        tests_str = "\n\n".join(test_files) if test_files else "No test files"

        # Read environment files
        environment_files = []
        if self._task_paths.environment_dir.exists():
            print(
                "Warning: tasks check currently only supports Dockerfile environments."
            )
            dockerfile = self._task_paths.environment_dir / "Dockerfile"
            if dockerfile.exists():
                content = dockerfile.read_text()
                environment_files.append(f"=== Dockerfile ===\n{content}")

        environment_files_str = (
            "\n\n".join(environment_files)
            if environment_files
            else "No environment files"
        )

        # Format prompt
        prompt = template.format(
            instruction=instruction,
            solution=solution,
            test_script=tests_str,
            environment_files=environment_files_str,
            criteria_guidance=self._build_criteria_guidance(),
            json_structure=self._build_json_structure(),
        )
        return prompt

    async def check(self) -> QualityCheckResult:
        if not os.environ.get("ANTHROPIC_API_KEY"):
            raise RuntimeError(
                "ANTHROPIC_API_KEY environment variable is required. "
                "Set it with: export ANTHROPIC_API_KEY=sk-ant-..."
            )

        prompt = self._build_prompt()

        options = ClaudeAgentOptions(
            permission_mode="bypassPermissions",
            allowed_tools=["Read", "Glob", "Grep"],
            cwd=str(self._task_dir),
            model=self._model_name,
            output_format={
                "type": "json_schema",
                "schema": self._response_model.model_json_schema(),
            },
        )
        structured_output = None
        async for message in query(prompt=prompt, options=options):
            if isinstance(message, ResultMessage):
                structured_output = message.structured_output

        if structured_output is None:
            raise ValueError("SDK did not return structured output")
        parsed = self._response_model.model_validate(structured_output)

        return QualityCheckResult(checks=parsed.model_dump())
