"""Prompt payload model with Langfuse prompt compilation."""

from __future__ import annotations

from inspect import Parameter, signature
from typing import Any, NoReturn

from pydantic import BaseModel, ConfigDict, Field, JsonValue, model_validator

from dr_langfuse.errors.codes import ErrorCode
from dr_langfuse.errors.error_envelope import ErrorEnvelope
from dr_langfuse.errors.langfuse_error import LangfuseError
from dr_langfuse.prompts.prompt_fetch_request import PromptFetchRequest


class PromptPayload(BaseModel):
    """Resolved prompt payload returned by Langfuse."""

    model_config = ConfigDict(arbitrary_types_allowed=True)

    prompt_name: str | None = Field(default=None, min_length=1)
    system_content: str = ""
    task_content: str | None = Field(default=None, min_length=1)
    label: str | None = None
    version: int | None = None

    # Construction inputs — excluded from serialization
    prompt: Any | None = Field(default=None, exclude=True, repr=False)
    request: PromptFetchRequest | None = Field(default=None, exclude=True, repr=False)

    @model_validator(mode="before")
    @classmethod
    def _resolve_from_prompt(cls, data: Any) -> Any:
        if not isinstance(data, dict):
            return data
        prompt = data.get("prompt")
        request = data.get("request")
        if prompt is None or not isinstance(request, PromptFetchRequest):
            return data

        compiled = compile_prompt_template(prompt, request.variables)
        system_content, task_content = extract_prompt_content(compiled)

        # Resolve label
        resolved_label = request.label
        if resolved_label is None:
            labels = getattr(prompt, "labels", None)
            if isinstance(labels, list) and labels:
                first_label = labels[0]
                if isinstance(first_label, str):
                    resolved_label = first_label
        if resolved_label is None:
            prompt_label = getattr(prompt, "label", None)
            if isinstance(prompt_label, str):
                resolved_label = prompt_label

        # Resolve version
        v = getattr(prompt, "version", None)
        resolved_version = v if v is not None else request.version

        data.setdefault("prompt_name", request.prompt_name)
        data.setdefault("system_content", system_content)
        data.setdefault("task_content", task_content)
        data.setdefault("label", resolved_label)
        data.setdefault("version", resolved_version)

        return data

    @model_validator(mode="after")
    def _require_required_fields(self) -> PromptPayload:
        if self.prompt_name is None:
            raise ValueError(
                "PromptPayload.prompt_name is required when prompt-based inference is unavailable"
            )
        if self.task_content is None:
            raise ValueError(
                "PromptPayload.task_content is required when prompt-based inference is unavailable"
            )
        return self


def compile_prompt_template(prompt: Any, variables: dict[str, JsonValue]) -> Any:
    """Compile a Langfuse prompt object with the given variables."""
    compile_fn = getattr(prompt, "compile", None)
    if not callable(compile_fn):
        raise LangfuseError(
            ErrorEnvelope(
                code=ErrorCode.MALFORMED_REQUEST,
                message="Langfuse prompt does not expose a callable compile method",
                retriable=False,
                details={
                    "operation": "fetch_prompt",
                    "reason": "missing_compile_method",
                },
            )
        )

    try:
        params = list(signature(compile_fn).parameters.values())
    except (TypeError, ValueError):
        try:
            return compile_fn(**variables)
        except TypeError as exc:
            _raise_compile_type_error(exc)

    accepts_keyword_args = any(param.kind == Parameter.VAR_KEYWORD for param in params)
    if not accepts_keyword_args and variables:
        accepted_names = {
            param.name
            for param in params
            if param.kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY)
        }
        unknown = sorted(name for name in variables if name not in accepted_names)
        if unknown:
            unknown_display = ", ".join(unknown)
            raise LangfuseError(
                ErrorEnvelope(
                    code=ErrorCode.MALFORMED_REQUEST,
                    message="Langfuse prompt.compile does not accept provided variables",
                    retriable=False,
                    details={
                        "operation": "fetch_prompt",
                        "reason": "unknown_compile_variables",
                        "unknown_variables": unknown_display,
                    },
                )
            )

    try:
        return compile_fn(**variables)
    except TypeError as exc:
        _raise_compile_type_error(exc)


def _raise_compile_type_error(exc: TypeError) -> NoReturn:
    raise LangfuseError(
        ErrorEnvelope(
            code=ErrorCode.MALFORMED_REQUEST,
            message="Langfuse prompt.compile rejected the provided variables",
            retriable=False,
            details={
                "operation": "fetch_prompt",
                "reason": "compile_invocation_type_error",
                "exception": str(exc),
            },
        )
    ) from exc


def normalize_text_content(value: Any) -> str:
    """Normalize LLM message content to a plain string."""
    if isinstance(value, str):
        return value
    if isinstance(value, list):
        parts: list[str] = []
        for item in value:
            text = normalize_text_content(item)
            if text:
                parts.append(text)
        return "\n".join(parts)
    if isinstance(value, dict):
        text_value = value.get("text")
        if isinstance(text_value, str):
            return text_value
        content_value = value.get("content")
        if isinstance(content_value, str):
            return content_value
    return ""


def extract_prompt_content(raw_prompt: Any) -> tuple[str, str]:
    """Extract (system_content, task_content) from a compiled chat prompt."""
    if not isinstance(raw_prompt, list):
        raise LangfuseError(
            ErrorEnvelope(
                code=ErrorCode.MALFORMED_REQUEST,
                message="Langfuse prompt must compile to a chat message list",
                retriable=False,
                details={"operation": "fetch_prompt", "reason": "invalid_prompt_shape"},
            )
        )

    system_parts: list[str] = []
    user_message_count = 0
    user_contents: list[str] = []
    for msg in raw_prompt:
        if not isinstance(msg, dict):
            raise LangfuseError(
                ErrorEnvelope(
                    code=ErrorCode.MALFORMED_REQUEST,
                    message="Langfuse prompt message must be an object",
                    retriable=False,
                    details={
                        "operation": "fetch_prompt",
                        "reason": "invalid_message_shape",
                    },
                )
            )

        role = str(msg.get("role", "")).lower()
        content = normalize_text_content(msg.get("content")).strip()
        if role == "system":
            if content:
                system_parts.append(content)
            continue
        if role == "user":
            user_message_count += 1
            if content:
                user_contents.append(content)
            continue
        raise LangfuseError(
            ErrorEnvelope(
                code=ErrorCode.MALFORMED_REQUEST,
                message="Langfuse prompt contains unsupported message role",
                retriable=False,
                details={"operation": "fetch_prompt", "reason": "invalid_message_role"},
            )
        )

    if user_message_count != 1:
        raise LangfuseError(
            ErrorEnvelope(
                code=ErrorCode.MALFORMED_REQUEST,
                message="Langfuse prompt must contain exactly one user message",
                retriable=False,
                details={
                    "operation": "fetch_prompt",
                    "reason": "invalid_user_message_count",
                },
            )
        )
    if len(user_contents) != 1:
        raise LangfuseError(
            ErrorEnvelope(
                code=ErrorCode.MALFORMED_REQUEST,
                message="Langfuse prompt user message content must be non-empty",
                retriable=False,
                details={
                    "operation": "fetch_prompt",
                    "reason": "empty_user_message_content",
                },
            )
        )
    return "\n\n".join(system_parts), user_contents[0]
