"""Typed argument parsing for MCP tool calls.

Supports:
  - JSON string: '{"key": "value"}'
  - Key=value pairs: key=value key2=value2  (schema-aware coercion)
  - Raw JSON values: key:='{"nested": true}'
  - File input: -f args.json
  - Stdin: --stdin or piped data (auto-detected, non-blocking)

When a tool schema is provided, key=value coercion uses the declared types
from the server — no guessing.  Without a schema, conservative coercion is
applied (bool, null, int, JSON structures; floats stay as strings).
"""

import json
import select
import sys
from pathlib import Path
from typing import Any


class ArgParseError(Exception):
    pass


def _stdin_has_data() -> bool:
    """Non-blocking check for data available on stdin."""
    if sys.stdin.isatty():
        return False
    try:
        ready, _, _ = select.select([sys.stdin], [], [], 0)
        return bool(ready)
    except (ValueError, OSError):
        return False


def parse_tool_args(
    args: tuple[str, ...],
    file_path: str | None = None,
    from_stdin: bool = False,
    schema: dict[str, Any] | None = None,
) -> dict:
    """Parse mixed argument formats into a dict for call_tool.

    Args:
        args: CLI positional arguments (key=value pairs or a JSON string).
        file_path: Path to a JSON file containing arguments.
        from_stdin: Explicitly read JSON from stdin (blocks until EOF).
        schema: Tool inputSchema dict (with "properties") for type-aware
                 coercion of key=value pairs.  Optional.
    """
    if from_stdin:
        raw = sys.stdin.read().strip()
        if not raw:
            return {}
        try:
            return json.loads(raw)
        except json.JSONDecodeError as exc:
            raise ArgParseError(f"Invalid JSON from stdin: {exc}") from exc

    if not args and not file_path and _stdin_has_data():
        raw = sys.stdin.read().strip()
        if not raw:
            return {}
        try:
            return json.loads(raw)
        except json.JSONDecodeError as exc:
            raise ArgParseError(f"Invalid JSON from stdin: {exc}") from exc

    if file_path:
        path = Path(file_path)
        if not path.exists():
            raise ArgParseError(f"File not found: {file_path}")
        try:
            return json.loads(path.read_text())
        except json.JSONDecodeError as exc:
            raise ArgParseError(f"Invalid JSON in {file_path}: {exc}") from exc

    if not args:
        return {}

    # Single JSON object argument
    if len(args) == 1 and args[0].strip().startswith("{"):
        try:
            return json.loads(args[0])
        except json.JSONDecodeError as exc:
            raise ArgParseError(f"Invalid JSON argument: {exc}") from exc

    # Multiple args that look like a split JSON object (bad shell quoting)
    joined = " ".join(args)
    if joined.lstrip().startswith("{"):
        try:
            return json.loads(joined)
        except json.JSONDecodeError:
            pass  # fall through to key=value parsing

    prop_types = _extract_prop_types(schema) if schema else {}

    result: dict = {}
    for arg in args:
        if ":=" in arg:
            key, raw_val = arg.split(":=", 1)
            try:
                result[key] = json.loads(raw_val)
            except json.JSONDecodeError as exc:
                raise ArgParseError(f"Invalid JSON in {key}:= value: {exc}") from exc
        elif "=" in arg:
            key, val = arg.split("=", 1)
            result[key] = _coerce_value(val, prop_types.get(key))
        else:
            raise ArgParseError(
                f"Invalid argument '{arg}'. Use key=value, key:='{{json}}', or a JSON string."
            )
    return result


def _extract_prop_types(schema: dict[str, Any]) -> dict[str, str]:
    """Extract {property_name: json_type} from a tool's inputSchema."""
    props = schema.get("properties", {})
    return {
        name: defn.get("type", "string")
        for name, defn in props.items()
        if isinstance(defn, dict)
    }


def _coerce_value(val: str, target_type: str | None = None) -> Any:
    """Coerce a CLI string value to the type declared by the tool schema.

    When target_type is known, coerce precisely.  When unknown (no schema),
    apply conservative heuristics: bool, null, int, JSON structures.
    """
    # Explicit quotes → always string (user override)
    if len(val) >= 2 and (
        (val[0] == '"' and val[-1] == '"') or (val[0] == "'" and val[-1] == "'")
    ):
        return val[1:-1]

    if target_type is not None:
        return _coerce_to_type(val, target_type)

    return _coerce_without_schema(val)


def _coerce_to_type(val: str, target_type: str) -> Any:
    """Coerce val to a known JSON schema type."""
    if target_type == "string":
        return val

    if target_type == "boolean":
        low = val.lower()
        if low in ("true", "1", "yes"):
            return True
        if low in ("false", "0", "no"):
            return False
        return val

    if target_type == "integer":
        try:
            return int(val)
        except ValueError:
            return val

    if target_type == "number":
        try:
            return float(val)
        except ValueError:
            return val

    if target_type in ("array", "object"):
        try:
            parsed = json.loads(val)
            if target_type == "array" and isinstance(parsed, list):
                return parsed
            if target_type == "object" and isinstance(parsed, dict):
                return parsed
        except json.JSONDecodeError:
            pass
        return val

    if target_type == "null":
        if val.lower() == "null":
            return None
        return val

    return val


def _coerce_without_schema(val: str) -> Any:
    """Conservative coercion when no schema is available.

    Coerces: true/false → bool, null → None, pure digits → int,
    JSON arrays/objects → parsed.
    Does NOT coerce floats (too many false positives: DOIs, arXiv IDs, versions).
    """
    low = val.lower()
    if low == "true":
        return True
    if low == "false":
        return False
    if low == "null":
        return None

    if val.startswith(("[", "{")):
        try:
            return json.loads(val)
        except json.JSONDecodeError:
            pass

    try:
        return int(val)
    except ValueError:
        pass

    return val
