import asyncio
import json
import os
from dataclasses import dataclass
from pathlib import Path
from statistics import mean
from typing import Optional

from dotenv import load_dotenv
from jinja2 import Template
from openai import AsyncAzureOpenAI
from typing_extensions import Never
from agent_framework import Executor, WorkflowBuilder, WorkflowContext, handler

load_dotenv()

# Load Jinja2 prompt templates
_TEMPLATES_DIR = Path(__file__).parent
_ANSWER_RELEVANCE_TEMPLATE = Template(
    _TEMPLATES_DIR.joinpath("answer_relevance.jinja2").read_text(encoding="utf-8")
)
_CONVERSATION_QUALITY_TEMPLATE = Template(
    _TEMPLATES_DIR.joinpath("conversation_quality_prompt.jinja2").read_text(encoding="utf-8")
)
_CREATIVITY_TEMPLATE = Template(
    _TEMPLATES_DIR.joinpath("creativity.jinja2").read_text(encoding="utf-8")
)
_GROUNDING_PROMPT = _TEMPLATES_DIR.joinpath("grounding_prompt.jinja2").read_text(encoding="utf-8")

SUPPORTED_METRICS = ("answer_relevance", "conversation_quality", "creativity", "grounding")


@dataclass
class EvalInput:
    chat_history: list
    metrics: str = "creativity,conversation_quality,answer_relevance,grounding"


def _select_metrics(metrics_str: str) -> dict:
    user_selected = [m.strip() for m in metrics_str.split(",") if m.strip()]
    return {m: (m in user_selected) for m in SUPPORTED_METRICS}


def _validate_input(chat_history: list, selected_metrics: dict) -> dict:
    dict_metric_required_fields = {
        "answer_relevance": {"question", "answer"},
        "conversation_quality": {"question", "answer"},
        "creativity": {"question", "answer"},
        "grounding": {"answer", "context"},
    }
    actual_input_cols = set()
    for item in chat_history[:1]:
        actual_input_cols.update(item.get("inputs", {}).keys())
        actual_input_cols.update(item.get("outputs", {}).keys())

    data_validation = dict(selected_metrics)
    for metric in selected_metrics:
        if selected_metrics[metric]:
            if not dict_metric_required_fields[metric] <= actual_input_cols:
                data_validation[metric] = False
    return data_validation


def _convert_chat_history_to_conversation(chat_history: list) -> str:
    conversation = ""
    for i in chat_history:
        conversation += f"User: {i['inputs']['question']}\nBot: {i['outputs']['answer']}\n"
    return conversation


def _get_score(result: Optional[str]) -> Optional[float]:
    if result is None:
        return None
    try:
        result_dict = json.loads(result)
        score = result_dict.get("score", None)
        return score
    except json.JSONDecodeError:
        return None


class MultiTurnMetricsExecutor(Executor):
    """Evaluates multi-turn conversations on up to 4 metrics.

    Handles conditional metric activation (gap #4), multi-turn grounding iteration
    (gap #10), and dot-notation output access (gap #11) all within a single executor.
    """

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self._client = AsyncAzureOpenAI(
            azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"],
            api_version=os.environ.get("AZURE_OPENAI_API_VERSION", "2024-02-01"),
            api_key=os.environ["AZURE_OPENAI_API_KEY"],
        )
        self._deployment = os.environ.get("AZURE_OPENAI_DEPLOYMENT", "gpt-4")

    async def _llm_call(self, prompt: str) -> str:
        response = await self._client.chat.completions.create(
            model=self._deployment,
            messages=[{"role": "system", "content": prompt}],
            temperature=0,
            top_p=1,
            presence_penalty=0,
            frequency_penalty=0,
        )
        return response.choices[0].message.content or ""

    async def _eval_answer_relevance(self, conversation: str) -> Optional[float]:
        prompt = _ANSWER_RELEVANCE_TEMPLATE.render(conversation=conversation)
        result = await self._llm_call(prompt)
        return _get_score(result)

    async def _eval_conversation_quality(self, conversation: str) -> Optional[float]:
        prompt = _CONVERSATION_QUALITY_TEMPLATE.render(conversation=conversation)
        result = await self._llm_call(prompt)
        return _get_score(result)

    async def _eval_creativity(self, conversation: str) -> Optional[float]:
        prompt = _CREATIVITY_TEMPLATE.render(conversation=conversation)
        result = await self._llm_call(prompt)
        return _get_score(result)

    async def _eval_grounding(self, chat_history: list) -> Optional[float]:
        """Iterates through chat_history turns, calls LLM per turn, averages scores (gap #10)."""
        scores = []
        for item in chat_history:
            context = item["outputs"].get("context", "")
            answer = item["outputs"].get("answer", "")
            prompt = _GROUNDING_PROMPT.replace("{context}", context).replace("{answer}", answer)
            result = await self._llm_call(prompt)
            try:
                scores.append(int(result.strip()))
            except (ValueError, TypeError):
                pass
        if scores:
            return mean(scores)
        return None

    @handler
    async def evaluate(self, input: EvalInput, ctx: WorkflowContext[Never, dict]) -> None:
        selected = _select_metrics(input.metrics)
        validated = _validate_input(input.chat_history, selected)
        conversation = _convert_chat_history_to_conversation(input.chat_history)

        tasks = {}
        if validated.get("answer_relevance"):
            tasks["answer_relevance"] = self._eval_answer_relevance(conversation)
        if validated.get("conversation_quality"):
            tasks["conversation_quality"] = self._eval_conversation_quality(conversation)
        if validated.get("creativity"):
            tasks["creativity"] = self._eval_creativity(conversation)
        if validated.get("grounding"):
            tasks["grounding"] = self._eval_grounding(input.chat_history)

        results_dict = {}
        if tasks:
            keys = list(tasks.keys())
            values = await asyncio.gather(*tasks.values())
            for k, v in zip(keys, values):
                results_dict[k] = v

        # Fill in None for metrics that were not computed
        for metric in SUPPORTED_METRICS:
            if metric not in results_dict:
                results_dict[metric] = None

        await ctx.yield_output(results_dict)


def create_workflow():
    _executor = MultiTurnMetricsExecutor(id="multi_turn_metrics")
    return WorkflowBuilder(name="EvalMultiTurnMetricsRow", start_executor=_executor).build()
