import inspect
import re
import typing
from typing import Any, Callable


# Mapping from Python types to JSON Schema type strings
_PYTHON_TO_JSON_TYPE: dict[type, str] = {
    str: 'string',
    int: 'integer',
    float: 'number',
    bool: 'boolean',
    list: 'array',
    dict: 'object',
    bytes: 'string',
    type(None): 'null',
}

# Reverse mapping from JSON Schema type strings to Python types
_JSON_TO_PYTHON_TYPE: dict[str, type] = {
    'string': str,
    'integer': int,
    'number': float,
    'boolean': bool,
    'array': list,
    'object': dict,
    'null': type(None),
}


def python_type_to_json_type(py_type: type) -> str:
    """Convert a Python type to a JSON Schema type string."""
    origin = typing.get_origin(py_type)
    if origin is list:
        return 'array'
    if origin is dict:
        return 'object'
    if origin is typing.Union:
        args = typing.get_args(py_type)
        non_none = [a for a in args if a is not type(None)]
        if non_none:
            return python_type_to_json_type(non_none[0])
        return 'null'
    return _PYTHON_TO_JSON_TYPE.get(py_type, 'string')


def json_type_to_python_type(json_type: str) -> type:
    """Convert a JSON Schema type string to a Python type."""
    return _JSON_TO_PYTHON_TYPE.get(json_type, str)


def parse_docstring(docstring: str | None) -> tuple[str, dict[str, str]]:
    """Parse a Google-style docstring into description and parameter descriptions.

    Args:
        docstring: The docstring to parse.

    Returns:
        A tuple of (description, {param_name: param_description}).
    """
    if not docstring:
        return '', {}

    lines = docstring.strip().split('\n')
    description_lines: list[str] = []
    param_descriptions: dict[str, str] = {}

    in_args = False
    in_other_section = False
    current_param: str | None = None

    section_headers = frozenset((
        'args:', 'arguments:', 'parameters:', 'params:',
        'returns:', 'return:', 'raises:', 'note:', 'notes:',
        'example:', 'examples:', 'yields:', 'yield:',
    ))

    for line in lines:
        stripped = line.strip()

        if stripped.lower() in section_headers:
            if stripped.lower() in ('args:', 'arguments:', 'parameters:', 'params:'):
                in_args = True
                in_other_section = False
            else:
                in_args = False
                in_other_section = True
            current_param = None
            continue

        if in_args:
            match = re.match(r'\s+(\w+)(?:\s*\([^)]*\))?\s*:\s*(.*)', line)
            if match:
                current_param = match.group(1)
                param_descriptions[current_param] = match.group(2).strip()
            elif current_param and stripped:
                param_descriptions[current_param] += ' ' + stripped
        elif not in_other_section:
            if stripped:
                description_lines.append(stripped)

    return ' '.join(description_lines), param_descriptions


def mcp_schema_to_parameters(input_schema: dict[str, Any] | None) -> dict[str, dict[str, Any]]:
    """Convert an MCP ``inputSchema`` (JSON Schema) to callite's parameter format.

    The returned dict maps parameter names to dicts with keys:
    ``type``, ``description``, ``required``, and optionally ``default``.

    Args:
        input_schema: The ``inputSchema`` dict from an MCP ``Tool`` object.

    Returns:
        A parameter dict compatible with callite's ``__describe__`` output.
    """
    if not input_schema:
        return {}

    properties: dict[str, Any] = input_schema.get('properties', {})
    required_set: set[str] = set(input_schema.get('required', []))
    parameters: dict[str, dict[str, Any]] = {}

    for pname, pinfo in properties.items():
        param: dict[str, Any] = {
            'type': pinfo.get('type', 'string'),
            'description': pinfo.get('description', ''),
            'required': pname in required_set,
        }
        if 'default' in pinfo:
            param['default'] = pinfo['default']
        parameters[pname] = param

    return parameters


def extract_tool_metadata(
    handler: Callable[..., Any],
    description: str | None = None,
    method_name: str | None = None,
) -> dict[str, Any]:
    """Extract MCP tool metadata from a Python function.

    Inspects the function's type hints, signature, and docstring to produce
    a metadata dict suitable for __describe__ responses and MCP tool registration.

    Args:
        handler: The function to extract metadata from.
        description: Explicit description override. Falls back to docstring.
        method_name: Explicit method name override. Falls back to __name__.

    Returns:
        A dict with keys: name, description, parameters.
    """
    name = method_name or getattr(handler, '__name__', 'unknown')
    doc_desc, param_docs = parse_docstring(getattr(handler, '__doc__', None))
    desc = description or doc_desc

    try:
        hints = typing.get_type_hints(handler)
    except Exception:
        hints = getattr(handler, '__annotations__', {})

    sig = inspect.signature(handler)

    parameters: dict[str, dict[str, Any]] = {}
    for param_name, param in sig.parameters.items():
        py_type = hints.get(param_name, str)
        if py_type is inspect.Parameter.empty:
            py_type = str

        param_info: dict[str, Any] = {
            'type': python_type_to_json_type(py_type),
        }

        if param_name in param_docs:
            param_info['description'] = param_docs[param_name]

        if param.default is not inspect.Parameter.empty:
            param_info['default'] = param.default
            param_info['required'] = False
        else:
            param_info['required'] = True

        parameters[param_name] = param_info

    return {
        'name': name,
        'description': desc,
        'parameters': parameters,
    }
