"""Test quality review endpoint.

Reviews AI-generated test sources for unrealistic patterns (cache warm-up,
internal state manipulation, identical inputs, etc.) and returns per-function
verdicts of "pass" or "repair" with reasons.

Currently enabled for Python only — other languages return all-pass immediately.
"""

from __future__ import annotations

import asyncio
import json
import logging
from pathlib import Path
from typing import Any

import sentry_sdk
from jinja2 import Environment, FileSystemLoader, StrictUndefined
from ninja import NinjaAPI
from openai.types.chat import (
    ChatCompletionMessageParam,
    ChatCompletionSystemMessageParam,
    ChatCompletionUserMessageParam,
)

from aiservice.analytics.posthog import ph
from aiservice.common.markdown_utils import extract_code_block_with_context
from aiservice.common_utils import validate_trace_id
from aiservice.llm import llm_client
from aiservice.llm_models import HAIKU_MODEL
from authapp.auth import AuthenticatedRequest
from core.shared.testgen_review.models import (
    CoverageDetails,
    FunctionVerdict,
    TestgenReviewErrorSchema,
    TestgenReviewResponseSchema,
    TestgenReviewSchema,
    TestReview,
)

testgen_review_api = NinjaAPI(urls_namespace="testgen_review")

_prompts_dir = Path(__file__).parent / "prompts"
_jinja_env = Environment(loader=FileSystemLoader(_prompts_dir), keep_trailing_newline=True, undefined=StrictUndefined)  # noqa: S701


@testgen_review_api.post(
    "/", response={200: TestgenReviewResponseSchema, 400: TestgenReviewErrorSchema, 500: TestgenReviewErrorSchema}
)
async def testgen_review(
    request: AuthenticatedRequest, data: TestgenReviewSchema
) -> tuple[int, TestgenReviewResponseSchema | TestgenReviewErrorSchema]:
    # Only enabled for Python for now
    if data.language != "python":
        return 200, TestgenReviewResponseSchema(reviews=[])

    if not validate_trace_id(data.trace_id):
        return 400, TestgenReviewErrorSchema(error="Invalid trace ID. Please provide a valid UUIDv4.")

    ph(request.user, "aiservice-testgen-review-called")

    try:
        coverage_context = (
            _build_coverage_context(data.coverage_details, data.function_source_code) if data.coverage_details else ""
        )

        async with asyncio.TaskGroup() as tg:
            tasks = [
                tg.create_task(
                    _review_single_test(
                        test_source=test_entry.test_source,
                        test_index=test_entry.test_index,
                        failed_test_functions=test_entry.failed_test_functions,
                        failure_messages=test_entry.failure_messages,
                        function_source_code=data.function_source_code,
                        function_name=data.function_name,
                        trace_id=data.trace_id,
                        user_id=request.user,
                        call_sequence=data.call_sequence,
                        coverage_summary=data.coverage_summary,
                        coverage_context=coverage_context,
                    )
                )
                for test_entry in data.tests
            ]
        reviews = [task.result() for task in tasks]

        ph(request.user, "aiservice-testgen-review-completed", properties={"trace_id": data.trace_id})
        return 200, TestgenReviewResponseSchema(reviews=reviews)

    except Exception as e:
        logging.exception("Error in testgen_review")
        sentry_sdk.capture_exception(e)
        return 500, TestgenReviewErrorSchema(error="Internal server error")


async def _review_single_test(
    test_source: str,
    test_index: int,
    failed_test_functions: list[str],
    failure_messages: dict[str, str],
    function_source_code: str,
    function_name: str,
    trace_id: str,
    user_id: str,
    call_sequence: int | None,
    coverage_summary: str = "",
    coverage_context: str = "",
) -> TestReview:
    # Functions that failed behavioral tests are definitively bad — include error details in reason
    failed_verdicts = [
        FunctionVerdict(
            function_name=fn,
            verdict="repair",
            reason=failure_messages.get(fn, "Failed behavioral test against original code"),
        )
        for fn in failed_test_functions
    ]

    # Build prompt for AI review of passing functions — include error context so the AI can see patterns
    failed_note = ""
    if failed_test_functions:
        failed_lines = []
        for fn in failed_test_functions:
            error_msg = failure_messages.get(fn, "")
            if error_msg:
                failed_lines.append(f"- `{fn}`: {error_msg}")
            else:
                failed_lines.append(f"- `{fn}`")
        failed_note = (
            "Note: The following functions failed behavioral tests and are already flagged for repair. "
            "Do NOT include them in your review — only review the remaining functions.\n" + "\n".join(failed_lines)
        )

    system_prompt = _jinja_env.get_template("testgen_review_system_prompt.md.j2").render()
    user_prompt = _jinja_env.get_template("testgen_review_user_prompt.md.j2").render(
        function_name=function_name,
        function_source_code=function_source_code,
        test_source=test_source,
        failed_note=failed_note,
        coverage_summary=coverage_summary,
        coverage_context=coverage_context,
    )

    messages: list[ChatCompletionMessageParam] = [
        ChatCompletionSystemMessageParam(role="system", content=system_prompt),
        ChatCompletionUserMessageParam(role="user", content=user_prompt),
    ]

    obs_context: dict[str, Any] = {}
    if call_sequence is not None:
        obs_context["call_sequence"] = call_sequence

    response = await llm_client.call(
        llm=HAIKU_MODEL,
        messages=messages,
        call_type="testgen_review",
        trace_id=trace_id,
        user_id=user_id,
        context=obs_context,
    )

    cost = response.cost
    logging.debug("testgen_review LLM cost: %s", cost)

    ai_verdicts = _parse_review_response(response.content.strip())
    return TestReview(test_index=test_index, functions=failed_verdicts + ai_verdicts)


def _parse_review_response(review_text: str) -> list[FunctionVerdict]:
    if result := extract_code_block_with_context(review_text, language="json"):
        _before, json_content, _after = result
        try:
            data = json.loads(json_content.strip())
            # Support both formats: {"functions": [...]} or [...]
            fn_list = data if isinstance(data, list) else data.get("functions", [])
            return [
                FunctionVerdict(
                    function_name=fn.get("function_name", ""),
                    verdict=fn.get("verdict", "pass"),
                    reason=fn.get("reason", ""),
                )
                for fn in fn_list
                if fn.get("verdict") == "repair"
            ]
        except (json.JSONDecodeError, AttributeError):
            logging.warning("testgen_review: failed to parse AI response JSON")
    return []


def _build_coverage_context(details: CoverageDetails, function_source_code: str = "") -> str:
    source_lines: dict[int, str] = {}
    if function_source_code:
        for i, line in enumerate(function_source_code.splitlines(), start=1):
            source_lines[i] = line

    lines = [f"Coverage: {details.coverage_percentage:.1f}% (threshold: {details.threshold_percentage:.0f}%)"]
    below_threshold = details.coverage_percentage < details.threshold_percentage
    if below_threshold:
        lines.append(
            "⚠ Coverage is BELOW the required threshold — tests that only hit trivial/early-return paths should be flagged for repair."
        )

    mc = details.main_function
    lines.append(f"\n{mc.name}: {mc.coverage:.1f}% coverage")
    if mc.unexecuted_lines and source_lines:
        lines.append("  Unexecuted lines:")
        for ln in mc.unexecuted_lines:
            code = source_lines.get(ln, "")
            lines.append(f"    L{ln}: {code}")
    elif mc.unexecuted_lines:
        lines.append(f"  Unexecuted lines: {mc.unexecuted_lines}")

    if details.dependent_function:
        dc = details.dependent_function
        lines.append(f"\n{dc.name}: {dc.coverage:.1f}% coverage")
        if dc.unexecuted_lines and source_lines:
            lines.append("  Unexecuted lines:")
            for ln in dc.unexecuted_lines:
                code = source_lines.get(ln, "")
                lines.append(f"    L{ln}: {code}")
        elif dc.unexecuted_lines:
            lines.append(f"  Unexecuted lines: {dc.unexecuted_lines}")

    return "\n".join(lines)
