"""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__}"
