"""General utility functions for the Swarm framework."""

import base64
import importlib
import json
import logging
from typing import Any, Dict, Optional, cast

logger = logging.getLogger(__name__)


def merge_fields(target: Any, source: Any) -> None:
    """Merge source fields into target fields."""
    for key, value in source.items():
        if isinstance(value, str):
            target[key] += value
        elif value is not None and isinstance(value, dict):
            merge_fields(target[key], value)


def merge_chunk(final_response: Dict[str, Any], delta: Dict[str, Any]) -> None:
    """Merge a streaming chunk into the final response."""
    delta.pop("role", None)
    merge_fields(final_response, delta)

    tool_calls = delta.get("tool_calls")
    if tool_calls and len(tool_calls) > 0:
        index = tool_calls[0].pop("index")
        merge_fields(final_response["tool_calls"][index], tool_calls[0])


def extract_interactive_display_text(
    content_json: str,
    *,
    tool_name: Optional[str] = None,
) -> str:
    """Extract display text from interactive tool content using a fallback chain.

    Tries data.text → data.label → data.title → top-level title.
    Returns "" if all fields are empty or parsing fails.

    Shared between DialogueRunner (persistence) and ChunkProcessor (TTS fallback)
    so both paths stay in sync.

    Args:
        content_json: JSON string from tool message content[0]["text"].
        tool_name: Optional tool name for warning logs.
    """
    try:
        content = json.loads(content_json) if content_json else {}
        data = content.get("data", {})
        for key in ("text", "label", "title"):
            value = data.get(key, "")
            if isinstance(value, str) and value.strip():
                return value.strip()
        # Top-level title fallback
        top_title = content.get("title", "")
        if isinstance(top_title, str) and top_title.strip():
            return top_title.strip()
        logger.warning(
            "interactive_display.no_text",
            extra={"tool_name": tool_name},
        )
        return ""
    except (json.JSONDecodeError, KeyError, IndexError):
        logger.warning(
            "interactive_display.parse_failed",
            extra={"tool_name": tool_name},
        )
        return ""


def image_to_base64(image_path: str, scale_factor: float = 0.5) -> str:
    """
    Convert an image to a base64-encoded string, with optional resizing.

    Args:
        image_path (str): The path to the image file.
        scale_factor (float, optional): Factor to scale the image dimensions. Defaults to 0.5.

    Returns:
        str: Base64-encoded string representation of the (optionally resized) image.

    Raises:
        IOError: If there's an error opening or processing the image file.
    """
    from io import BytesIO

    try:
        np = cast(Any, importlib.import_module("numpy"))
    except ImportError as exc:  # pragma: no cover - exercised via import patching
        raise ImportError(
            "image support requires numpy. Install it with "
            "'pip install \"swarm-agents[image]\"'."
        ) from exc

    with open(image_path, "rb") as image_file:
        img = np.frombuffer(image_file.read(), dtype=np.uint8)
        img = img.reshape((-1, 3))  # Assuming 3 channels (RGB)
        resized_img = img[:: int(1 / scale_factor)]
        buffer = BytesIO()
        np.save(buffer, resized_img, allow_pickle=False)
        return base64.b64encode(buffer.getvalue()).decode("utf-8")
