import html
from pathlib import Path

from mlflow.models.model import ModelInfo
from mlflow.models.signature import ModelSignature
from mlflow.types import schema
from mlflow.utils import databricks_utils


def _is_input_string(inputs: schema.Schema) -> bool:
    return (
        not inputs.has_input_names()
        and len(inputs.input_types()) == 1
        and inputs.input_types()[0] == schema.DataType.string
    )


def _is_input_agent_compatible(inputs: schema.Schema) -> bool:
    if _is_input_string(inputs):
        return True
    if not inputs.has_input_names():
        return False
    messages = inputs.input_dict().get("messages")
    if not messages:
        return False
    if not isinstance(messages.type, schema.Array):
        return False
    items = messages.type.dtype
    if not isinstance(items, schema.Object):
        return False
    properties = items.properties
    content = next(filter(lambda prop: prop.name == "content", properties), None)
    role = next(filter(lambda prop: prop.name == "role", properties), None)
    return (
        content
        and content.dtype == schema.DataType.string
        and role
        and role.dtype == schema.DataType.string
    )


def _is_output_string_response(outputs: schema.Schema) -> bool:
    if not outputs.has_input_names():
        return False
    content = outputs.input_dict().get("content")
    if not content:
        return False
    return content.type == schema.DataType.string


def _is_output_string(outputs: schema.Schema) -> bool:
    return (
        not outputs.has_input_names()
        and len(outputs.input_types()) == 1
        and outputs.input_types()[0] == schema.DataType.string
    )


def _is_output_chat_completion_response(outputs: schema.Schema) -> bool:
    if not outputs.has_input_names():
        return False
    choices = outputs.input_dict().get("choices")
    if not choices:
        return False
    if not isinstance(choices.type, schema.Array):
        return False
    items = choices.type.dtype
    if not isinstance(items, schema.Object):
        return False
    properties = items.properties
    message = next(filter(lambda prop: prop.name == "message", properties), None)
    if not message:
        return False
    if not isinstance(message.dtype, schema.Object):
        return False
    message_properties = message.dtype.properties
    content = next(filter(lambda prop: prop.name == "content", message_properties), None)
    role = next(filter(lambda prop: prop.name == "role", message_properties), None)
    return (
        content
        and content.dtype == schema.DataType.string
        and role
        and role.dtype == schema.DataType.string
    )


def _is_output_agent_compatible(outputs: schema.Schema) -> bool:
    return (
        _is_output_string_response(outputs)
        or _is_output_string(outputs)
        or _is_output_chat_completion_response(outputs)
    )


def _is_signature_agent_compatible(signature: ModelSignature) -> bool:
    """Determines whether the given signature is compatible with the agent eval schema.

    See https://docs.databricks.com/en/generative-ai/agent-evaluation/evaluation-schema.html.
    The schema accepts the OpenAI spec, as well as simpler formats such as vanilla string response
    and `StringResponse`.
    """
    return _is_input_agent_compatible(signature.inputs) and _is_output_agent_compatible(
        signature.outputs
    )


def _should_render_agent_eval_template(signature: ModelSignature) -> bool:
    if not databricks_utils.is_in_databricks_runtime():
        return False
    from IPython import get_ipython

    if get_ipython() is None:
        return False
    return _is_signature_agent_compatible(signature)


def _generate_agent_eval_recipe(model_uri: str) -> str:
    resources_dir = Path(__file__).parent / "notebook_resources"
    pip_install_command = """%pip install -U databricks-agents
dbutils.library.restartPython()
## Run the above in a separate cell ##"""
    eval_with_synthetic_code = (
        (resources_dir / "eval_with_synthetic_example.py")
        .read_text()
        .replace("{{pipInstall}}", pip_install_command)
        .replace("{{modelUri}}", model_uri)
    )
    eval_with_dataset_code = (
        (resources_dir / "eval_with_dataset_example.py")
        .read_text()
        .replace("{{pipInstall}}", pip_install_command)
        .replace("{{modelUri}}", model_uri)
    )

    # Remove the ruff noqa comments.
    ruff_line = "# ruff: noqa: F821, I001\n"
    eval_with_synthetic_code = eval_with_synthetic_code.replace(ruff_line, "")
    eval_with_dataset_code = eval_with_dataset_code.replace(ruff_line, "")

    return (
        (resources_dir / "agent_evaluation_template.html")
        .read_text()
        .replace("{{eval_with_synthetic_code}}", html.escape(eval_with_synthetic_code))
        .replace("{{eval_with_dataset_code}}", html.escape(eval_with_dataset_code))
    )


def maybe_render_agent_eval_recipe(model_info: ModelInfo) -> None:
    # For safety, we wrap in try/catch to make sure we don't break `mlflow.*.log_model`.
    try:
        if not _should_render_agent_eval_template(model_info.signature):
            return

        from IPython.display import HTML, display

        display(HTML(_generate_agent_eval_recipe(model_info.model_uri)))
    except Exception:
        pass
