"""Shared helper utilities for MCP server tools."""

from __future__ import annotations

import json
import math
from typing import Any

_TRUTHY = {"true", "1", "yes", "on"}
_FALSY = {"false", "0", "no", "off"}


def coerce_bool(value: Any, default: bool | None = None) -> bool | None:
    """Attempt to coerce a loosely-typed value to a boolean."""
    if value is None:
        return default
    if isinstance(value, bool):
        return value
    if isinstance(value, str):
        lowered = value.strip().lower()
        if lowered in _TRUTHY:
            return True
        if lowered in _FALSY:
            return False
        return default
    return bool(value)


def parse_json_payload(value: Any) -> Any:
    """
    Attempt to parse a value that might be a JSON string into its native object.

    This is a tolerant parser used to handle cases where MCP clients or LLMs
    serialize complex objects (lists, dicts) into strings. It also handles
    scalar values like numbers, booleans, and null.

    Args:
        value: The input value (can be str, list, dict, etc.)

    Returns:
        The parsed JSON object/list if the input was a valid JSON string,
        or the original value if parsing failed or wasn't necessary.
    """
    if not isinstance(value, str):
        return value

    val_trimmed = value.strip()

    # Fast path: if it doesn't look like JSON structure, return as is
    if not (
        (val_trimmed.startswith("{") and val_trimmed.endswith("}")) or
        (val_trimmed.startswith("[") and val_trimmed.endswith("]")) or
        val_trimmed in ("true", "false", "null") or
        (val_trimmed.replace(".", "", 1).replace("-", "", 1).isdigit())
    ):
        return value

    try:
        return json.loads(value)
    except (json.JSONDecodeError, ValueError):
        # If parsing fails, assume it was meant to be a literal string
        return value


def coerce_int(value: Any, default: int | None = None) -> int | None:
    """Attempt to coerce a loosely-typed value to an integer."""
    if value is None:
        return default
    try:
        if isinstance(value, bool):
            return default
        if isinstance(value, int):
            return value
        s = str(value).strip()
        if s.lower() in ("", "none", "null"):
            return default
        return int(float(s))
    except Exception:
        return default


def coerce_float(value: Any, default: float | None = None) -> float | None:
    """Attempt to coerce a loosely-typed value to a float-like number."""
    if value is None:
        return default
    try:
        # Treat booleans as invalid numeric input instead of coercing to 0/1.
        if isinstance(value, bool):
            return default
        if isinstance(value, (int, float)):
            return float(value)
        s = str(value).strip()
        if s.lower() in ("", "none", "null"):
            return default
        return float(s)
    except (TypeError, ValueError):
        return default


def normalize_properties(value: Any) -> tuple[dict[str, Any] | None, str | None]:
    """
    Robustly normalize a properties parameter to a dict.

    Handles various input formats from MCP clients/LLMs:
    - None -> (None, None)
    - dict -> (dict, None)
    - JSON string -> (parsed_dict, None) or (None, error_message)
    - Invalid values -> (None, error_message)

    Returns:
        Tuple of (parsed_dict, error_message). If error_message is set, parsed_dict is None.
    """
    if value is None:
        return None, None

    # Already a dict - return as-is
    if isinstance(value, dict):
        return value, None

    # Try parsing as string
    if isinstance(value, str):
        # Check for obviously invalid values from serialization bugs
        if value in ("[object Object]", "undefined", "null", ""):
            return None, f"properties received invalid value: '{value}'. Expected a JSON object like {{\"key\": value}}"

        parsed = parse_json_payload(value)
        if isinstance(parsed, dict):
            return parsed, None

        return None, f"properties must be a JSON object (dict), got string that parsed to {type(parsed).__name__}"

    return None, f"properties must be a dict or JSON string, got {type(value).__name__}"


def normalize_vector3(value: Any, param_name: str = "vector") -> tuple[list[float] | None, str | None]:
    """
    Normalize a vector parameter to [x, y, z] format.

    Handles various input formats from MCP clients/LLMs:
    - None -> (None, None)
    - list/tuple [x, y, z] -> ([x, y, z], None)
    - dict {x, y, z} -> ([x, y, z], None)
    - JSON string "[x, y, z]" or "{x, y, z}" -> parsed and normalized
    - comma-separated string "x, y, z" -> ([x, y, z], None)

    Returns:
        Tuple of (parsed_vector, error_message). If error_message is set, parsed_vector is None.
    """
    if value is None:
        return None, None

    # Handle dict with x/y/z keys (e.g., {"x": 0, "y": 1, "z": 2})
    if isinstance(value, dict):
        if all(k in value for k in ("x", "y", "z")):
            try:
                vec = [float(value["x"]), float(value["y"]), float(value["z"])]
                if all(math.isfinite(n) for n in vec):
                    return vec, None
                return None, f"{param_name} values must be finite numbers, got {value}"
            except (ValueError, TypeError, KeyError):
                return None, f"{param_name} dict values must be numbers, got {value}"
        return None, f"{param_name} dict must have 'x', 'y', 'z' keys, got {list(value.keys())}"

    # If already a list/tuple with 3 elements, convert to floats
    if isinstance(value, (list, tuple)) and len(value) == 3:
        try:
            vec = [float(value[0]), float(value[1]), float(value[2])]
            if all(math.isfinite(n) for n in vec):
                return vec, None
            return None, f"{param_name} values must be finite numbers, got {value}"
        except (ValueError, TypeError):
            return None, f"{param_name} values must be numbers, got {value}"

    # Try parsing as string
    if isinstance(value, str):
        # Check for obviously invalid values
        if value in ("[object Object]", "undefined", "null", ""):
            return None, f"{param_name} received invalid value: '{value}'. Expected [x, y, z] array or {{x, y, z}} object"

        parsed = parse_json_payload(value)

        # Handle parsed dict
        if isinstance(parsed, dict):
            return normalize_vector3(parsed, param_name)

        # Handle parsed list
        if isinstance(parsed, list) and len(parsed) == 3:
            try:
                vec = [float(parsed[0]), float(parsed[1]), float(parsed[2])]
                if all(math.isfinite(n) for n in vec):
                    return vec, None
                return None, f"{param_name} values must be finite numbers, got {parsed}"
            except (ValueError, TypeError):
                return None, f"{param_name} values must be numbers, got {parsed}"

        # Handle comma-separated strings "1,2,3", "[1,2,3]", or "(1,2,3)"
        s = value.strip()
        if (s.startswith("[") and s.endswith("]")) or (s.startswith("(") and s.endswith(")")):
            s = s[1:-1]
        parts = [p.strip() for p in (s.split(",") if "," in s else s.split())]
        if len(parts) == 3:
            try:
                vec = [float(parts[0]), float(parts[1]), float(parts[2])]
                if all(math.isfinite(n) for n in vec):
                    return vec, None
                return None, f"{param_name} values must be finite numbers, got {value}"
            except (ValueError, TypeError):
                return None, f"{param_name} values must be numbers, got {value}"

        return None, f"{param_name} must be a [x, y, z] array or {{x, y, z}} object, got: {value}"

    return None, f"{param_name} must be a list, dict, or string, got {type(value).__name__}"


def normalize_string_list(value: Any, param_name: str = "list") -> tuple[list[str] | None, str | None]:
    """
    Normalize a string list parameter that might be a JSON string or plain string.

    Handles various input formats from MCP clients/LLMs:
    - None -> (None, None)
    - list/tuple of strings -> (list, None)
    - JSON string '["a", "b", "c"]' -> parsed and normalized
    - Plain non-JSON string "foo" -> treated as ["foo"]

    Returns:
        Tuple of (parsed_list, error_message). If error_message is set, parsed_list is None.
    """
    if value is None:
        return None, None

    # Already a list/tuple - validate and return
    if isinstance(value, (list, tuple)):
        # Ensure all elements are strings
        if all(isinstance(item, str) for item in value):
            return list(value), None
        return None, f"{param_name} must contain only strings, got mixed types"

    # Try parsing as JSON string (immediate parsing for string input)
    if isinstance(value, str):
        val_trimmed = value.strip()
        # Check for obviously invalid values
        if val_trimmed in ("[object Object]", "undefined", "null", ""):
            return None, f"{param_name} received invalid value: '{value}'. Expected a JSON array like [\"item1\", \"item2\"]"

        # Check if it looks like a JSON array but will fail to parse
        looks_like_json_array = (val_trimmed.startswith("[") and val_trimmed.endswith("]"))

        parsed = parse_json_payload(value)
        # If parsing succeeded and result is a list, validate and return
        if isinstance(parsed, list):
            # Validate all elements are strings
            if all(isinstance(item, str) for item in parsed):
                return parsed, None
            return None, f"{param_name} must contain only strings, got: {parsed}"
        # If parsing returned the original string but it looked like a JSON array,
        # it's malformed JSON - return error instead of treating as single item
        if parsed == value and looks_like_json_array:
            return None, f"{param_name} has invalid JSON syntax: '{value}'. Expected a valid JSON array like [\"item1\", \"item2\"]"
        # If parsing returned the original string (plain non-JSON), treat as single item
        if parsed == value:
            # Treat as single-element list
            return [value], None

        return None, f"{param_name} must be a JSON array (list), got string that parsed to {type(parsed).__name__}"

    return None, f"{param_name} must be a list or JSON string, got {type(value).__name__}"


def normalize_color(value: Any, output_range: str = "float") -> tuple[list[float] | None, str | None]:
    """
    Normalize a color parameter to [r, g, b, a] format.

    Handles various input formats from MCP clients/LLMs:
    - None -> (None, None)
    - list/tuple [r, g, b] or [r, g, b, a] -> normalized with optional alpha
    - dict {r, g, b} or {r, g, b, a} -> converted to list
    - hex string "#RGB", "#RRGGBB", "#RRGGBBAA" -> parsed to [r, g, b, a]
    - JSON string -> parsed and normalized

    Args:
        value: The color value to normalize
        output_range: "float" for 0.0-1.0 range, "int" for 0-255 range

    Returns:
        Tuple of (parsed_color, error_message). If error_message is set, parsed_color is None.
    """
    if value is None:
        return None, None

    def _to_output_range(components: list[float], from_hex: bool = False) -> list:
        """Convert color components to the requested output range."""
        if output_range == "int":
            if from_hex:
                # Already 0-255 from hex parsing
                return [int(c) for c in components]
            # Check if input is normalized (0-1) or already 0-255
            if all(0 <= c <= 1 for c in components):
                return [int(round(c * 255)) for c in components]
            return [int(c) for c in components]
        else:  # float
            if from_hex:
                # Convert 0-255 to 0-1
                return [c / 255.0 for c in components]
            if any(c > 1 for c in components):
                return [c / 255.0 for c in components]
            return [float(c) for c in components]

    # Handle dict with r/g/b keys
    if isinstance(value, dict):
        if all(k in value for k in ("r", "g", "b")):
            try:
                color = [float(value["r"]), float(value["g"]), float(value["b"])]
                if "a" in value:
                    color.append(float(value["a"]))
                else:
                    if output_range == "int" and all(0 <= c <= 1 for c in color):
                        color.append(1.0)
                    else:
                        color.append(1.0 if output_range == "float" else 255)
                return _to_output_range(color), None
            except (ValueError, TypeError, KeyError):
                return None, f"color dict values must be numbers, got {value}"
        return None, f"color dict must have 'r', 'g', 'b' keys, got {list(value.keys())}"

    # Already a list/tuple - validate
    if isinstance(value, (list, tuple)):
        if len(value) in (3, 4):
            try:
                color = [float(c) for c in value]
                if len(color) == 3:
                    if output_range == "int" and all(0 <= c <= 1 for c in color):
                        color.append(1.0)
                    else:
                        color.append(1.0 if output_range == "float" else 255)
                return _to_output_range(color), None
            except (ValueError, TypeError):
                return None, f"color values must be numbers, got {value}"
        return None, f"color must have 3 or 4 components, got {len(value)}"

    # Try parsing as string
    if isinstance(value, str):
        if value in ("[object Object]", "undefined", "null", ""):
            return None, f"color received invalid value: '{value}'. Expected [r, g, b, a] or {{r, g, b, a}}"

        # Handle hex colors
        if value.startswith("#"):
            h = value.lstrip("#")
            try:
                if len(h) == 3:
                    # Short form #RGB -> expand to #RRGGBB
                    components = [int(c + c, 16) for c in h] + [255]
                    return _to_output_range(components, from_hex=True), None
                elif len(h) == 6:
                    components = [int(h[i:i+2], 16) for i in (0, 2, 4)] + [255]
                    return _to_output_range(components, from_hex=True), None
                elif len(h) == 8:
                    components = [int(h[i:i+2], 16) for i in (0, 2, 4, 6)]
                    return _to_output_range(components, from_hex=True), None
            except ValueError:
                return None, f"Invalid hex color: {value}"
            return None, f"Invalid hex color length: {value}"

        # Try parsing as JSON
        parsed = parse_json_payload(value)

        # Handle parsed dict
        if isinstance(parsed, dict):
            return normalize_color(parsed, output_range)

        # Handle parsed list
        if isinstance(parsed, (list, tuple)) and len(parsed) in (3, 4):
            try:
                color = [float(c) for c in parsed]
                if len(color) == 3:
                    if output_range == "int" and all(0 <= c <= 1 for c in color):
                        color.append(1.0)
                    else:
                        color.append(1.0 if output_range == "float" else 255)
                return _to_output_range(color), None
            except (ValueError, TypeError):
                return None, f"color values must be numbers, got {parsed}"

        # Handle tuple-style strings "(r, g, b)" or "(r, g, b, a)"
        s = value.strip()
        if (s.startswith("[") and s.endswith("]")) or (s.startswith("(") and s.endswith(")")):
            s = s[1:-1]
        parts = [p.strip() for p in s.split(",")]
        if len(parts) in (3, 4):
            try:
                color = [float(p) for p in parts]
                if len(color) == 3:
                    if output_range == "int" and all(0 <= c <= 1 for c in color):
                        color.append(1.0)
                    else:
                        color.append(1.0 if output_range == "float" else 255)
                return _to_output_range(color), None
            except (ValueError, TypeError):
                pass  # Fall through to error message

        return None, f"Failed to parse color string: {value}"

    return None, f"color must be a list, dict, hex string, or JSON string, got {type(value).__name__}"


def extract_screenshot_images(response: dict[str, Any]) -> "ToolResult | None":
    """If a Unity response contains inline base64 images, return a ToolResult
    with TextContent + ImageContent blocks. Returns None for normal text-only responses.

    Shared screenshot handling (used by manage_camera).
    """
    from fastmcp.server.server import ToolResult
    from mcp.types import TextContent, ImageContent

    if not isinstance(response, dict) or not response.get("success"):
        return None

    data = response.get("data")
    if not isinstance(data, dict):
        return None

    # Batch images (surround/orbit mode) — multiple screenshots in one response
    screenshots = data.get("screenshots")
    if screenshots and isinstance(screenshots, list):
        blocks: list[TextContent | ImageContent] = []
        summary_screenshots = []
        for s in screenshots:
            summary_screenshots.append({k: v for k, v in s.items() if k != "imageBase64"})
        text_result = {
            "success": True,
            "message": response.get("message", ""),
            "data": {
                "sceneCenter": data.get("sceneCenter"),
                "sceneRadius": data.get("sceneRadius"),
                "screenshots": summary_screenshots,
            },
        }
        blocks.append(TextContent(type="text", text=json.dumps(text_result)))
        for s in screenshots:
            b64 = s.get("imageBase64")
            if b64:
                blocks.append(TextContent(type="text", text=f"[Angle: {s.get('angle', '?')}]"))
                blocks.append(ImageContent(type="image", data=b64, mimeType="image/png"))
        return ToolResult(content=blocks)

    # Single image (include_image or positioned capture) or contact sheet
    image_b64 = data.get("imageBase64")
    if not image_b64:
        return None
    text_data = {k: v for k, v in data.items() if k != "imageBase64"}
    text_result = {"success": True, "message": response.get("message", ""), "data": text_data}
    return ToolResult(
        content=[
            TextContent(type="text", text=json.dumps(text_result)),
            ImageContent(type="image", data=image_b64, mimeType="image/png"),
        ],
    )


def build_screenshot_params(
    params: dict[str, Any],
    *,
    screenshot_file_name: str | None = None,
    screenshot_super_size: int | str | None = None,
    camera: str | None = None,
    include_image: bool | str | None = None,
    max_resolution: int | str | None = None,
    capture_source: str | None = None,
    batch: str | None = None,
    view_target: str | int | list[float] | None = None,
    orbit_angles: int | str | None = None,
    orbit_elevations: list[float] | str | None = None,
    orbit_distance: float | str | None = None,
    orbit_fov: float | str | None = None,
    view_position: list[float] | str | None = None,
    view_rotation: list[float] | str | None = None,
) -> dict[str, Any] | None:
    """Populate screenshot-related keys in *params* dict. Returns an error dict
    if validation fails, or None on success.

    Shared screenshot handling (used by manage_camera).
    """
    if screenshot_file_name:
        params["fileName"] = screenshot_file_name
    coerced_super_size = coerce_int(screenshot_super_size, default=None)
    if coerced_super_size is not None:
        params["superSize"] = coerced_super_size
    if camera:
        params["camera"] = camera
    coerced_include_image = coerce_bool(include_image, default=None)
    if coerced_include_image is not None:
        params["includeImage"] = coerced_include_image
    coerced_max_resolution = coerce_int(max_resolution, default=None)
    if coerced_max_resolution is not None:
        if coerced_max_resolution <= 0:
            return {"success": False, "message": "max_resolution must be a positive integer."}
        params["maxResolution"] = coerced_max_resolution
    if capture_source is not None:
        normalized_capture_source = str(capture_source).strip().lower()
        if normalized_capture_source not in {"game_view", "scene_view"}:
            return {
                "success": False,
                "message": "capture_source must be either 'game_view' or 'scene_view'.",
            }
        params["captureSource"] = normalized_capture_source
    if batch:
        params["batch"] = batch
    if view_target is not None:
        params["viewTarget"] = view_target

    # Orbit params
    coerced_orbit_angles = coerce_int(orbit_angles, default=None)
    if coerced_orbit_angles is not None:
        params["orbitAngles"] = coerced_orbit_angles
    if orbit_elevations is not None:
        if isinstance(orbit_elevations, str):
            try:
                orbit_elevations = json.loads(orbit_elevations)
            except (ValueError, TypeError):
                return {"success": False, "message": "orbit_elevations must be a JSON array of floats."}
        if not isinstance(orbit_elevations, list) or not all(
            isinstance(v, (int, float)) for v in orbit_elevations
        ):
            return {"success": False, "message": "orbit_elevations must be a list of numbers."}
        params["orbitElevations"] = orbit_elevations
    coerced_orbit_distance = coerce_float(orbit_distance, default=None)
    if orbit_distance is not None and coerced_orbit_distance is None:
        return {"success": False, "message": "orbit_distance must be a number."}
    if coerced_orbit_distance is not None:
        params["orbitDistance"] = coerced_orbit_distance
    coerced_orbit_fov = coerce_float(orbit_fov, default=None)
    if orbit_fov is not None and coerced_orbit_fov is None:
        return {"success": False, "message": "orbit_fov must be a number."}
    if coerced_orbit_fov is not None:
        params["orbitFov"] = coerced_orbit_fov
    if view_position is not None:
        vec, err = normalize_vector3(view_position, "view_position")
        if err:
            return {"success": False, "message": err}
        params["viewPosition"] = vec
    if view_rotation is not None:
        vec, err = normalize_vector3(view_rotation, "view_rotation")
        if err:
            return {"success": False, "message": err}
        params["viewRotation"] = vec
    if params.get("captureSource") == "scene_view":
        if coerced_super_size is not None and coerced_super_size > 1:
            return {
                "success": False,
                "message": "capture_source='scene_view' does not support super_size above 1.",
            }
        if batch:
            return {
                "success": False,
                "message": "capture_source='scene_view' does not support batch modes.",
            }
        if view_position is not None or view_rotation is not None:
            return {
                "success": False,
                "message": "capture_source='scene_view' does not support view_position/view_rotation.",
            }
        if camera:
            return {
                "success": False,
                "message": "capture_source='scene_view' does not support camera selection.",
            }

    return None
