#
# Copyright 2026 The Dapr Authors
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#     http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

import logging
from typing import (
    Any,
    Callable,
    Iterator,
    Literal,
    Optional,
    Type,
    TypeVar,
    Union,
)

from pydantic import BaseModel

from dapr_agents.llm.utils.stream import StreamHandler
from dapr_agents.llm.utils.structure import StructureHandler
from dapr_agents.types.message import (
    LLMChatCandidateChunk,
    LLMChatResponse,
)

logger = logging.getLogger(__name__)
T = TypeVar("T", bound=BaseModel)


class ResponseHandler:
    """
    Handles both streaming and non-streaming chat completions from various LLM providers.
    """

    @staticmethod
    def process_response(
        response: Any,
        llm_provider: str,
        response_format: Optional[Type[T]] = None,
        structured_mode: Literal["json", "function_call"] = "json",
        stream: bool = False,
        on_chunk: Optional[Callable[[LLMChatCandidateChunk], None]] = None,
    ) -> Union[
        Iterator[LLMChatCandidateChunk],  # when streaming
        LLMChatResponse,  # non‑stream + no format
        T,  # non‑stream + single structured format
        list[T],  # non‑stream + list structured format
    ]:
        """
        Process a chat completion.

        - **Streaming** (`stream=True`):
          Yields `LLMChatCandidateChunk` via `StreamHandler`, honoring `on_chunk` / `on_final`.
        - **Non-streaming** (`stream=False`):
          1. Normalize provider envelope → `LLMChatResponse`.
          2. If no `response_format` requested, return that `LLMChatResponse`.
          3. Otherwise, extract the first assistant message, parse & validate it
             against your Pydantic `response_format`, and return the model (or list).

        Args:
            response:         Raw API return (stream iterator or full response object).
            llm_provider:     e.g. `"openai"`.
            response_format:  Optional Pydantic model (or `List[Model]`) for structured output.
            structured_mode:  `"json"` or `"function_call"` (only non-stream).
            stream:           Whether this is a streaming call.
            on_chunk:         Callback on every partial `LLMChatCandidateChunk`.

        Returns:
            • **streaming**: `Iterator[LLMChatCandidateChunk]`
            • **non-stream + no format**: full `LLMChatResponse`
            • **non-stream + format**: validated Pydantic model instance or `List[...]`
        """
        provider = llm_provider.lower()

        # ─── Streaming ─────────────────────────────────────────────────────────
        if stream:
            return StreamHandler.process_stream(
                stream=response,
                llm_provider=provider,
                on_chunk=on_chunk,
            )
        else:
            # ─── Non‑streaming ─────────────────────────────────────────────────────
            # 1) Normalize full response → LLMChatResponse
            if provider in ("openai", "nvidia"):
                from dapr_agents.llm.openai.utils import process_openai_chat_response

                llm_resp: LLMChatResponse = process_openai_chat_response(response)
            elif provider == "huggingface":
                from dapr_agents.llm.huggingface.utils import process_hf_chat_response

                llm_resp = process_hf_chat_response(response)
            elif provider == "dapr":
                from dapr_agents.llm.dapr.utils import process_dapr_chat_response

                llm_resp = process_dapr_chat_response(response)
            else:
                # if you add more providers, handle them here
                llm_resp = response

            # 2) If no structured format requested, return the full response
            if response_format is None:
                return llm_resp

            # 3) They did request a Pydantic model → extract first assistant message
            first_candidate = next(iter(llm_resp.results), None)
            if not first_candidate:
                raise ValueError("No candidates in LLMChatResponse")
            assistant = first_candidate.message

            # 3a) Get the raw JSON or function‐call payload
            raw = StructureHandler.extract_structured_response(
                message=assistant,
                llm_provider=llm_provider,
                structured_mode=structured_mode,
            )

            # 3b) Wrap List[Model] → IterableModel if needed
            fmt = StructureHandler.normalize_iterable_format(response_format)
            # 3c) Ensure exactly one Pydantic model inside
            model_cls = StructureHandler.resolve_response_model(fmt)
            if model_cls is None:
                raise TypeError(
                    f"Cannot resolve a Pydantic model from {response_format!r}"
                )

            # 3d) Validate JSON/dict → Pydantic
            validated = StructureHandler.validate_response(raw, fmt)
            logger.info("Structured output successfully validated.")

            # 3e) Return the validated model (don't unwrap iterable models)
            return validated
